Appearance
Appearance
The Metaplay SDK uses special classes called Actions to modify data by running code on both the client and the server to ensure equal results and server-authoritative gameplay. This means Actions need to have deterministic code to guarantee equal results on the client and server, and some special care needs to be taken to enforce that.
Whenever the player or any client event triggers an Action, it is executed immediately on the client and sent to be remotely run on the server. This way, the clients can provide immediate feedback to players without any network delays, while the server securely runs all Actions to prevent hacked clients from cheating in the game.
The Metaplay SDK automatically keeps track of the execution Ticks, or timestamps, to make sure the server and client models stay deterministically in sync. In the case of client and server de-syncs (for example, due to a hacked client or non-deterministic game logic), the server state keeps the game secure.
Deterministic Code
Some commonly used features are not, in fact, deterministic, such as most random number generators and floating-point operations. To address this, the Metaplay SDK provides deterministic RandomPCG
and F32
that can be serialized with the player model.
An example of something that would be done with a PlayerAction
in a Metaplay-powered game is buying an item from the game's store.
First, the Action checks if the player has enough gold for the item they're trying to buy and if so, remove the gold from the player's wallet and add the requested item to their inventory.
[ModelAction(typeCode: ActionCodes.BuyItemAction)]
public class BuyItemAction : PlayerAction
{
public ItemId Item { get; }
[MetaDeserializationConstructor]
public BuyItemAction(ItemId item)
{
Item = item;
}
public override MetaActionResult Execute(PlayerModel player, bool commit)
{
int cost = FindCostForItem(player, Item);
if (player.Gold < cost)
return ActionResult.NotEnoughGold;
if (commit)
{
player.Gold -= cost;
player.Inventory.Add(Item);
}
return ActionResult.Success;
}
}
Let's break this all down:
commit
flag on the Execute
method. The Action should only apply its effects if commit
is set to true. Otherwise, the method should only do a dry-run, which can be used to check whether the Action would complete successfully, like checking if the player has enough resources, for example.[ModelAction(typeCode)]
attribute must be included in all Action classes. The typeCode
must resolve to an integer value, and this value must be unique within the game as the SDK uses it to uniquely identify the Action in serialized form.PlayerAction
, which is the base class for all Actions operating on PlayerModel
. A game may include multiple such domains, each with its own XxxModel
class and a corresponding XxxAction
base class for its Actions.Execute()
must check the validity of the Action. If any validation attempts fail, a corresponding ActionResult
value should be returned (or ActionResult.Success
upon successfully executing the Action).MetaActionResult
type, and ActionResult
is a static class containing all the game-specific error codes.The members of an Action (such as Item
here) should be declared as C# properties, with a public getter and the members should be immutable, i.e., the members should not be modified after the creation of the Action.
For Actions with members, it is good practice to use deserialization constructors. This constructor should take the values for all the Action's members as inputs and then initialize them. It should also have the MetaDeserializationConstructor
attribute.
Safety First!
Hacked clients could send any Actions with arbitrary arguments to the server, so it is important that the shared logic fully validates that the Actions are legitimate. For example, if we instead let the client decide and send the cost to the server, a hacked client could set the cost to 0 or even a negative value.
When a user purchases an item, you might want to update the UI to show the item in their inventory. Actions, however, do not have access to game logic. There are two main approaches to resolve this: you can poll the Player Model for any changes, or you can use the client listener feature.
Polling is the easiest way to get data from the Player Model; however, it is also triggered every frame. Therefore, it is mostly useful for data that changes often.
For example, to visualize a gold counter for the user, we can poll the Player Model in the Update
loop and update the UI accordingly.
public class GoldCounter : MonoBehaviour
{
public TextMeshProUGUI GoldText;
void Update()
{
GoldText.text = MetaplayClient.PlayerModel.Gold.ToString();
}
}
Client listeners are useful for incremental or one-off changes; think about adding an item to the inventory or triggering an animation from an Action. Each project comes with an IPlayerModelClientListener
interface, which serves as a bridge between the Metaplay server logic and the game logic.
We'll take a look at how to use a client listener to update the inventory UI.
First, we have to define the method the listeners will invoke.
public class IPlayerModelClientListener
{
public void OnItemAdded(ItemId item);
}
By default, the ApplicationStateManager
implements the IPlayerModelClientListener
interface, so let's add the implementation there.
public class ApplicationStateManager : MonoBehaviour, IMetaplayLifecycleDelegate, IPlayerModelClientListener
{
...
public void OnItemAdded(ItemId item)
{
// Tell the UI layer to update the inventory view to include the newly added item.
UI.AddItemToInventory(item);
}
}
Lastly, we have to invoke the OnItemAdded
method from our Action.
[ModelAction(ActionCodes.BuyItemAction)]
public class BuyItemAction : PlayerAction
{
...
public override MetaActionResult Execute(PlayerModel player, bool commit)
{
int cost = FindCostForItem(player, Item);
if (player.Gold < cost)
return ActionResult.NotEnoughGold;
if (commit)
{
player.Gold -= cost;
player.Inventory.Add(Item);
player.ClientListener.OnItemAdded(Item);
}
return ActionResult.Success;
}
}
And there we have it! Your game can now respond to the behavior of your Actions.
Executing PlayerAction
s is quite simple, you can just invoke the ExecuteAction
method in your gameplay logic.
public void BuyButtonOnClick()
{
MetaplayClient.PlayerContext.ExecuteAction(new BuyItemAction(Item));
}
Guilds and Other Entities
Players are not the only game entities that benefit from the patterns of Models and Actions. For example, a guild has its own Model to manage its state and respective Actions to modify it.
The next step is to take a look at the game configs system. Game configs allow you to set up and edit your game's data separately from the game's code, so designers can iterate easily and you can add content without code updates. Following this page's example, in Setting Up Game Configs, we'll add an item class to the store and exemplify how to add products while the game is live. You can also further explore some of the features presented on this page:
[MetaSerializable]
attribute works.