Unity Gaming Services tutorial series part 6: Player data

Welcome back to our Unity Gaming Services tutorial series. In this sixth post in our series, you’ll learn about how to create, modify, and save player data. It covers workflows like:

  • Cloud Code setup: Create and deploy modules
  • Implementation: Player username input and server validation
  • Cloud Save integration: SaveData and GetData methods
  • Unity Analytics: Real-time player data

Where to find all of the content in the Unity Gaming Services tutorial series

The UGS series consists of 10 sets of tutorials, with each set comprising a video and written tutorial.

Video tutorials

The full video tutorial series will be here. We’ll add the video tutorials to this playlist throughout October.

Written tutorials

You’ll find links to all articles in this series as they become available in Part 1, the introduction post.

Demo project and scripts

You can find the Gem Hunter Match – Cloud Edition demo project in the GitHub page.

The scripts used in the tutorials can be downloaded here:

UGSTutorialSeries_Scripts.zip (1.6 MB)

Watch the video version of this tutorial here:

Use Unity Cloud Code to help protect against hackers

When a commercial game-as-a-service goes live, it can end up as an open target for cheaters and hackers who can ruin the experience for everyone. Any client-side code built from your Unity project is vulnerable to reverse engineering and manipulation.

For this reason, we recommend that you use Unity Cloud Code from the start. Cloud Code is a service for writing and running server-side code in the cloud, protecting against the potential hacking of your player logic and data.

Integrate Cloud Code early on in your project development to provide better security for your entire game and take advantage of powerful capabilities, like deploying updates to live games without requiring users to download patches.

This post takes you step-by-step through setting up Cloud Code. Then, you’ll see how to implement player username input with server-side validation, which will check that the name is the appropriate character length.

From there, you’ll learn how to integrate Unity’s Cloud Save service by using the SaveData and GetData method examples from the documentation. Finally, you’ll get tips for implementing Unity Analytics, which provides real-time insights on your players.

The aim is to familiarize yourself with the workflows for setting up client-server communication. This is different from typical self-contained Unity projects because it’s more like setting up a conversation between two different systems.

Cloud Code setup

The first step is to go to the Unity Package Manager in the Unity Registry and install the Cloud Code package along with its samples.

With the Package Manager window open, also install the Deployment package from the Unity Registry.

Switch to the Project Settings window and you should now see Cloud Code under Services, alongside the other services you would have enabled by going through the previous tutorials.

Click on Services > Cloud Code > Go to Dashboard to access Cloud Code setup guides and documentation from the Editor.

Download and set up .NET

Go to dotnet.microsoft.com/download and click on Download .NET 9.0. Go back to the Editor, click the menu dropdown Edit > Preferences, and select Cloud Code.
Note: On Windows it’s via Edit > Preferences and on macOS it’s Unity > Settings > Cloud Code.

In the .NET development environment section (via Edit > Preferences) modify your .NET path to the one you have installed – if you’ve been following along with this tutorial series, it should be Program Files\dotnet\dotnet.exe. Click Apply in the bottom right corner of the window to apply the changed path if necessary.

Create a C# module reference file

Go to the Project window and in the Assets folder right-click and choose Create > Services > Cloud Code C# Module Reference. Give it a name based on your project and add “cloud” to the end to keep things clear while you proceed.

Select the Cloud Code Module Reference file and take a look at the options available in its Inspector. Click the Generate Solution button towards the upper right corner of the Inspector. This will create your Cloud Code C# Module solution. This solution will be generated in a folder that has the same name as your reference (so, as suggested above, your project name + cloud). You’ll also see the Cloud Code Reference file in the Deployment window.

Now open your Unity Project folder (outside of the Assets folder using the Windows Explorer or Mac OS finder) and you’ll see the solution folder there. Inside that folder is the Cloud Code C# Module solution you generated, just like it appears in the Inspector. Right-click on the solution and open it in Visual Studio.

Note: If you don’t see your Solution Explorer in Visual Studio, Go to View > Solution Explorer and open up Example.cs.

Once you open the Example.cs script you’ll see that there is a Cloud Code function called SayHello. This function takes in a string and returns Hello(string name).

using Unity.Services.CloudCode.Core;

namespace HelloWorld;

public class MyModule
{
    [CloudCodeFunction("SayHello")]
    public string Hello(string name)
    {
        return $"Hello, {name}!";
    }
}

Keep this open. Go back to Unity, to the Deployment window, select your module and click Deploy Selected. You should see a status indication showing the module is up to date.

In the Inspector of your Cloud Code Module Reference file, click the Go to Dashboard button. In the dashboard, click on Cloud Code in the left-side menu to see your module uploaded.

Click on the module and you’ll see the SayHello method endpoint; it takes one parameter and returns a string. How do you actually call this method? It’s quite straightforward.

Go back to Unity; the first thing you need to do is generate code bindings from the Deployment window (you’ll need to do this anytime you have new Cloud Code functions or significant changes). Bindings are type-safe client code.

Click Generate Code Bindings in the Inspector of your Cloud Code Module Reference file. You should see your bindings generated in a Cloud Code folder (in your Assets folder via the Project window). What’s generated is a wrapper for your function calls, and now you’ll want to call this code from your game.

Let’s make a new script – you can create it in the Player Login folder that you created previously in the Assets folder – and call it PlayerDataManager.

Start with adding the following

Unity.Services.CloudCode;
Unity.Services.CloudCode.GeneratedBindings;

You then need to initialize Unity Services (which you should already be doing in your LoginManager script in Awake if you have been following this tutorial series). You only want to initialize the services once in your project, and it makes the most sense to do it in the LoginManager before you try signing in anonymously for a returning player, which of course uses the Unity Services.

You want to wait to call the Cloud Code until the player has successfully signed in because many of the actions you want to do in Cloud Code will require the player to be authenticated in one way or another.

In the LoginManager, let’s make an event for when the player has successfully signed in, called PlayerSignedIn and then let’s invoke the event after SignInAnonymouslyAsync().

//initialization that can be called with the event after succesful sign in

private async void InitServices() 
    {
        if (UnityServices.State == ServicesInitializationState.Uninitialized)
        {
            Debug.Log("Services Initializing");
            await UnityServices.InitializeAsync();
        }
    }

Go back to PlayerDataManager.cs. Note that at this point your bindings will by default be called MyModuleBindings.

You’ll need a reference to the LoginManager to subscribe to the PlayerSignedIn event, so let’s subscribe a method called InitializePlayer to PlayerSignedIn.

Next, initialize and cache your Cloud Code bindings by creating a new MyModuleBindings instance. Unsubscribe from the PlayerSignedIn event at OnDisable.

using System;
using UnityEngine;
using Unity.Services.CloudCode;
using Unity.Services.CloudCode.GeneratedBindings;
using Unity.Services.CloudCode.GeneratedBindings.UGSTutorialCloud;
using Newtonsoft.Json;
using System.Linq;

public class PlayerDataManager : MonoBehaviour
{
public LoginManager LoginManager;
private UGSTutorialCloudBindings UGSTutorialBindings;
public string PlayerName;

    private void OnEnable()
    {
        LoginManager.PlayerSignedIn += InitializePlayer;
    }

    void Start()
    {
        UGSTutorialBindings = new UGSTutorialCloudBindings(CloudCodeService.Instance);
    }

    private async void InitializePlayer()
    {
        try
        {
            var playerDataResponse = await UGSTutorialBindings.SayHello(PlayerName);
        }

        catch (CloudCodeException ex)
        {
            Debug.LogException(ex);
        }
    }

    private void OnDisable()
    {
        LoginManager.PlayerSignedIn -= InitializePlayer;
    }
}

In InitializePlayer, use the bindings to call the SayHello method. We can call any Cloud Code function from the bindings.

Back in Unity, create an empty GameObject, call it PlayerDataManager, add the script of the same name to it, drag it into the LoginManager, and put a name in the name field.

Press Play and if you don’t see the Player ID press the button for Anonymous Login, and you should see “Hello (your name)" in the Console.

Server and Client validation: Cloud Code workflow

Stop the game and go back to Example.cs in the Solution Explorer window. In these next steps, you’ll write a new function to try out the Cloud Code workflow with a practical example.

Let’s take the example of a player who wants to edit their username, which would require you to validate and save the new name in the cloud.

