Introduction

Virtual Currency is a basic feature that most alternative platforms provide. The minimum requirement is the ability to create a key/value pair, tied to the player’s account which can be credited and debited to change the player’s balance.

Virtual Currency is usually associated with other features like Achievements and Virtual Goods. Those features should allow Virtual Currency to be granted or consumed on behalf of the player. They should also be accessible through some sort of API so that the developer can choose how they are credited and debited, such as GameSparks’s SparkVirtualCurrency API.

Some of the platforms discussed below include these features, but there are always differences between their implementation and GameSparks’. Keep in mind that crediting/debiting the player directly from the client is not recommended for any platform, so, in some places you may need to use the platform's Cloud-Code alternative to credit and debit the player.

GameSparks has two versions of Virtual Currency. One is the old version of currency which is the numbered currency values that come with every new player account.

The others are the currencies you can set up and name yourself from the configurator.

The only other feature related to Virtual Currency which GameSparks has, is the ability to automatically set a sign-up bonus for each currency. This isn't available for all alternative platforms, but we will show how that can be achieved where possible.

Beamable

As with most features in Beamable, you create Virtual Currencies as Content through the Content Manager tab.

Once you have given this content a name there are a few other parameters we have to set up. If you click on one of these Virtual Currencies, you can see some options in the Inspector.

An important field here is “Write Self”. This should be un-selected for the normal operation of your game. In general, you do not want users to be able to modify their balance from the client as this would leave your game open for anyone to hack the API and grant themselves Virtual Currency.

Instead, we set this as false, and we will show you how you can use microservices to credit/debit player currency.

Sign-Up Bonus

Another option you can see is the equivalent of the GameSparks sign-up bonus. You can set a starting amount for each of your currencies for the player.

Note - Remember to push these changes to the backend before testing to make sure they are available when it comes to calling API requests.

As with other Beamable Content objects, we have multiple ways to reference CurrencyContent through the API. We will show some sample code below with a few options, however, remember that if you have the “Write Self” unclicked, these will not work for you, this is only an example.

private IBeamableAPI beamableAPI = null;

[SerializeField]
public CurrencyRef coinsRef = null;

async void Start()
{
   beamableAPI = await Beamable.API.Instance;

   // get the content reference manually if you know the Id //
   var xpRef = new ContentRef(typeof(CurrencyContent), "currency.XP");

   await beamableAPI.InventoryService.AddCurrency(coinsRef.GetId(), 1);
   await beamableAPI.InventoryService.SetCurrency(xpRef.GetId(), 100);
   // below, for example, you dont need the ContentRef object //
   long gemBal = await beamableAPI.InventoryService.GetCurrency("currency.GEMS");
   Debug.Log("GEMS: "+gemBal);
   // we'll use this to find the player in the portal //
   Debug.Log("PlayerId: "+beamableAPI.User.id);
}

The example above shows how to get the CurrencyContent object for use with the APIs. The coinsRef variable would be set from the editor.

The playerId is included here so that you can grab it and search for that player in the Beamable portal. You should be able to see your updated player balances.

Example: Leveling System

As already mentioned, we should not be crediting the player directly from the client API, so we are going to cover an example which shows crediting and debiting from the server, along with some server-side logic to control crediting and debiting.

For this example, we will cover a few different points. We are going to create some custom content and use that to define a set of rewards for each level, along with the XP required for each level.

Since this is all going to be using Microservices, we are not going to cover every step required. To see examples of how to create Microservices and custom content, check out the topic on Cloud-Code here.

The JSON content we are reproducing looks something like this, a common example of level-up rules for a GameSparks title.

{
  "levels": [
    {
    "level": 2,
    "xpReq": 1000,
    "rewards": [
        {
        "currType": "COINS",
        "amount": 100
        },
        {
        "currType": "GEMS",
        "amount": 5
        }
    ]
    },
....
  ]
}

Converting this JSON to a ContentObject would look something like below.

We already covered configuring XP, COINS and GEMS as Virtual Currency. We are going to use LEVEL as a Stat on the player’s account. This will come in handy as Stats can be used to target groups of players for all sorts of use cases. We will not cover those examples here, but LEVEL is a good example of where to use a Stat instead of Content Data.

Note - Remember to add the custom content assembly reference to your Microservice (example here).

Another component you might want is a response data structure. For this example, this structure will contain the player’s current level and XP, along with any rewards given, and a boolean to indicate if the player leveled up. This is a common response for GameSparks developers modelling a similar level-up event with their Cloud-Code events.

We will stick this class into our common GS content folder so we can get it into the Microservice as an assembly reference. Remember to add the custom-content reference to the Microservice before proceeding.

