I am trying to include TBMP into my Mahjong game and this issue has been stuck with me for a few months now, and I am no closer to resolving it.
I have four players in a game and the game progressing counterclockwise. So imagine you have four players at a table:
Player 3
---------------------------
Player 4 | | Player 2
---------------------------
Player 1
Now Player 1 takes a turn, the data is pushed via JSON into the game data, and Player 2 gets notified that it is his turn. Player 3 and 4 also get a notification that a match update has occurred, but when they query the game data, it returns the stale game information, unless it becomes their turn, then they get the accurate and current game data.
I would like all players to be updated after every turn, rather than getting all the updates in one go when it becomes their turn.
I register an update listener as follows:
mTurnBasedMultiplayerClient.registerTurnBasedMatchUpdateCallback(mMatchUpdateCallback);
I have a function that processes the update as follows:
private TurnBasedMatchUpdateCallback mMatchUpdateCallback = new TurnBasedMatchUpdateCallback() {
#Override
public void onTurnBasedMatchReceived(#NonNull TurnBasedMatch turnBasedMatch) {
int turnStatus = turnBasedMatch.getTurnStatus();
// OK, it's active. Check on turn status.
switch (turnStatus) {
case TurnBasedMatch.MATCH_TURN_STATUS_MY_TURN:
MainActivity.mTurnData =
MahjongTurn.unpersist(turnBasedMatch.getData());
setLocalGameValues();
state = GameState.Playing;
return;
case TurnBasedMatch.MATCH_TURN_STATUS_THEIR_TURN:
MainActivity.mTurnData =
MahjongTurn.unpersist(turnBasedMatch.getData());
setLocalGameValues();
state = GameState.MultiWait;
return;
}
}
#Override
public void onTurnBasedMatchRemoved(#NonNull String matchId) {
game.showToast("A match was removed.");
state = GameState.MultiWait;
return;
}
};
Anyway, the listener is registered correctly and I can see that my listener gets called, but the call to turnBasedMatch.getData() only returns the correct game data when it is also the player's turn.
So Player 4 gets the updates from Player 1, Player 2 and Player 3 only when it becomes his turn. He gets notified that there is an update to the match, but has no way of knowing what the current game data looks like.
Is this the way it is meant to work? Am I doing something wrong?
One workaround for this is to store all the turns inside the message data. This way, I can treat the game data like a Message Queue and replay everything up to the current turn. This is not ideal, but it works.
Related
My goal is to pause the Current track right after it finishes, but the default behavior of playlist playback will not pause until the whole Playlist is finished.
I've tried using onPositionDiscontinuity() but it is called after the track has changed to the next one.
override fun onPositionDiscontinuity(reason: Int) {
super.onPositionDiscontinuity(reason)
if (reason == SimpleExoPlayer.DISCONTINUITY_REASON_PERIOD_TRANSITION) {
Log.v("xxx", "called!") //not called at the end of current track
}
}
And it seems like not supported natively (by official):
https://github.com/google/ExoPlayer/issues/3773
You can use the setPauseAtEndOfMediaItems method available on SimpleExoplayer.Builder like so:
player = SimpleExoPlayer.Builder(context)
.setPauseAtEndOfMediaItems(true)
.yourOtherOptions()
.build()
Unfortunately, there is no direct callback available to notify the end of the last frame of the current track. The only thing available with the ConcatenatingMediaSource, to know the end of a track is onPositionDiscontinuity(), but as you know that would be dispatched only after the first frame of the next track is already rendered. So in that case I think we can have the below possibilities wrt your use case:
Use VideoFrameMetadataListener interface and override the onVideoFrameAboutToBeRendered(), which would be called on the playback thread when a video frame is about to be rendered. In your case just before the next track rendering. link
Get the track duration [getDuration()] and keep getting the current playback position using getCurrentPosition() in every second(or any other time interval). And pause the playback when it returns the specified time. You can use a CountDownTimer for this and in the timer callback, onTick(), invoke getCurrentPosition() for the current track.
Use PlayerMessage to fire events at specified playback positions: The playback position at which it should be executed can be set using PlayerMessage.setPosition.link
Use onMediaItemTransition(): Called when playback transitions to another media item. Here generally we update the application’s UI for the new media item. So instead of updating the UI, we can pause the playback. Not sure if this gets called before or after onPositionDiscontinuity(). Feasibility needs to be verified.
While playing videos from a ConcatenatingMediaSource playlist, I would like the player to pause automatically at the start of a new item eg. not automatically playing it.
Using the demo application, I modified onPositionDiscontinuity function to detect current item change:
int currentPosition = 0;
#Override
public void onPositionDiscontinuity(#Player.DiscontinuityReason int reason) {
if (inErrorState) {
// This will only occur if the user has performed a seek whilst in the error state. Update
// the resume position so that if the user then retries, playback will resume from the
// position to which they seeked.
updateResumePosition();
}
if (player.getCurrentWindowIndex() != currentPosition) {
currentPosition = player.getCurrentWindowIndex();
player.setPlayWhenReady(false);
}
}
While this code pauses the player, it does not clear the surface view used by the player hence we are still seeing the last frame of the previous video. I suppose this callback is invoked too soon, but that's the only callback I found which was always invoked on playlist item change (onPlayerStateChanged might not be invoked).
How can I have the first frame of the newly current item displayed instead of the previous item last frame?
My weak workaround is to delay invocation of 200ms with Handler().postDelayed({ mPlayer?.playWhenReady = false }, 200).
This is not supported yet:
The problem with using the event listener is that there is no way to ensure the request to pause in your event listener
is handled while the first frame of the new playlist item is showing.
If stopping at approximately the right frame is fine, your solution of
pausing the player on position discontinuity looks fine, but I think
there's no guarantee that the video renderer will have kept up with
the player position which is why you still see a frame from the
previous source at the point of pausing the player. For what it's
worth, in my testing the video renderer did advance to the first frame
of the next playlist item before the request to pause was handled.
A couple of other suggestions:
You could try customizing your MediaSource to insert a position discontinuity at the start of each period. I think then you'd get
onRenderedFirstFrame at the start of each item. If you pause the
player there you can guarantee to pause the player at a frame in the
new playlist item.
To get this to work perfectly, in the sense that the first frame of the new playlist item is shown and the player pauses before any
other frame is shown, it will be necessary to coordinate showing a
frame and blocking further rendering (on the playback thread) with
pausing the player (on the thread your app is using to interact with
the payer). This will require a bit of code, but I think it is
probably possible roughly as follows: subclass MediaCodecVideoRenderer
and override onStreamChanged to get the time offset of the new
playlist item. Then override processOutputBuffer. In this method you
can detect when the first frame of the new stream has been rendered
(based on the time offset) and then prevent processing any output
frames until you've paused the player on your app's thread. After the
request to pause has been handled and the renderer is stopped
(onStopped) you can unblock processOutputBuffer.
We could also look at supporting this directly in the player, but it
will likely be a low priority at least for the moment.
I am trying to make a four player turn based game.
Now in case their are less than 4 human player I want to start X human player and 4-X AI player. Is it possible with the google turn based multiplayer https://developers.google.com/games/services/android/turnbasedMultiplayer
Also in case a human player leaves I want to replace it with the AI player instead of cancelling the game. Does it support this ?
I am implementing this and have it working 90%. When it is a new turn in my game, I check if the next player is a BOT or HUMAN player. This is prearranged using a seating algorithm, so that I space out the HUMANs neatly. i.e. opposite when there are two human players. When the next player is a BOT, I take a turn, but do not advance the Particpant. That way it will persist and send an update without passing the turn to the other player. When the next player is a HUMAN, I do a take Turn and switch to the next player. The code looks something like this....
if (Players[playerTurn%4]==BOT) {
Games.TurnBasedMultiplayer.takeTurn(MainActivity.mHelper.getApiClient(), MainActivity.mMatch.getMatchId(),
MainActivity.mTurnData.persist(), myParticipantId).setResultCallback(
new ResultCallback() {
#Override
public void onResult(TurnBasedMultiplayer.UpdateMatchResult result) {
processResult(result);
}
});
state = GameState.Playing;
} else {
Games.TurnBasedMultiplayer.takeTurn(MainActivity.mHelper.getApiClient(), MainActivity.mMatch.getMatchId(),
MainActivity.mTurnData.persist(), nextParticipantId).setResultCallback(
new ResultCallback<TurnBasedMultiplayer.UpdateMatchResult>() {
public void onResult(TurnBasedMultiplayer.UpdateMatchResult result) {
processResult(result);
}
});
state = GameState.multiplayerWait;
}
I am not sure how to swap inactive players with another player. I don't think this is possible unless you run your own server or use something like Parse.
I am working on a multiplayer domino game where a device acts as a host and makes the decisions for the rest of the players. So far, it works great when everyone has a good connection, but once you introduce some latency, all things go to heck. One problem I'm having is that I need to move some sprites around on command. When everyone has played, the host device will send a message out that tells the remote players who won the hand. After that message is sent, the winner can make the next move.
I move the dominoes to the right using:
void HelloWorld::onChatReceived(AppWarp::chat chatevent)
{
if (chatprefix.compare("_determinewinner_")==0)
{
//chatstring in this case is the playerID who won the hand
MultiPlayerdetermineTrickWinner(atoi(chatstring.c_str()));
}
}
void HelloWorld::MultiPlayerdetermineTrickWinner(int winningplayer)
{
...
for (int i = 0; i <marray_table->count(); ++i)
{
Domino *marray_table_tile = (Domino *)(marray_table->objectAtIndex(i));
CCMoveTo *translate = CCMoveTo::create(0.1f,ccp(pos_x,pos_y));
CCRotateBy *rotleft=CCRotateBy::create(0.1f, 90*int_rotate);
marray_table_tile->runAction(CCSequence::create(translate,rotleft,NULL));
}
...
}
When the players selects a tile, I call
CCMoveTo *translate = CCMoveTo::create(0.1f,ccp(pos_x,pos_y));
selectedtile->runAction(CCSequence::create(translate,NULL));
marray_table->addObject(selectedtile);
The problem I have is, that there are times when the latency is so bad, that the runaction never completes and the tiles don't actually make it to their final position. For instance, if the host player won the hand and makes their move before the other players received the "determinewinner" message. So short of having all the players tell the host when it can move on, how do I schedule a function to occur only after all actions have been completed?
I wrote a music player using OpenSL ES. It works fine besides one warning message coming out of libOpenSLES library. Here is the message.
03-05 00:10:15.367: W/libOpenSLES(12055): Missed SL_PLAYEVENT_HEADATNEWPOS for position 7000; current position 724009
...
03-05 00:10:27.226: W/libOpenSLES(12055): Missed SL_PLAYEVENT_HEADATNEWPOS for position 329015; current position 816013
It comes when I seek a media track. Sometimes I can seek with no warning, sometimes the message appears in the log.
Implementation is very simple. At initialization I pick up the seek control.
SLObjectItf decoder;
SLSeekItf seek;
...
SLresult result = (*decoder)->GetInterface(decoder, SL_IID_SEEK, &seek);
Then later, when user changes track position, I call SetPosition method as following.
SLresult result = (*seek)->SetPosition(seek, position, SL_SEEKMODE_ACCURATE);
Both calls return success result, and position change works all the time too. The only issue is the warning message mentioned above.
Any ideas why this message comes and how to avoid it?
Update:
Although the half of bounties were automatically assigned, the question is not yet answered. We don't know what causes the issue and how to avoid it.
A quick google of part of that log message found the following code snip, with your log print in the middle here, (complete source):
// nextVirtualMarkerMs will be set to the position of the next upcoming virtual marker
int32_t nextVirtualMarkerMs;
if (mObservedPositionMs <= virtualMarkerMs && virtualMarkerMs <= positionMs) {
// we did pass through the virtual marker, now compute the next virtual marker
mDeliveredNewPosMs = virtualMarkerMs;
nextVirtualMarkerMs = virtualMarkerMs + mPositionUpdatePeriodMs;
// re-synchronize if we missed an update
if (nextVirtualMarkerMs <= positionMs) {
SL_LOGW("Missed SL_PLAYEVENT_HEADATNEWPOS for position %d; current position %d",
nextVirtualMarkerMs, positionMs);
// try to catch up by setting next goal to current position plus update period
mDeliveredNewPosMs = positionMs;
nextVirtualMarkerMs = positionMs + mPositionUpdatePeriodMs;
}
notify(PLAYEREVENT_PLAY, (int32_t) SL_PLAYEVENT_HEADATNEWPOS, true /*async*/);
If you search pass a certain point there's some catching up to be done (jump) and perhaps an update was missed, that's what the log is stating, that we need to resynchronize the marker position.
Update
The whole code snip above is for calculating the oneshot (a stable temporary state) pause time if interpret the code correct, definition of oneshot, row 116:
// deferred (non-0 timeout) handler for SL_PLAYEVENT_*
// As used here, "one-shot" is the software equivalent of a "retriggerable monostable
// multivibrator" from electronics. Briefly, a one-shot is a timer that can be triggered
// to fire at some point in the future. It is "retriggerable" because while the timer
// is active, it is possible to replace the current timeout value by a new value.
// This is done by cancelling the current timer (using a generation count),
// and then posting another timer with the new desired value.
And you get the log message because of jumping to often for the code to catch up, I think. So try to not jump that often? By the way it's a warning log, it will catch up the next time in this code path.