Write a new method called HandleNewPlayerNameEntry; have it take in a string and return a string, just like the example.

As with the above, add a Cloud Code function attribute to your method so the function can be called remotely from the client. Before you save the name, check that it’s valid.

Do a couple common validation checks, like checking whether the name is less than four or greater than 16 characters (which would make it invalid). You can also use LINQ to check whether there are any special characters (all the characters should be a letter or digit).

If the name is invalid, you can throw an argument exception. This is your server-side validation, which we’ll come back to explore later on in the tutorial.

 [CloudCodeFunction("HandleNewPlayerNameEntry")]
    public string HandleNewPlayerNameEntry(string newName)
    {
        if (IsPlayerNameValid(newName))
        {
             return newName;
        }

//////
private bool IsPlayerNameValid(string name)
    {
        if (name.Length is < 4 or > 16)
        {
            return false;
        }

        // Check for special characters using LINQ
        if (!name.All(c => char.IsLetterOrDigit(c)))
        {
            return false;
        }

        return true;
    }

If you try to access the new Cloud Code function from PlayerDataManager, you’ll see the SayHello, but not the HandleNewPlayerName endpoint for the Cloud Code function in the autocompletion of your IDE. That’s because you need to generate bindings again from the Deployment window. Then add this line to the PlayerDataManager in the InitializePlayer() method, after the previous example, for quick testing.

//...
private async void InitializePlayer()
    {
        try
        {
var playerDataResponse = await UGSTutorialBindings.SayHello(PlayerName);

PlayerName = await UGSTutorialBindings.HandleNewPlayerNameEntry(PlayerName);
Debug.Log($"Saved new player name in the cloud: {PlayerName}");
        }

        catch (CloudCodeException ex)
        {
            Debug.LogException(ex);
        }
    }
//...

Select the PlayerDataManager object in the Hierarchy; for simplicity, just edit the PlayerName on the component in the Inspector to test. One more important step: You generated the bindings but you need to deploy the module to the Cloud Code service to make the endpoints accessible to the game client.

Open the Deployment window via the top menu Services > Deployment. With the module selected, right click Deploy and now you’re good to test.

Make sure you’re signed in and after pressing Play you should get a “Hello, (name)!” message from the SayHello example function. Try changing the name in the PlayerManager component to something invalid and press Play again – in the image, you can see the instructor changed it from “jimjam” to “jim”, and duly gets a warning message from the client validation.

Next, let’s pretend you’re hacking the code on your device, and bypassing client validation. While you’re here, add a warning log in the catch to represent the message you might give the player.

Try fewer than four characters, for example. You get a script error and your warning message; try putting in a bunch of special characters and you should also be barred from that. Check the Console for the exception error message.

Now, try a name that should pass. Remember that you aren’t actually saving the name because when you restart the game it goes back to the original player name.

So, at this point, you’ve done server-side validation in Cloud Code, but you can save on server calls and provide more immediate feedback to the player by doing client-side validation too.

In order to do this with the example, copy the IsPlayerNameValid method and implement it client-side in the PlayerDataManager. That way we immediately give the player a pop-up that the name is too short, too long, or otherwise invalid.

Cloud Save

Now you’ll want to save your player data; this is done in Cloud Code after the data has passed our server-side validation.

Open up PlayerDataManager.cs in your Unity project and create a new method called SaveNewPlayerName().

Back in the Editor, create a button for SavePlayerName, for example, in a new canvas. Assuming that you are signed up with an anonymous account in the Editor, you can deactivate the previous example sign-in buttons if you like.

Then, drop the PlayerDataManager into the On Click slot and let’s have On Click trigger the SaveNewPlayerName method.

The next task is figuring out how to integrate Cloud Code with Unity Gaming Services, like Cloud Save. Remember that Cloud Save can be used directly from the client, as shown in the documentation, but in this exercise you’ll validate first with Cloud Code and, once validated, save from server-side, which is the most secure option to save sensitive player data.

As explained in the documentation, there’s a GameApiClient class that’s a simplified interface for calling Unity Gaming Services like Cloud Save, Economy, or Friends from your Cloud Code modules.

To use the GameApiClient interface you need to register the GameApiClient as a singleton with this ModuleConfig class. The docs provide this class example, including how to save and load data.

Copy the ModuleConfig class and rather than put it in this example script, click on the project in the Solution Explorer. Then, go to Add > Class, call the class ModuleConfig and paste in the example code.

Add using.Unity.Services.CloudCode.Core at the top, and since the GameApiClient will be using Unity.Services.CloudCode.Apis add it too.

using Microsoft.Extensions.DependencyInjection;
using System;
using Unity.Services.CloudCode.Apis;
using Unity.Services.CloudCode.Core;

namespace UGSTutorialCloud;

public class ModuleConfig : ICloudCodeSetup
{
    public void Setup(ICloudCodeConfig config)
    {
        config.Dependencies.AddSingleton(GameApiClient.Create());
    }
}

Then, add the Microsoft.Extensions.DependencyInjection namespace. By doing this, you’re registering the GameAPIClient as a shared resource that any part of your Cloud Code can use.

Using Save and Load Data Examples

Go back to the GameApiClient class section of UGS documentation and copy the SaveData and GetData functions, as well as the ILogger and constructor at the top. You don’t need to copy the ModuleConfig part as it’s in a separate script in the previous step.

Paste that code at the top of your script and then replace the example class name with your PlayerDataService class name. Note that SaveData uses gameAPIClient.CloudSaveData.SetItemAsync, and GetData uses gameAPIClient.CloudSaveData.GetItemAsync.

Also note how the IExecutionContext provides information like the PlayerID, ProjectID, and authentication tokens. Context is how Cloud Code knows who and what project is calling it, which is important for security and data management.

Next, remove the Cloud Code function attribute and make these methods private.

Getting or reading data is safe, but you want to be careful with writing data. Imagine a bank allowing anyone to call and demand any amount that they wanted.

The endpoint should be designed for specific purposes and the Cloud Code function HandleNewPlayerNameEntry is a good example of this. Let’s add a call to the SaveData method. You’ll need to make several important changes to your function, so let’s look at them all and walk through it.

///...
[CloudCodeFunction("HandleNewPlayerNameEntry")]
    public async Task<string> HandleNewPlayerNameEntry(IExecutionContext context, IGameApiClient gameApiClient, string newName)
    {
        if (!IsPlayerNameValid(newName))
        {
            throw new ArgumentException("Name is not valid");
        }

        if (IsPlayerNameValid(newName))
        {
            PlayerData? playerData = await GetPlayerDataOrThrow(context, gameApiClient);

            if (playerData == null)
            {
                throw new InvalidOperationException($"Player data not found for player: {context.PlayerId}");
            }

            playerData.DisplayName = newName;
            await SaveData(context, gameApiClient, k_PlayerDataKey, playerData);

            return newName;
        }

        throw new ArgumentException("Name is not valid");
    }
private async Task<PlayerData> GetPlayerDataOrThrow(IExecutionContext context, IGameApiClient gameApiClient)
    {
        var (exists, playerData) = await TryGetPlayerData(context, gameApiClient);

        if (!exists || playerData == null)
        {
            throw new InvalidOperationException($"Player data not found for player: {context.PlayerId}");
        }

        return playerData;
    }
public async Task SaveData(IExecutionContext context, IGameApiClient gameApiClient, string key, object value)
    {
        try
        {
            await gameApiClient.CloudSaveData.SetItemAsync(
                context, 
                context.AccessToken, 
                context.ProjectId,
                context.PlayerId ?? throw new InvalidOperationException("PlayerId is null"), 
                new SetItemBody(key, value));
        }
        catch (ApiException ex)
        {
            m_Logger.LogError("Failed to save data. Error: {Error}", ex.Message);
            throw new Exception($"Failed to save data for playerId {context.PlayerId}. Error: {ex.Message}");
        }
    }
///...

Because SaveData is an asynchronous task, you must use the await keyword when calling it, and that requires you to mark our function with async and change the return type to Task<string> because you’re still returning the player’s name.

SaveData requires specific parameters – the execution context – again for the PlayerID, ProjectID, AccessToken, and gameapiClient. You’ll also need a key to identify your data in storage. At the class level up top add the line seen in the following screen shot.

Add a public constant string variable named k_PlayerNameKey, with the value Player_Name using uppercase because it’s a common convention for constant key values.

You’re passing the key and the new name parameter as the value that should be stored. Note that there is player name support through the authentication service. In the context of this tutorial you’re experimenting with usernames – using them as a simple piece of player data with which to explore dual validation: The client-side and server-side validation that you’re doing with the same IsPlayerNameValid logic.

Testing and saving
From here, let’s head back to Unity and generate bindings via the top menu Services > Cloud Code > Generate All Modules Bindings and then deploy the module.

Start the game and (assuming you have something typed for player name), click Save Player Name. There should be a log, “saved new player name in the cloud”.

Make note of the Player ID and then confirm that the player name is saved by going to the Unity Dashboard. Click on Cloud Save > Player Data > Player ID and you should see the player data saved.

In the Player ID view you’ll see three access classes listed in the top row of the view:

  • Default, which is readable and writable by the player
  • Public data, which is the same but it’s readable by any other player
  • Protected, which is only readable by the player, the player can’t write or modify this data

Unity Analytics

In addition to saving the player’s data, you’ll also want to collect data that gives you insights about the players and your game.

In the Unity Dashboard, click the + icon next to the Shortcuts menu item and then select Analytics.

Once the Analytics view loads, go back to the Unity Package Manager and click the cloud icon on the left – this is where you find packages for Unity services. Find the Analytics package and click Install. It’s also a good idea to install the samples for the Analytics package.

Analytics documentation lays out the steps to set it up for your game, like setting up the dashboard and installing the package. The documentation also provides you with the initialization line you need after UnityServices.InitializeAsync() confirming player consent, but you can skip this for the purposes of this tutorial.

As outlined in the docs, call the line AnalyticsService.Instance.StartDataCollection() and then switch back to the Editor.

You’re going to initialize Unity Services in the LoginManager you previously created, so you’ll need to create a dedicated script for initialization.

Right-click in your Project view, create a new script, and call it GameInitializer. Open the script and delete Start and Update.

Use Awake and paste the Analytics initialization. Go over to LoginManager and take the Unity Services Initialization and paste it into the GameInitializer script. After you paste this, mark Awake as Async since InitializeAsync() is a task. That’s all you need to do in the GameInitializer.

using Unity.Services.Analytics;
using Unity.Services.Core;
using UnityEngine;

public class GameInitializer : MonoBehaviour
{
    private async void Awake()
    {
        if (UnityServices.State == ServicesInitializationState.Uninitialized)
        {
            Debug.Log("Services Initializing");
            await UnityServices.InitializeAsync();
        }

        AnalyticsService.Instance.StartDataCollection();
    }
}

However, moving the initialization means that you’ll have a race condition issue in the LoginManager in trying to subscribe to PlayerAccountService.Instance.SignedIn when Unity services is not yet initialized. The async task can take some time, so let’s move this subscription to a new method, UnitySignInSubscription. And then, do a check if the services are initialized. In that case, right away, you can subscribe to the PlayerAccountService event. Otherwise, the subscription will have to wait until the initialized event fires. And of course, you’ll need to unsubscribe to this in OnDisable(), which you should change to OnDestroy() since you’re subscribing in OnAwake.

///...
private void Awake()
    {
	//if you subscribe to a unity service make sure you check services are intiialized before using them
        if (UnityServices.State == ServicesInitializationState.Initialized)
        {
            UnitySignInSubscription();
        }
        else
        {
            UnityServices.Initialized += UnitySignInSubscription;
        }
    }

private void UnitySignInSubscription()
    {
//we use this signedin event as an example but could be another service event that works for your project
        PlayerAccountService.Instance.SignedIn += SignInOrLinkWithUnity;
    }
private void OnDisable()
    {
        PlayerAccountService.Instance.SignedIn -= SignInOrLinkWithUnity;
        UnityServices.Initialized -= UnitySignInSubscription;
    }
///...

Go back to Unity and play the game because that was all you needed to do to get some analytics data. After you’ve done that, go to the Unity Dashboard > Analytics > Game Performance but you won’t see anything yet because it takes a couple of hours for the results to show.

In the next tutorial, you’ll learn how to add the Unity Economy service.