[System.Serializable]
public class GSAddXPResponse
{
   public GSAddXPResponse()
   {
       rewards = new List<LevelUpReward>();
   }
   public int currentLevel;
   public long currentXP;
   public bool hasLeveledUp;
   public List<LevelUpReward> rewards;
   public string error;
}

Now we can start on the Microservice code. As you will have found from other tutorials to this point, you will first need to modify the function to return this GSAddXPResponse. The function will also need to take an integer or a long to add XP. We are also going to add the playerId to this function so we can get the player’s level as a Stat.

[ClientCallable]
public async Task<GSAddXPResponse> GrantXP(long xp, long playerId)
{
   return null;
}

Our code will perform the following steps:

  1. We are going to load the level definitions content file. If we fail to load that definition, we cannot proceed with this example, so we return an error here if needed.
  2. We will get the player’s current level from the Stat service.
  3. Get the player’s current XP from the inventory service.
  4. Run through the level definitions to see if the player should be leveled up.
  5. Deliver rewards if the player has leveled up.
  6. Add the XP.
  7. Return the response.
[ClientCallable]
public async Task<GSAddXPResponse> GrantXP(long xp)
{
   GSAddXPResponse response = new GSAddXPResponse();
   // declare some params we need for this example //
   string levelStatKey = "LEVEL";
   string xpCurrKey = "currency.XP";
   // we need to following for setting and getting the right stats //
   string domain = "game";
   string access = "public";
   string type = "player";
   // load the level Defs //
   GSLevelsDef levelDefDetails;
   try
   {
       levelDefDetails = (GSLevelsDef) await Services.Content.GetContent("level_defs.levelDefs");
       Debug.Log(levelDefDetails.Id);
   }
   catch (Exception e)
   {
       Debug.LogError(e.Message);
       response.error = e.Message;
       return response;
   }
   Debug.Log($"Fetching Player Level for player {Context.UserId}...");
   int currLevel;
   Dictionary<string, string> stats = await Services.Stats.GetStats(domain, access, type, Context.UserId, new []{ levelStatKey });
   // We might not have the Stat implemented yet, this is a common case when working from JS //
   // So lets check that here //
   if (stats.ContainsKey(levelStatKey))
   {
       currLevel = Int32.Parse(stats[levelStatKey]);
   }
   else
   {
       currLevel = 1;
   }
   Debug.Log($"Current Level: {currLevel}");
   // Lets grab the player's current Xp now //
   long currXp = await Services.Inventory.GetCurrency(xpCurrKey);
   Debug.Log($"Current XP: {currXp}");
   // Now we will go through the level definitions and decide if we have leveled up //
   foreach (LevelDef levelDef in levelDefDetails.levelDefs)
   {
       if (((currXp+xp) >= levelDef.xpReq) && (currLevel < levelDef.nextLevel))
       {
           Debug.Log("Player Leveled Up!");
           response.hasLeveledUp = true; // << add to the response
           // a little validation to make sure we have rewards for this level //
           if (levelDef.rewards.Length > 0)
           {
               // Deliver rewards to player //
               foreach (LevelUpReward reward in levelDef.rewards)
               {
                   string currencyId = "currency." + reward.currencyType;
                   await Services.Inventory.AddCurrency(currencyId, reward.amount);
                   // add the reward to the response //
                   response.rewards.Add(reward);
                   Debug.Log($"Granted: {reward.amount} {reward.currencyType}");
               }
           }
           else
           {
               Debug.LogWarning("No rewards to deliver...");
           }
           // Level Up the player //
           Dictionary<string, string> newStats = new Dictionary<string, string>();
           int newLevel = (currLevel + 1);
           newStats.Add(levelStatKey, newLevel.ToString());
           await Services.Stats.SetStats(domain, access, type, Context.UserId, newStats);
           currLevel = newLevel;
           break;
       }
       else if(levelDef.nextLevel > currLevel)
       {
           Debug.Log($"XP req: {currXp/levelDef.xpReq}");
           break;
       }
   }
   Debug.Log($"Granting XP {xp}");
   // add the Xp anyway //
   await Services.Inventory.AddCurrency(xpCurrKey, xp);
   currXp = await Services.Inventory.GetCurrency(xpCurrKey);
   Debug.Log($"New XP: {currXp}");
   Debug.Log($"Curr Level: {currLevel}");
   // Now we'll construct the response //
   response.currentLevel = currLevel;
   response.currentXP = currXp;
   return response;
}

And we can trigger this server function using this example code…

