Appearance
Appearance
Fundamentally, the execution of the game logic in Metaplay happens on a bounded model timeline, where all Ticks and actions move a model forward in the timeline, producing a logically new version.
On this page, we'll go over the general how-tos of Ticking and how models stay synchronized across the server and clients.
The Tick frequency is defined by setting a model's TicksPerSecond
to the desired value, which the framework then uses when deciding how often to call the model's Tick()
method. Higher frequency generally provides a smoother simulation but also causes more resources to be consumed on the backend, especially if lots of objects are updated on each Tick. You can also choose hybrid approaches, where only a subset of objects is updated on each tick to conserve CPU resources.
While some games might not require the Ticking mechanism at all, it should still be kept around. Metaplay's action synchronization and checksum comparison between client and server rely on Ticks being present.
Let's look at an example of using Ticks to implement an in-game timer that replenishes some player resources (i.e. energy, gold, or HP).
protected override void GameTick(IChecksumContext checksumCtx)
{
MetaTime curTime = CurrentTime;
foreach (RechargeableModel recheargeable in Rechargeables.Values)
TickRechargeable(recheargeable, curTime);
}
void TickRechargeable(RechargeableModel recheargeable, MetaTime curTime)
{
// Check whether a rechargeable resource's timer is completed
// \note If timers can be faster than a tick, then this should be done in a loop
if (curTime >= recheargeable.ReadyAt)
{
rechargeable.Value += rechargeable.ChargedAmount
// Invoke the listener on the client (in case it wants to show some gfx effect)
ClientListener.OnRecargeableCharged(recheargeable);
}
}
Metaplay keeps a copy of the timeline on both the client and the server so that all Ticks and actions are executed on both ends. This method forms the basis of the server-authoritative model which, in turn, prevents the clients from cheating by modifying their state.
The Ticks and actions of the PlayerModel
are first executed client-side, so the client can immediately react to any of the player's actions. This way of operating ensures the game still feels responsive to its players. After an action is executed on the client, it is then serialized and sent to the server to be executed a second time. The server-side copy of the PlayerModel
is always the authoritative one, thus even if a hacked client modifies its local state in memory, the cheats never propagate to the server.
⚠️ Bug Alert
The server may not directly modify the contents of PlayerModel
as that would cause the client and the server versions to diverge, resulting in a desynchronization error. To learn how to safely modify the PlayerModel
from the server, take a look at PlayerModel Server Updates.
While the client-first execution model is the default for PlayerModel
, it is not the only way of operating. For example, a real-time multiplayer game might use a model where the server drives the updates and the clients follow slightly behind. Such a model would be capable to keep all the clients fully synchronized.
Metaplay relies on both copies of the PlayerModel
to stay synchronized so that the execution model and validation work correctly. The timing and outputs of all Ticks and actions must remain identical on both sides. The framework itself synchronizes the timing, but properly producing identical outputs is your responsibility as the game's developer. The player model and player action code are shared between the player and server, so ensuring identical outputs on both boils down to being careful in situations where the same code might produce different outputs, which will be detailed in the next section.
Metaplay takes periodic checksums of both states to compare and verify that they remain the same. If the two states do not match, an out-of-sync event occurs. In these cases, Metaplay will automatically compare the two states and print out the difference between them. Although the resulting model state is still expected to be the same, the checksums don't consider the ActionResults
themselves for actions or Ticks. This means you can have differing ActionResults
on the client and the server if desired, such as for visualization purposes.
Here are the most common limitations caused by the synchronization that you might face, and how to deal with them:
HashSet<Key>
and Dictionary<Key, Value>
) do not have a well-defined iteration order, which in turn can lead to differing results across platforms or even app instances. To avoid this, such collections should always be processed in a deterministic order, which can be achieved by using either OrderedDictionary
(OrderedSet
) or SortedDictionary
(SortedSet
) instead. See Deterministic Collections for more details.RandomPCG
class that provides high-quality RNG with a tiny memory footprint. See Implementing Cheat-Proof Randomization for more information.System.DateTime
and System.Duration
classes are not deterministic. The Metaplay-provided MetaTime
and MetaDuration
should be used instead. They provide similar functionality to the system classes.As mentioned before, modifying the player model from the server directly would cause a desynchronization event. There is, however, a correct way to do this, as described in PlayerModel Server Updates.
Here's a recap of the pages mentioned in the last section to avoid the most common synchronization problems: