Appearance
Tutorial: Minimal SDK Integration
Add the Metaplay SDK into an existing game and start syncing player state.
Appearance
Add the Metaplay SDK into an existing game and start syncing player state.
For a minimal Metaplay SDK integration, we'll initialize the Metaplay SDK into the game and set up basic player logins to the game server. We will then synchronize the full player state to the server and add tracking of completed runs as the first game-specific player action.
At the end of this chapter, we will have integrated and initialized the Metaplay SDK in the Trash Dash project, making it ready to adopt live-ops focused SDK features in the next chapters.
The integration process is split into four steps:
To keep the tutorial focused, we will not dive deep into the SDK features themselves. Here are the concepts we'll be applying in this chapter, along with links for further reading if you're curious:
We'll integrate the Metaplay SDK to the project using the Metaplay CLI. You can follow the installation instructions from the GitHub repository. Then, we can use the CLI to initialize the SDK in the project.
Use the Metaplay CLI's init command to initialize the project:
MyProject$ metaplay init project --no-sampleThis command will initialize the SDK by adding the following files to the project:
Assets/
+---SharedCode/ Files shared between the client & server.
| +---Player/PlayerModel.cs Player state model.
| +---Player/PlayerActions.cs Player actions that update the player state.
Backend/ Game-specific backend projects.
+---Server/ Main game server C# project.
+---BotClient/ Headless bot client for testing.
MetaplaySDK/ Full Metaplay SDK source.
metaplay-project.yaml Metaplay project configuration file.The init command also adds the Metaplay SDK Unity package (full source in MetaplaySDK/Client/) to the Unity project.
To statically access the Metaplay client, we will create a MetaplayClient class that will be used to access the client instance from anywhere in the game.
Create a new MetaplayClient class and inherit from MetaplayClientBase<PlayerModel>:
/// <summary>
/// Helper class to access the Metaplay client.
/// Provides APIs that are statically typed
/// with the game-specific <see cref="PlayerModel"/>.
/// </summary>
public class MetaplayClient : MetaplayClientBase<PlayerModel> { }At this point, you can already use the CLI's dev server command to run the game server locally and visit the built-in LiveOps Dashboard. To stop the server, you can use ctrl + c or your shell's interrupt shortcut.
MyProject$ metaplay dev serverAfter the server has started, you can visit the built-in LiveOps Dashboard at http://localhost:5550.

With the SDK initialization done, we will next use the SDK to establish a connection to the game server so the client can sign in and maintain a session. This setup can easily be modified to fit inside your existing initialization flow.
We will add a MonoBehaviour to handle the active persistent connection service to the bootstrap/start scene and call it MetaplayService. This will be a singleton that owns the SDK client instance and exposes the connection state and helpers. We'll initialize the SDK client on startup, and handle connectivity changes like attempting reconnects on failures and resetting to the start scene on session loss.
In the new MetaplayService MonoBehaviour, inherit from IMetaplayLifecycleDelegate and IPlayerModelClientListener, and implement the required methods:
public partial class MetaplayService : MonoBehaviour, IMetaplayLifecycleDelegate, IPlayerModelClientListenerWhen the game starts, initialize the connection to the server:
private async void OnEnable()
{
await InitUnityServices();
// Don't destroy this GameObject when loading new scenes.
DontDestroyOnLoad(gameObject);
// Start connecting to the Metaplay server.
InitConnection();
}
public void InitConnection()
{
if (MetaplayClient.IsInitialized)
{
MetaplayClient.Deinitialize();
}
MetaplayClient.Initialize(new MetaplayClientOptions
{
// Hook all the lifecycle and connectivity callbacks back to this class.
LifecycleDelegate = this,
// Initialize Metaplay's IAP manager for usage in the store
IAPOptions = new MetaplayIAPOptions
{
EnableIAPManager = true,
}
// Check out the other members from MetaplayClientOptions,
// these are optional but have useful settings
// or provide useful information.
});
MetaplayClient.Connect();
}Initializing the IAP manager is not strictly necessary. The sample uses it to implement IAPs in the store, however we won't dive deeper into that in this tutorial.
When the session is established, hook up your client-side listener and load local state as needed:
/// <summary>
/// Callback invoked when the connection to the Metaplay server is established.
/// </summary>
public Task OnSessionStartedAsync()
{
// Hook this class as the listener for game-specific player model events.
MetaplayClient.PlayerModel.ClientListener = this;
// Game Specific
// Initialize and load the player state
PlayerData.Create();
// Hook to trigger loading state transitions
ConnectionEstablished?.Invoke();
return Task.CompletedTask;
}To keep this example simple, we're not handling connection errors, but in your game you should handle them appropriately. Check out the links in the SDK Features Quick Reference for more information.
With the connection management in place, you can test it in the Unity editor.
metaplay dev server.