int grantedXP = 400;
LevelManagerMSClient _levelManagerMSClient = new LevelManagerMSClient();
GSAddXPResponse response = await _levelManagerMSClient.GrantXP(grantedXP);
if (String.IsNullOrEmpty(response.error))
{
   // Log out the details to verify //
   Debug.Log($"Current Level: {response.currentLevel}");
   Debug.Log($"Current XP: {response.currentXP}");
   Debug.Log($"Has leveled Up: {response.hasLeveledUp}");
   Debug.Log($"Delivered {response.rewards.Count} Rewards...");
   // OnLevelUp(response) //
}
else
{
   // we have an error //
   // OnResponseError(response) //
   Debug.LogError(response.error);
}

AccelByte

Virtual Currency in AccelByte has similar workings to GameSparks where we use it to purchase or deliver Virtual Goods, however, with AccelByte’s Virtual Goods we have to set up the Virtual Currencies before we can use them.

You can find two types of currencies: Real and Virtual. These types of currencies are not like the Currency Type in GameSparks but instead are more beneficial in setting up Items (Virtual Goods) in AccelByte. In fact, they are defined to specify what kind of currency we can accept for payments. Real currency is like any other actual currency while Virtual is a virtual currency in the game.

With AccelByte you must first create a wallet specifically for each Virtual Currency in order to add a value to it. This differs from GameSparks where we create currency automatically when the player account is created, and the sign-up bonus can credit the player freely from there.

Creating New Virtual Currencies

From the admin portal we can find Currencies under the E-Commerce category. If we click on that option, it will bring up a page where we can create different currencies. Click on Add option and then a small window will pop-up. We can select currency type, currency code and symbol and click Add to configure the currency. We can see our listed created currencies as shown below.

Player Wallet

When currencies are added to the player’s account they appear in their player wallet.

The wallet is automatically created when the first currencies are credited to the player, however, the wallet cannot be created on its own, it is created automatically.

However, the AccelByte Unity SDK is designed to prevent hacking by players who would reverse engineer those APIs to give themselves an advantage, like crediting themselves currency or granting themselves items. This means we cannot update the player’s currency and therefore their wallet from Unity. So how do we do that?

There is an example of how you can achieve this for testing in our topic on Cloud-Code here, however, the longer-term solution is to create your own backend that can utilize the AccelByte Golang or JS SDKs or work with AccelByte to create your own custom microservice.

Sign-Up Bonus

Accelbyte does not provide a signup bonus like in GameSparks. However, there is an alternative solution through Augment. We have already shown off crediting the player wallet with the desired amount. If you would like to trigger this signup bonus when the player is created then we need to choose one of the Kafka triggers. You can find more info regarding player wallet in this here.

Nakama

As is the case with GameSparks, Virtual Currency in Nakama is tied to the player’s account. The APIs for crediting and debiting the player are not available through the client and instead are accessed from Nakama’s version of Cloud-Code called the runtime server.

Getting Player Wallet

The first thing we are going to look at is how to get the player’s wallet balance in Unity so we can display the player’s current balance.

As this is the first time the player has encountered Virtual Currency there will be no currencies included in the player’s wallet.

We will add those later and you can also check out the section below on sign-up bonuses. For now we are going to manually add some currencies to the player’s wallet so we have something to display in Unity in this example.

You will need to find a registered player’s account in the Developer Portal.

To do this you will need to have a Nakama instance running. In our case we have the instance running locally on http://127.0.0.1:7351. Check out the guide here on how to get an instance running locally.

Click on the “Accounts” option on the left-hand side menu of the Developer Portal and then click on the player you want to select.

Next, you need to click on the “Wallet” tab.

We want to update the JSON here in order to give this player some currency we can get back from the server in Unity. We will include non-currency parameters such as level and xp, as these parameters often benefit from using the same APIs as regular Virtual Currency.

Remember to hit the save button to make sure your changes are updated. Now let us go back into Unity and see how we can get this balance back.

You can do this with just a few lines of code. Remember that these API calls need to be made from within asynchronous methods in your C# code.

