Appearance
Queuing Server Actions
This article describes how to use a queue pattern to time server actions more precisely.
Appearance
This article describes how to use a queue pattern to time server actions more precisely.
PlayerModel diverge. The server cannot directly modify checksummed PlayerModel fields without causing a Desync.A common need in game server programming is modifying a player's PlayerModel from the server, for example granting a reward, applying a penalty, or updating state in response to a server-side event. The Metaplay SDK provides server actions to solve this without causing a Desync.
A synchronized server action executes at the same logical time on both the server and client, keeping them in sync. This is the recommended default for most server-initiated model changes.
For cases where you need more control over when the client processes a change, for example waiting until the player is on the main game screen before showing a reward, you can use a queue pattern with unsynchronized server actions. The pattern works as follows:
[NoChecksum] queue of pending operations in PlayerModel.UnsynchronizedServerAction to add operations to the queue.PlayerAction at an appropriate time.Below, we walk through this pattern using an example where the player receives a gift. The gift could originate from another player, result from an event, or any other reason.
Alternative: In-Game Mail
For simpler cases like granting items or sending messages, you can use the LiveOps Dashboard mail system instead of implementing custom server actions. Check out Implementing In-Game Mail for more information.
You can't directly modify the player's PlayerModel, as it would result in a Desync. Instead, we'll update it indirectly via a queue or pending state from which the client claims the result (either via PlayerAction or automatically).
This is the class representing the gift we wish our player to receive:
[MetaSerializable]
public class Gift
{
[MetaMember(1)] ItemId ItemId { get; private set; }
[MetaMember(2)] int Count { get; private set; }
Gift() {}
public Gift(ItemId itemId, int count)
{
ItemId = itemId;
Count = count;
}
}To store it, we'll create the aforementioned NoChecksum queue of pending gifts in PlayerModel:
public class PlayerModel
{
// Using [NoChecksum] to allow modifications by the server without Desyncs.
// Items are enqueued using ServerActions and dequeued using PlayerActions.
[MetaMember(120), NoChecksum] public Queue<Gift> PendingGifts;
}Since PlayerModel.PendingGifts uses the [NoChecksum] attribute, we can write to it from the server without causing a Desync. It's okay to do this because we don't need the gifts to be added immediately, so the fact that the queues in the client and server will be different won't matter to us. Since we'll add gifts to this queue with an UnsynchronizedServerAction, we know the client's player model will receive them eventually.
INFO
Note When using [NoChecksum] variables for server-to-client communication, you should take care to prevent unexpected behavior. For this reason, try to keep the data flow as simple as possible. For example, a one-way FIFO queue, always written to by the server and read from the client, will always stay consistent even when the server writes new events into the queue while the client is popping previous ones.
The next step is to add the action that appends the gift to the player's PendingGifts queue. The UnsynchronizedServerActions get executed on different ticks (and timestamps) on the server and the client. They always run on the server first and on the client later.
[ModelAction(ActionCodes.PlayerGiftReceived)]
public class PlayerGiftReceived : PlayerUnsynchronizedServerAction
{
// Gift to append to PendingGifts queue
public Gift Gift { get; private set; }
public PlayerGiftReceived() { }
public PlayerGiftReceived(Gift gift) { Gift = gift; }
public override ActionResult Execute(PlayerModel player, bool commit)
{
if (commit)
player.PendingGifts.Enqueue(Gift);
return ActionResult.Success;
}
}The PlayerGiftReceived action is executed on the server in PlayerActor.cs (or in OfflineServer.cs, if running in Offline Mode).
// Enqueue the gift to PlayerModel (also gets run on client)
ExecuteServerAction(new PlayerGiftReceived(someGift));The client can then check for any incoming pending gifts in its update loop and handle them as necessary, as we'll see in the next section.
Tip
It's better to use polling (e.g., checking the state manually from time to time) for incoming pending events instead of triggering and listening to events when receiving gifts. Polling is more robust in the face of errors such as losing network connectivity. The polling will also automatically handle cases where, after logging in, there are already pending items in the queue.
Finally, the player claims the pending gift with a PlayerClaimPendingGift action, which dequeues the first item from PendingGifts and adds its contents to the player's inventory:
[ModelAction(ActionCodes.PlayerClaimPendingGift)]
public class PlayerClaimPendingGift : PlayerAction
{
public PlayerClaimPendingGift() { }
public override MetaActionResult Execute(PlayerModel player, bool commit)
{
// Must have an item in PendingGifts queue to claim
if (player.PendingGifts.Count == 0)
return ActionResult.NoPendingGifts;
if (commit)
{
// Pop first received gift from queue
Gift gift = player.PendingGifts.Dequeue();
// Add received gift to player's inventory.
player.Inventory.AddItems(gift.ItemId, gift.Count);
}
return ActionResult.Success;
}
}Often, there is an animation or dialog with a button to press associated with receiving items. It is usually best to invoke the PlayerClaimPendingGift Action at the end of the desired interaction so that, if the client crashes, the player will still be able to see the animation or tap the button upon restarting the game. Otherwise, the reward might be added to the player's inventory without them realizing it.
Besides modifying models from the server, you can use the in-game mail and broadcast systems to send messages and gifts, either individually or en masse, using the dashboard. The Implementing In-Game Mail page details how to integrate the base system and Managing Broadcasts looks at using the broadcast system to send messages to a large number of players.
Starting with Game Server Architecture, we have a whole roster of pages on server programming you can check out in the Game Server Programming section of the sidebar. This includes information about Entity Schema Versions and Migrations, Custom Entities, Database Scan Jobs and more.