Work with user profiles

There is already a ready-made module for working with the user profile in this framework. This module automatically sends all changes in the profile to clients, and also has a basic mechanism for saving data. Also, when a player connects to the game server / room, the server can receive the profile of this player and, after making changes to this profile, send these changes to the master server.

Main Principles Of Working With Profiles

In most cases, when working with a profile, you will need to do the following:

  • Define the structure of your profile (profile factory) on the master server side.
  • (Optional) Get a client-side profile to receive updates and display them on the user's screen.
  • (Optional) Get a profile on the game server/room side and make changes to it, which will be sent to the master server and then to the client.

Defining the Profile Structure On the Master Server Side

As mentioned earlier, we already have everything you need to work with profile data. This is done by the ProfilesModule. Therefore, in order not to write code from scratch, we will only extend our ProfilesModule class and make some changes and additions.

Create a script file with the ProfilesModuleTutorial class derived from the ProfilesModule. This will be our module for working with the profile. The guide of how to create custom modules is described here.

using MasterServerToolkit.MasterServer;
using MasterServerToolkit.Networking;
using MasterServerToolkit.Utils;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace MasterServerToolkit.Tutorials
{
    public class ProfilesModuleTutorial : ProfilesModule
    {
        
    }
}

Next, we need to create profile keys. These keys will help us determine which profile properties have been changed. We will create the following properties:

  • DisplayName - the display name of the player.
  • Avatar - the player's avatar.
  • Bronze - the amount of bronze the player has.
  • Silver - the amount of silver the player has.
  • Gold - the amount of gold the player has.

Let's add these fields as enums as part of our module for working with the profile.

namespace MasterServerToolkit.Tutorials
{
    public enum ObservablePropertiyCodes { DisplayName, Avatar, Bronze, Silver, Gold }

    public class ProfilesModuleTutorial : ProfilesModule
    {
        
    }
}

Next, as mentioned earlier, we need to define the structure or factory of our profile. This structure will fill in the profile of a new player or a player who started the game again with primary data . To do this, the ProfilesModule has a special delegate property that we will link our factory to. This field is called ProfileFactory. To do this, create a method that returns ObservableServerProfiler. Let's call it, for example, CreateProfileInServer() and pass two arguments to it. The first will be used as profile ID, in our case, we use the username taken from the AuthModule. This module is already connected to our profile module. The second is Peer(Connection) of our client who will own this profile.

Since our method returns ObservableServerProfile, we must immediately initialize it inside this method and return it. This is where we need our keys that we created earlier. Let's create an instance of the ObservableServerProfile class and add all the properties that are necessary for our game. Also, when adding properties to specify their initial value, it is possible to do this via the property inspector is our module.

namespace Aevien.Tutorials
{
    public enum ObservablePropertiyCodes { DisplayName, Avatar, Bronze, Silver, Gold }

    public class ProfilesModuleTutorial : ProfilesModule
    {
        [Header("Start Values"), SerializeField]
        private float bronze = 100;
        [SerializeField]
        private float silver = 50;
        [SerializeField]
        private float gold = 50;
        [SerializeField]
        private string avatarUrl = "https://i.imgur.com/JQ9pRoD.png";

        private ObservableServerProfile CreateProfileInServer(string username, IPeer clientPeer)
        {
            return new ObservableServerProfile(username, clientPeer)
            {
                new ObservableString((short)ObservablePropertiyCodes.DisplayName, SimpleNameGenerator.Generate(Gender.Male)),
                new ObservableString((short)ObservablePropertiyCodes.Avatar, avatarUrl),
                new ObservableFloat((short)ObservablePropertiyCodes.Bronze, bronze),
                new ObservableFloat((short)ObservablePropertiyCodes.Silver, silver),
                new ObservableFloat((short)ObservablePropertiyCodes.Gold, gold)
            };
        }
    }
}

Next, you need to override the module initialization method and specify our factory in it. About what the initialization method of the module for details is here.

namespace MasterServerToolkit.Tutorials
{
    public enum ObservablePropertiyCodes { DisplayName, Avatar, Bronze, Silver, Gold }

    public class ProfilesModuleTutorial : ProfilesModule
    {
        [Header("Start Values"), SerializeField]
        private float bronze = 100;
        [SerializeField]
        private float silver = 50;
        [SerializeField]
        private float gold = 50;
        [SerializeField]
        private string avatarUrl = "https://i.imgur.com/JQ9pRoD.png";

        public override void Initialize(IServer server)
        {
            base.Initialize(server);

            // Set the new factory in ProfilesModule
            ProfileFactory = CreateProfileInServer;
        }

        private ObservableServerProfile CreateProfileInServer(string username, IPeer clientPeer)
        {
            return new ObservableServerProfile(username, clientPeer)
            {
                new ObservableString((ushort)ObservablePropertiyCodes.DisplayName, SimpleNameGenerator.Generate(Gender.Male)),
                new ObservableString((ushort)ObservablePropertiyCodes.Avatar, avatarUrl),
                new ObservableFloat((ushort)ObservablePropertiyCodes.Bronze, bronze),
                new ObservableFloat((ushort)ObservablePropertiyCodes.Silver, silver),
                new ObservableFloat((ushort)ObservablePropertiyCodes.Gold, gold)
            };
        }
    }
}

