Appearance
Appearance
Although you can implement much of the game's functionality on shared code, some processes are better left to the server. Logic that you wish to keep secret, like chest randomization or rewards, will be inaccessible even to hacked clients if you run them only on the server. This page will teach you how to transmit these results to the player model.
As the player model is updated by the client first and only later by the server, any modifications originating on the server require some extra steps to avoid causing Desyncs. There are three approaches to server-side player model updates:
PlayerModel
DirectlyYou can also use a hybrid approach to implement a queue pattern. Both synchronized and unsynchronized server actions run at a later timestamp than they are initiated, which allows for a little less control than might be necessary in some cases. Using a queue pattern allows you to store some pending operations on the client that can be initiated on client-side code, thus taking effect for the player instantly.
The SDK computes checksums of models based on all members not marked with the [NoChecksum]
attribute. This checksum is calculated on both the server and client to ensure the client's calculations have the same results as the authoritative execution on the server. When the client and server's checksums differ, a Desync is triggered.
To successfully modify a checksummed member without causing a Desync, the action must execute simultaneously on both the client and the server. You can achieve this by issuing a SynchronizedServerAction
, which is first sent to the client to determine the shared timeline position and executed only at a later, shared time. You can use this approach to modify any member.
Race condition warning
As the actions need to be sent to the client before execution, the time from the action issue to the execution can grow large, especially if the client app has been put into the background or if there are poor network conditions. You should be careful to check that any preconditions still hold by the time the action is executed.
For example, if we wanted to give a player some extra gold to celebrate their birthday, we could do the following:
class PlayerModel
{
....
[MetaMember(103)] public int NumGold;
}
[ModelAction(101)]
public class PlayerAddGold : PlayerSynchronizedServerActionCore<PlayerModel>
{
public int NumToAdd { get; private set; }
PlayerAddGold() { }
public PlayerAddGold(int numToAdd) { NumToAdd = numToAdd; }
public override MetaActionResult Execute(PlayerModel player, bool commit)
{
if (commit)
{
player.NumGold += NumToAdd;
}
return MetaActionResult.Success;
}
}
On the server, we use the EnqueueServerAction
method instead of ExecuteAction
. This happens since the action doesn't execute instantly and needs to be put in a queue.
void OnHappyBirthday()
{
EnqueueServerAction(new PlayerAddGold(100));
}
Pro tip!
The synchronized actions are not required to inherit from the helper PlayerSynchronizedServerActionCore<>
class. Instead, the Action is only required to have the [ModelActionExecuteFlags(ModelActionExecuteFlags.FollowerSynchronized)]
flag defined. This can be useful for reusing actions for multiple modes by adding different ModelActionExecuteFlags
.
Besides synchronized actions, you can also use UnsynchronizedServerAction
.The server will perform the action immediately, and the client will do it as soon as possible. We call this an “unsynchronized action” as the client may already have advanced on the timeline, and hence, the action would get executed later on the client than on the server. As the execution time may vary, the action may only modify a [ServerOnly]
or [NoChecksum]
member, or risk causing a Desync. Editing other members will raise a warning if debug checks are enabled.
For example, let's say we have a tip-of-the-day string on the player model that we'd like to update and have the client be able to see it:
class PlayerModel
{
...
// Using [NoChecksum] to allow modifications by the server without Desyncs.
[MetaMember(102), NoChecksum] public string TipOfTheDay;
}
In this case, we can create an UnsynchronizedServerAction
to update it.
[ModelAction(101)]
public class PlayerUpdateTotd : PlayerUnsynchronizedServerActionCore<PlayerModel>
{
public string TipOfTheDay { get; private set; }
PlayerUpdateTotd() { }
public PlayerUpdateTotd(string tipOfTheDay) { TipOfTheDay = tipOfTheDay; }
public override MetaActionResult Execute(PlayerModel player, bool commit)
{
if (commit)
{
player.TipOfTheDay = TipOfTheDay;
}
return MetaActionResult.Success;
}
}
To run the action, we use ExecuteServerActionImmediately
on the PlayerActor.cs
script.
void OnLoginOrAtMidnight()
{
string tipOfTheDay = TipGenerator.Instance.GetTipForDay(DateTime.UtcNow.TotalDays);
ExecuteServerActionImmediately(new PlayerUpdateTotd(tipOfTheDay));
}
Pro tip!
Like the synchronized actions, unsynchronized actions are not required to inherit from the helper PlayerUnsynchronizedServerActionCore<>
class. Instead, the SDK only requires the action to have a [ModelActionExecuteFlags(ModelActionExecuteFlags.FollowerUnsynchronized)]
flag defined. You can use this to repurpose the actions for multiple execution modes.
Lastly, you can update the PlayerModel
state from the server by modifying it directly. The main drawbacks are:
[NoChecksum]
fields.For example, if we add a member to the player model that tracks the number of times a player has logged in:
class PlayerModel
{
....
[MetaMember(101)] public int NumTimesLoggedIn;
}
We could do the following to update it:
void OnSessionStart()
{
// In this case, even though NumTimesLoggedIn is a checksummed field,
// the change doesn't trigger a desync as this method is only called
// when a player is logging in.
Model.NumTimesLoggedIn++
}
The client, however, does not automatically become aware of such direct modifications. If there is an ongoing session, only the old value will be available to the client until the start of the next session. As such, a direct modification to any member not marked as [ServerOnly]
or [NoChecksum]
will cause a Desync. Hence, you should only update such members when the client is known to be offline, such as in login hooks like the previous example, or when updating server-only members for internal bookkeeping.
Tip
You can terminate a player's session manually for rare one-off operations. The values will then be updated when the client reconnects.
If you need to kick a player from the game for any reason you can use the KickPlayerIfConnected
method:
KickPlayerIfConnected(PlayerForceKickOwnerReason.AdminAction);
In addition to the low-level action primitives, you can achieve a more fine-grained action timing by utilizing a queue pattern. The pattern works as follows:
[NoChecksum]
queue of pending actions or operations in PlayerModel
.UnsynchronizedServerAction
to add operations to the queue.PlayerAction
. You can delay initiating the action until the client is in a suitable state, such as on the main game screen.Below, we explain the steps using an example where the player receives a gift. The gift could originate from another player, result from an event, or any other reason you'd like.
When to use what
Synchronized server actions are the default recommended way to modify shared models from any event originating on the server, be it player interaction or autonomous operations. However, you can still grant players gifts or send them messages manually using the LiveOps Dashboard mail system. 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 situation. 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 UnsynchronizedServerAction
s 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 the 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 on the Game Server Programming section of the sidebar. This includes information about Entity Schema Versions and Migrations, Custom Server Entities, Database Scan Jobs and more.