/// <summary>
/// Retrieves the player data from the Nakama server based on the player's session
/// </summary>
/// <param name="session">The player's current Nakama session</param>
private async void GetPlayerData(ISession session)
{
   Debug.Log("Fetching Player Data...");
   IApiAccount account = await nakamaClient.GetAccountAsync(session);
   IApiUser user = account.User;
   Debug.LogFormat($"User wallet: '{account.Wallet}');
}

The “session” parameter for this method is taken from the authentication response. Check out the topic on Authentication here for more information.

If you test this method out you should see your player’s wallet balance displayed in the console.

The player wallet object is a string as you can see from the code above. You will need to parse this into a something usable in C# like Dictionary<string, int>.

The Nakama SDK comes with TinyJSON installed so we can use that to parse our responses.

Dictionary<string, int> wallet = Nakama.TinyJson.JsonParser.FromJson<Dictionary<string, int>>(account.Wallet);
foreach (var currency in wallet)
{
   Debug.Log($"{currency.Key}:{currency.Value}");
}

Player Wallet - Server API

As is the case for GameSparks, there is no way to credit and debit players directly from the client in Nakama.

In most cases you would never want to allow this in order to prevent a player from taking advantage of the service if they hacked the client.

Instead, you would do this through a request to the server, or let the server credit and debit the player based on some server-authoritative action like an Achievement event.

For this example we are going to look at an easy way to test crediting and debiting of players through an RPC function.

RPC functions are Nakama’s equivalent of a GameSparks LogEventRequests.

We are going to need a runtime server for this step so check out the guide on Cloud-Code in Nakama here for more details on how to set up a runtime server and get some Cloud-Code working.

We will create a new TypeScript file for this example called “rpcCreditPlayer.ts”. We will then add the following function.

/**
* Credits the player a given about of currency and returns the player's new balance
* @param context
* @param logger
* @param nk
* @param payloadString
* @returns {string} - current balance
*/
function rpcCreditPlayer(context: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, payloadString: string): string {
   logger.info("Data In: " + payloadString);
   logger.info("Crediting Player...");
   // parse the payload to JSON so we can work with it //
   var payloadData = JSON.parse(payloadString);
   // get the current balance so we can credit it //
   var currWallet = nk.accountGetId(context.userId).wallet;
   // credit the player //
   for (var currencyKey in currWallet) {
       logger.info(currencyKey);
       currWallet[currencyKey] += payloadData[currencyKey];
   }
   // save the balance back //
   for (var currencyKey in currWallet) {
       logger.info(`Updated ${currencyKey},  Value: ${currWallet[currencyKey]}`);
       nk.walletUpdate(context.userId, { [currencyKey]: currWallet[currencyKey] });
   }
   logger.info(`Updated Balance ${JSON.stringify(currWallet)}`);
   // return the new balance to the player //
   return JSON.stringify(currWallet);
}

This is all the code we need to update the player’s balance. You can see from the example above that all we are doing is getting the current balance from the player’s account, crediting the wallet and saving it back to the player account. It is a flow that you should recognise from GameSparks scriptData and privateData APIs if you use those in your game.

You can see more information about these wallet API calls here.

Reminder - Remember to build and redeploy your server instance for these changes to take effect.

Once your server has been updated you can check that the new RPC function was added by going to the “Runtime Modules” panel of the administrator portal. You will be able to see your new RPC function listed there.

Next we will add some code in Unity which will be able to hit this RPC with a payload to update the player’s balance.

Note - This is just an example. In a real case you would not want to allow the player to update their balance from the client directly.

Calling The Update From The Client

In order to send a request to the RPC we need to construct a payload and then convert that payload to a JSON string. Unlike GameSparks, we will need to use an await command to call this RPC so make sure you stick these calls in an asynchronous method.

Below is an example of how you can call the “creditPlayer” RPC function we created earlier from Unity.

/// <summary>
/// Updates the player's balance using the "creditPlayer" RPC
/// </summary>
/// <param name="session">Taken from the currently authenticated player's session</param>
/// <param name="coins"></param>
/// <param name="gems"></param>
/// <param name="level"></param>
/// <param name="xp"></param>
private async void UpdatePlayerWallet(ISession session, int coins, int gems, int level, int xp)
{
   Debug.Log($"Coins:{coins}, Gems:{gems}, Level:{level}, XP:{xp}");
   // construct request //
   string rpcId = "creditplayer";
   Dictionary<string, int> updatedWallet = new Dictionary<string, int>()
   {
       { "COINS", coins },
       { "GEMS", gems },
       { "LEVEL", level  },
       { "XP", xp }
   };
   var payload = Nakama.TinyJson.JsonWriter.ToJson(updatedWallet);
   // Send request to RPC //
   IApiRpc responseData = await nakamaClient.RpcAsync(session, rpcId, payload);
   // Now we can parse the response payload back to a dictionary //
   Debug.Log(responseData.Payload);
   Dictionary<string, int>  wallet = Nakama.TinyJson.JsonParser.FromJson<Dictionary<string, int>>(responseData.Payload);
   foreach (var currency in wallet)
   {
       Debug.Log($"{currency.Key}:{currency.Value}");
   }
}

Executing this RPC will get you a response object of the type IApiRpc.

There are a lot of details about the response parameters in that object but what we need is the Payload parameter.

This is a JSON string, so you can see in the example above that we use the TinyJSON API here again to parse the current balance back to a C# Dictionary.

We can see the output logged in the Unity editor console.

You can double-check your player’s balance was updated by opening the Developer Console and looking at your player’s wallet balance again.

If you have your terminal open and connected to your Nakama instance, or you are looking at the logs for your instance in Docker-Hub you will be able to see the logs we put in the RPC function there too.

SignUp Bonuses

We have looked at how you might credit and debit the player using the runtime server. The next thing would be to look at how you might replicate sign-up bonuses for your new players the same way as you have set up in GameSparks.

To do this, we are going to use some of the Nakama runtime server hooks. These are similar to the “Request” and “Response” scripts you are familiar with from GameSparks.

These hooks are raised before or after out-of-the-box server events; in our case after authentication.

Nakama does not have a hook for after-registration, so we are going to use the after-authentication hook instead.

We will add a flag to the player’s account to indicate that the player has been set up and will not go through the process on subsequent authentications.

OnAuthentication Script

Create a new TypeScript file in your runtime server project. For this example we called our file “onAuthentication”. As this is a hook and not an RPC like we created before, the entry function takes a slightly different set of parameters. Let's take a look at the code and then we will go through it step-by-step.

/**
* This function is raised when a player authenticates
* @param context
* @param logger
* @param nk
* @param data
*/
function onAuthentication(context: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, data: nkruntime.Session) {

   // First we need to check if this player has been set up already //
   var playerMetaData = nk.accountGetId(context.userId);
   var isAccountSetup = playerMetaData.user.metadata['isAccountSetup'];
   if (!isAccountSetup) {
       // Add the sign-up bonus to the player's wallet //
       var signUpBonus: any = {
           "COINS":    350,
           "GEMS":     5,
           "LEVEL":    0,
           "XP":       0
       };
       // Loop through each of the sign-up currencies and deliver them to the player //
       for (var currencyKey in signUpBonus) {
           logger.info(`Adding Sign-Up bonus ${currencyKey}, ${signUpBonus[currencyKey]}`);
           nk.walletUpdate(context.userId, { [currencyKey]: signUpBonus[currencyKey] });
       }
       var metaData = { "isAccountSetup": true };
       var playerData = nk.accountUpdateId(context.userId, null, null, null, null, null, null, metaData);
       logger.info("Player Initialized...");
   }
}
  1. First we need to get the flag from the account so that we know if this account has been set up or not. We are going to store this flag in the player’s metadata for this example.
  2. If there is no flag on the player’s account we will give the player their starting balance. To keep this example simple we are going to use a JSON object

Working With JSON in TypeScript

You’ll notice the JSON object is declared using the any data-type. This allows you to work with JSON as you are familiar with from JavaScript and GameSparks Cloud-Code, however, remember that when using TypeScript we should be declaring strict types when possible.

A better way to handle this JSON might be to declare an interface for your currency rewards and declare the signupBonus object as that data-type instead.

Public Metadata

All player metadata is public. Therefore it is accessible by other players who are loading your account by ID.

There is also a limit of 16K on player metadata. Coupled with the fact that this data is public, it might not always be the best place to store player data so keep this in mind.

Metadata or Storage Engine?

For this example we added the flag to the player’s metadata as it is a quick and easy way to set a flag on the player account. For more complicated player data you might want to use the Nakama Storage Engine which is similar to GameSparks’ GDS or runtime collections. There is more information on the Nakama Storage Engine included in the Cloud-Code topic topic here.

Registering The Hook

The next step is to register the script in the InitModule function of the main.ts script.

You can put this code underneath where you declared the “creditPlayer” RPC.

// Set up hooks.
   initializer.registerAfterAuthenticateDevice(onAuthentication);
   initializer.registerAfterAuthenticateEmail(onAuthentication);
   // register steam, fb, apple, etc

You can see that there are a lot of authentication hooks available, so you might want to apply the same script to multiple hooks depending on what authentication routes you are using in your game.

Reminder - Remember to build and redeploy your server instance for these changes to take effect.

Now let's test our code. From the last example you have seen that you can inspect a player’s wallet through the Developer Console, but you can also see the player’s metadata from there too. This would be set in the “Profile” tab of the player's account.

And to double-check you can also check the player’s wallet.

Now that we have Virtual Currency set up for our user, let's look at how we can use it to buy some Virtual Goods in the next topic here.