Now that the client is connected to the server, we will modify the game to upload the player state to the game server and keep it synchronized
We'll use a simple snapshot pattern for player state so you can start syncing quickly without having to refactor all of your game logic at once. The project we're migrating currently tracks player state in PlayerData.cs. We'll keep this class, but instead of saving to a local file, we'll upload its data to the server. On the server, we'll store the client's state in the PlayerModel under PlayerModel.PlayerData, so the server always has an up-to-date copy of the player's progress.
Quick reminder
Actions are special classes that the SDK uses to modify and update models and are the primary vehicles for gameplay programming. Actions run on both the client and the server to ensure that all operations are legitimate and working as intended.
When a player connects for the first time, we'll migrate any existing offline progress by uploading their local save file to the server as a one-off operation.
Let's start by defining the player state on the PlayerModel:
/// <summary>
/// Class for storing the state and updating the logic for a single player.
/// </summary>
[MetaSerializableDerived(1)]
public partial class PlayerModel :
PlayerModelBase<
PlayerModel,
PlayerStatisticsCore
>
{
// Game-specific state
// MetaMember is Metaplay's serialization attribute,
// Each id should be unique within this class.
[MetaMember(103)] public ClientPlayerData PlayerData { get; set; }
}Next, we'll create an action to migrate the offline state to the server:
[ModelAction(ActionCodes.MigrateStateFromOffline)]
public class MigrateStateFromOfflineAction : PlayerAction
{
public ClientPlayerData OfflinePlayerData { get; set; }
public override MetaActionResult Execute(PlayerModel player, bool commit)
{
// Only allow migration if not already migrated
if (player.OfflineStateMigrated)
return ActionResult.UnknownError;
if (commit)
{
if (OfflinePlayerData != null)
player.PlayerData = OfflinePlayerData;
player.OfflineStateMigrated = true;
}
return MetaActionResult.Success;
}
}Lastly, when the game loads for the first time, we'll invoke the action we just created to migrate the local save file to the PlayerModel:
// Migrate offline state to PlayerModel as a once-off operation
if (!MetaplayClient.PlayerModel.OfflineStateMigrated)
{
MigrateStateFromOfflineAction migrationAction = new MigrateStateFromOfflineAction();
if (File.Exists(m_Instance.saveFile))
{
Debug.Log("Save file found on disk, migrating to cloud save.");
migrationAction.OfflinePlayerData = new ClientPlayerData();
MigrateFromSaveFile(migrationAction.OfflinePlayerData);
File.Delete(m_Instance.saveFile);
}
MetaplayClient.PlayerContext.ExecuteAction(migrationAction);
}That's all you need to store your player's data on the Metaplay server.
Let's do the same for the player's state when the game saves locally. We'll save a snapshot to the server by sending an UpdateClientAuthoritativeStateAction with the current state.
public void Save()
{
UpdateClientAuthoritativeStateAction syncAction = new UpdateClientAuthoritativeStateAction();
syncAction.LicenseAccepted = licenceAccepted;
syncAction.TutorialDone = tutorialDone;
syncAction.UsedTheme = usedTheme;
syncAction.UsedCharacter = usedCharacter;
syncAction.UsedAccessory = usedAccessory;
syncAction.FTUELevel = ftueLevel;
syncAction.PreviousName = previousName;
syncAction.Missions = missions.Select(x => x.ToPersisted()).ToList();
MetaplayClient.PlayerContext.ExecuteAction(syncAction);
}The UpdateClientAuthoritativeStateAction ends up being similar to MigrateStateFromOfflineAction, it copies the parameters to PlayerModel.PlayerData without further validation.
[ModelAction(ActionCodes.UpdateClientAuthoritativeState)]
public class UpdateClientAuthoritativeStateAction : PlayerAction
{
public bool LicenseAccepted { get; set; }
public bool TutorialDone { get; set; }
public string UsedTheme { get; set; }
public int UsedCharacter { get; set; }
public int UsedAccessory { get; set; }
public int FTUELevel { get; set; }
public string PreviousName { get; set; }
public List<PersistedMission> Missions { get; set; }
public override MetaActionResult Execute(PlayerModel player, bool commit)
{
if (commit)
{
player.PlayerData.LicenseAccepted = LicenseAccepted;
player.PlayerData.TutorialDone = TutorialDone;
player.PlayerData.UsedTheme = UsedTheme;
player.PlayerData.UsedCharacter = UsedCharacter;
player.PlayerData.UsedAccessory = UsedAccessory;
player.PlayerData.FTUELevel = FTUELevel;
player.PlayerData.PreviousName = PreviousName;
player.PlayerData.Missions = Missions;
player.LastPlayerDataSyncTime = player.CurrentTime;
}
return MetaActionResult.Success;
}
}About Server vs Client Authority
In a fully server-authoritative model, you would create specific mutation actions for each game operation. This integration uses a hybrid approach where some state is server-authoritative (like purchases and run history) and some is client-authoritative (like UI preferences), uploaded as snapshots. For production, add validation on the server to check snapshot diffs and rollback misbehaving clients to the authoritative server state if needed.
Now that the player state is synchronized to the server, you can inspect it in the LiveOps Dashboard.
metaplay dev server.model.PlayerData, which contains the player's state.
To demonstrate a flavor of syncing data to the server that is additive rather than a complete overwrite, in this step we'll add saving the history of all completed runs in the game.
Instead of uploading full snapshots, we'll use specific actions to track individual game events. Create a new action StartRunAction that subtracts the player's consumable from the server state and records the run configuration:
[ModelAction(ActionCodes.StartRun)]
public class StartRunAction : PlayerAction
{
public ConsumableType ConsumableUsed { get; }
public string Theme { get; }
public string Character { get; }
public string Accessory { get; }
[MetaDeserializationConstructor]
public StartRunAction(ConsumableType consumableUsed, string theme, string character, string accessory)
{
ConsumableUsed = consumableUsed;
Theme = theme;
Character = character;
Accessory = accessory;
}
public override MetaActionResult Execute(PlayerModel player, bool commit)
{
if (commit)
{
if (ConsumableUsed != ConsumableType.NONE)
player.PlayerData.Consumables[ConsumableUsed] -= 1;
player.CurrentRun = new Run() { ConsumableUsed = ConsumableUsed, Character = Character, Theme = Theme, Accessory = Accessory };
player.EventStream.Event(new RunStartedEvent(Theme, Character, Accessory));
}
return MetaActionResult.Success;
}
}Quick reminder
Actions are special classes that the SDK uses to modify and update models and are the primary vehicles for gameplay programming. Actions run on both the client and the server to ensure that all operations are legitimate and working as intended.
Then when a new run starts, we'll send the StartRunAction:
MetaplayClient.PlayerContext.ExecuteAction(
new StartRunAction(
characterController.inventory?.GetConsumableType() ?? ConsumableType.NONE,
m_CurrentThemeData.themeName,
player.characterName,
usedAccessory));When the run completes, we'll capture the run data and send the EndRunAction to save it to the server's run history:
MetaplayClient.PlayerContext.ExecuteAction(new EndRunAction(
didUseConsumable: trackManager.characterController.inventory == null,
didCompleteRun: true,
deathEvent.character,
deathEvent.obstacleType,
deathEvent.themeUsed,
deathEvent.coins,
deathEvent.premium,
deathEvent.score,
F64.FromFloat(deathEvent.worldDistance)));For brevity, we've chosen to exclude the EndRunAction from this tutorial. In short, the action rewards the player, adds the run data to the history, updates the highscores, and clears the current run data.
Further Cheat-Proofing Possible
Like the PlayerData snapshot, this implementation accepts the client's reporting of runs as-is. For production, add validation on the server to check run data (distance, coins earned, etc.) and catch cheaters.
Additionally, consider making the full run history server-only to avoid transferring it to the client on session start, and bound the history size by aggregating old runs into per-month or per-year summaries.
Like before, you can inspect the run history in the LiveOps Dashboard.
metaplay dev server.model.RunHistory, which contains the info about your runs.
We have now successfully added the Metaplay SDK into the Trash Dash sample project and implemented the basic integration of player logins and state persistence. We have taken special care to not refactor the sample's existing codebase to make it easier to follow the changes.
While we at Metaplay consider the initial integration to already be quite exciting, the real value of using Metaplay is of course in the game features that we can now implement.