There is also another way to create the structure of the profile on the server side. In this way we are not required to extend profile module, we just simply search for it in the scene and initialize the profile factory method ProfileFactory as shown below.

var profilesModule = FindObjectOfType<ProfilesModule>();

profilesModule.ProfileFactory = (username, peer) => new ObservableServerProfile(username, peer)
{
    new ObservableString((ushort)ObservablePropertiyCodes.DisplayName, SimpleNameGenerator.Generate(Gender.Male)),
    new ObservableString((ushort)ObservablePropertiyCodes.Avatar, avatarUrl),
    new ObservableFloat((ushort)ObservablePropertiyCodes.Bronze, bronze),
    new ObservableFloat((ushort)ObservablePropertiyCodes.Silver, silver),
    new ObservableFloat((ushort)ObservablePropertiyCodes.Gold, gold)
};

If the factory method is set correctly, a default profile will be created for each user when the ProfileFactory method is called when the user is logged in to the game. If the user is not new, the master server will check the database for a profile and load it.

For now, this is all we need to work with the player profile on the master server side. Next, you need to write the logic for receiving and processing profile data on the client side.

Listening for changes in the profile on the client side

In order to receive profile changes from the server, you need to establish a connection with this profile. To do this, you need to send a request to the master server to get your profile data. You can do this in the following way:

 

Mst.Client.Profiles.FillInProfileValues(profile, (isSuccessful, profileError) =>
{
    if (!isSuccessful)
    {
        Logs.Error(profileError);
        return;
    }

    // Listen to property updates
    profile.OnPropertyUpdatedEvent += (code, property) =>
    {
        // Log a message, when property changes
        Logs.Info($"Property changed: {code} - {property}");
    };

});

As you can see, after establishing a connection with the profile, we immediately registered listening for changes to all its properties. You can also listen to a specific profile property separately.

// Get property by casting it to ObservableFloat
var bronzeProperty = profile.Get<ObservableFloat>((ushort)ObservablePropertiyCodes.Bronze);

// Register listening to changes
bronzeProperty.OnDirtyEvent += property =>
{
    Logs.Info($"Bronze changed to: {bronzeProperty.Value}");

    // OR
    Logs.Info($"Bronze changed to: {property.As<ObservableFloat>().Value}");
};

So, we have configured the profile to work with it on the master server and on the client. Now you need to make changes to this profile. For example, add gold after a certain time interval or change the display name of the player. This procedure can be performed both on the master server and in the game server/room. We will look at how to change the player profile on the game server/room side.

Changing profile data on the game server/room side

In this case, we will only change the profile data on the game server/room side. As with the client side, we need to create an instance of the player profile and make a request to the master server to get all its properties.

// Construct the profile
var profile = new ObservableServerProfile(username)
{
    new ObservableString((ushort)ObservablePropertiyCodes.DisplayName),
    new ObservableString((ushort)ObservablePropertiyCodes.Avatar),
    new ObservableFloat((ushort)ObservablePropertiyCodes.Bronze),
    new ObservableFloat((ushort)ObservablePropertiyCodes.Silver),
    new ObservableFloat((ushort)ObservablePropertiyCodes.Gold)
};

// Fill profile values
Mst.Server.Profiles.FillProfileValues(profile, (successful, error) =>
{
    if (!successful)
    {
        Logs.Error(error);
        return;
    }
});

Here we take the user's name and create a profile for it. Next, we send a request to the master server to fill in this profile with data. The username here plays the role of the ID that you will use to get profile information from the master server. If you look at the code of the ProfileModule, you can see that all user profiles are stored in the dictionary, that uses username as key. See the example below.

/// <summary>
/// List of the users profiles
/// </summary>
protected readonly ConcurrentDictionary<string, ObservableServerProfile> profilesList = new ConcurrentDictionary<string, ObservableServerProfile>();

protected virtual async void OnUserLoggedInEventHandler(IUserPeerExtension user)
{
    ...

    // Create a profile
    ObservableServerProfile profile;

    if (profilesList.ContainsKey(user.UserId))
    {
        // There's a profile from before, which we can use
        profile = profilesList[user.UserId];
        profile.ClientPeer = user.Peer;
    }
    else
    {
        // We need to create a new one
        profile = CreateProfile(user.UserId, user.Peer);
        profilesList.TryAdd(user.UserId, profile);
    }

    // Restore profile data from database
    await profileDatabaseAccessor.RestoreProfileAsync(profile);

    // 
    profile.ClearUpdates();

    // Save profile property
    user.Peer.AddExtension(new ProfilePeerExtension(profile, user.Peer));

    // Listen to profile events
    profile.OnModifiedInServerEvent += OnProfileChangedEventHandler;
}

After the profile of the player connected to the game server/room is received, we can easily any make changes to its properties. For example, to add gold or change the user's display name in our case, we do the following:

// Add 10 of gold
profile.Get<ObservableFloat>((ushort)ObservablePropertiyCodes.Gold).Add(10);

// Change user display name
profile.Get<ObservableString>((ushort)ObservablePropertiyCodes.DisplayName).Value = "New Display Name";

In this case, this is all you need to know about working with the profile in this framework.