Introduction

GameSparks Virtual Goods are a pretty simple feature. In essence they are just some configuration data which defines the code, name, description and the cost of the Virtual Good in some preconfigured Virtual Currency related to the platform.

GameSparks does have a number of other features such as bundles and tags. These additional features are available on some platforms but not all of them, so we won't discuss how to reproduce those. Instead we are going to focus on the following requirements.

  1. We need to be able to create a flexible definition that contains the code, name and description.
  2. We need to be able to define a cost for the item.
  3. We need an API which can grant this item to the player. Ideally the API should be able to debit the cost of the item when granting it.
  4. If the API cannot debit the cost of the item, we need to be able to debit the cost manually ourselves.

GameSparks Virtual Goods also include 3rd party integrations like Google or Apple Products which we can define with the Virtual Good and let GameSparks validate purchases and grant these items to the player from the backend. We aren't going to cover that in this basic example, but there is another example here where we discuss reproducing those features.

Beamable

Beamable takes a slightly different approach to GameSparks with its Virtual Goods, but their solution is not very complicated.

As you would expect, you can create item configurations and grant them to the player using built-in APIs. These APIs are also available using Microservices so you can let the granting of these items be server-authoritative.

For this topic, we will only cover some of the basic GameSparks Virtual Goods features, taking an existing GameSparks example and porting it to Beamable. There is another topic here which covers 3rd party transactions if that is what you are looking for.

Consider basic GameSparks Virtual Goods. Each item has a cost in one of the currencies we defined already. We can add a description field and a tag later, but other than that, we will keep these items very simple.

Items are already content-types in Beamable so we are going to use that predefined type and modify it for this use-case. There is an example of how to create new Content from scratch here if you want to do it that way.

The first step is to create a new C# script which is going to be a customized version of the out-of-the-box ItemContent class Beamable already has. By inheriting from the ItemContent class it allows our new GSVirtualGoodContent objects to use the same APIs regular Beamable items use for granting items to the player.

Beamable doesn't credit/debit the Virtual Currency cost at the same time as delivering Items, so this is something we are going to have to add ourselves.

Create a new C# script called GSVirtualGoodContent. I would advise you stick this into a folder so that all custom content you create that relates to GameSparks features are kept together. Another reason we should use a folder here is that we are going to need to create an assembly reference for our Microservice so that they can reference this GSVirtualGoodContent class. We’ll cover that later.

Next, we are going to have this class inherit from ItemContent and give it a content-type name. This is the name it will appear as in the Content Manager menu.

using Beamable.Common.Content;
using Beamable.Common.Inventory;

[ContentType("virtual_goods")]
[System.Serializable]
public class GSVirtualGoodContent : ItemContent
{

}

Now we need to define the Virtual Good content. To keep things simple we will just add a description and an array of costs. This should give you enough to get started if you need to create more complex replicas of GameSparks Virtual Good configs.

using Beamable.Common.Content;
using Beamable.Common.Inventory;

[ContentType("virtual_goods")]
[System.Serializable]
public class GSVirtualGoodContent : ItemContent
{
   public string description;
   public VirtualGoodCost[] costs;
}

[System.Serializable]
public class VirtualGoodCost
{
   public enum CurrencyType
   {
       GEMS,
       COINS
   }

   public CurrencyType currencyType;
   public int amount;
}

If you save your script and head back into the editor you will see this new content-type appear in the Content Manager and we can add a few new items

To create a new Item, open the Content Manager and select Items from the Create down-down menu.

You can change the item’s Id to match the shortCode or name of your item in GameSparks. Selecting the item will show you the item’s attributes in the Inspector tab.

Here you can see the Client Permission attribute which you may have seen in other topics already. This allows you to control if this item can be granted through the client or only from the server or portal. You would generally want items to be granted by the server through a Microservice for example, but we will show some examples of how to grant these items using the client API, so for that test you would need to check this attribute.

You can also change the tag of your item here. Tags are maintained across all content in your game, so you could make this tag “vg” as I have done there, or you can subdivide the tag into “vg_weapons” if you want. Using the content service API you can get content by tag which is helpful, but we won't cover that in this topic.

Before we can start working with these new items, we need to publish the changes to the backend. To do this, click on the Publish button in the Content Manager.

You can also check if your content has been published by checking the Content section of the portal.

Inventory Service API

Now that our Virtual Goods are set up we’ll show how you can grant these items using the InventoryService API.

This example will also show how to credit/debit the cost of the item and grant the item.

Note - This example will show off the client API. As we already mentioned, you generally don't want to give the client control over granting items so we will also show how this is done with Cloud-Code later.

First thing we need to do is load our content reference so we can get the costs we set on these objects. There are two conventions for doing this in Beamable. The first thing you can do is declare a ItemRef variable in your script and load the item from the editor. The other way is to just set the Virtual Good content Id directly as a string.

// content Id as a string //
string vgIdString = "items.virtual_goods.diamond_sword";
// content Id as content Ref //
[SerializeField]
private ItemRef vgRef;

Next we will actually write our checks and credit/debit the cost of the Virtual Good. There is nothing complicated here, we will just use the inventory-service APIs to check the player balance and grant the item.

To keep the code shorter I have used an exception to detect if the player has enough currency for the transaction, but there are other ways you could do it in your project which might suit better.

/// <summary>
/// Delivers the VG item to the player and debits the cost of the VG.
/// Raises an exception if the player does not have the required balance.
/// </summary>
/// <param name="vgId">Id of the GSVirtualGoodContent content </param>
/// <exception cref="Exception">Could not grant VG - Invalid VGId</exception>
public async void GrantVirtualGood(string vgId)
{
   // get the Beamable API //
   var beamableAPI = await Beamable.API.Instance;
   // First we need a description of our Virtual Good so we can check the costs //
   var itemDetails = (GSVirtualGoodContent) await beamableAPI.ContentService.GetContent(vgId);
   try
   {
       // Now we can check if the player has enough of this currency type //
       foreach (VirtualGoodCost cost in itemDetails.costs)
       {
           Debug.Log($"Type: {cost.currencyType}, amount: {cost.amount}");
           string currencyId = "currency." + cost.currencyType;
           long currBalance = await beamableAPI.InventoryService.GetCurrency(currencyId);
           Debug.Log($"Player Balance [{cost.currencyType}] = {currBalance}");
           if (currBalance < cost.amount)
           {
               throw new Exception($"Insufficient balance [{cost.currencyType}]");
           }
       }
       // We know the player has enough balance for the item, so we will debit the player the cost and grant the item //
       Debug.Log("Granting item to player...");
       // debit the player the cost //
       foreach (VirtualGoodCost cost in itemDetails.costs)
       {
           // debit the currency costs //
           string currencyId = "currency." + cost.currencyType;
           await beamableAPI.InventoryService.AddCurrency(currencyId, -cost.amount);
       }
       // grant the item //
       await beamableAPI.InventoryService.AddItem(itemDetails.Id);
   }
   catch (Exception e)
   {
       // Log Warning or Error //
       Debug.LogWarning(e.Message);
   }
}

Remember that this function has to be async in order for the beamable-API calls to work. You can then run the function by passing in the content id. Below are two examples of how to do this with the string or the ItemRef variable.

GrantVirtualGood(vgIdString);
GrantVirtualGood(vgRef.GetId());

If you run this example, you should be able to see the player's balance change from the portal.

Custom Item Data

If you check the item granted to the player in the portal you can see there appears to be a JSON object associated with the item.

This is custom data you can apply to the item if you need something like a unique instance. The API takes a Dictionary<string, object> so you can use that to reproduce the JSON you want to apply to the item.

Fetching Player Inventory

We are also going to use the Inventory Service for fetching data. This is pretty simple, there is a generic all for getting all items and we can specify what type we want to cast to, or filter from a generic type.

Something that is handy with this API is that we can also get the ItemContent info from the item itself, allowing us to take different actions based on the content-type or other attributes like tag, or custom attributes.

// get all the VG items //
List<InventoryObject<GSVirtualGoodContent>> inventoryList = await beamableAPI.InventoryService.GetItems<GSVirtualGoodContent>();
foreach (InventoryObject<GSVirtualGoodContent> vg in inventoryList)
{
   // get the properties (we didnt set any in this example) //
   // vg.Properties
   // get the vg type if we need it //
   Debug.Log(vg.ItemContent.Id);
   Debug.Log(vg.ItemContent.description);
}

Microservices Example

In most cases you will want Virtual Goods to be processed by the server and not the client. So for the next example we will go through how to create a Microservice to handle this. Much of the code and setup is the same as the example in the previous section so we won't go into too much detail around the code needed. We also wont go into much detail about creating and starting Microservices as that is already covered in this topic here.

We will start by creating a new Microservice, in this example we are calling it VirtualGoodsService.

As mentioned in other topics, Microservices are intended to group together common functionality into one service. They aren't like GameSparks events where we create one for each task, so you might want to group the function we will create for granting items into a larger service used for custom transactions or inventory management.

Before we start working with that Microservice, we need to make sure we can reference the GSVirtualGoodContent class. Although publishing the content to the server allows us to reference the content in the Microservice from the backend it won't be able to recognise that content until we add an assembly definition for that content.

To do this, go to your GSContent folder and add a new Assembly Definition. We can call it CustomGSContentDef. You are going to need to add some assembly references to this definition. Make sure to hit the “apply” button before proceeding.

Note - It's common for this step to mess with your reference definitions for some of the files referencing the Beamable namespace. If this happens, first try to re-import the files from the folder. You can also restart Unity, which can help solve the problem. If that doesn't work, try removing the using statements in the scripts causing the problem and re-import them again.

Now we are going to add this assembly definition to our Microservice. You can find your Microservice folder at Beamable -> Microservices -> VirtualGoodsService. You will find two files in there, click on the assembly reference file and add a reference for the CustomGSContentDef file we just created.

Remember to hit the “Apply” button to update the file.

Now we can proceed to editing the VirtualGoodsService script so open that script. We can basically copy-paste our previous code into this Microservice script but there will be a few lines we need to update. We will need to import some references for our debug logs and change any references to the beamableAPI object to Sevices.Content or Services.Inventory. Remember also to rename the function and make it async.

One small change we’ll have to add is some kind of response code. This would be something you might want from a GameSparks response as you may want to show the player a popup if something went wrong or they did not have sufficient balance for the item. So we will also make this Microservice function return Task that will let us call await on the function and return a string which will be our response code.

[ClientCallable]
public async Task<string> GrantVirtualGood(string vgId)
{
  // First we need a description of our Virtual Good so we can check the costs //
  var itemDetails = (GSVirtualGoodContent) await Services.Content.GetContent(vgId);
  try
  {
     // Now we can check if the player has enough of this currency type //
     foreach (VirtualGoodCost cost in itemDetails.costs)
     {
        Debug.Log($"Type: {cost.currencyType}, amount: {cost.amount}");
        string currencyId = "currency." + cost.currencyType;
        long currBalance = await Services.Inventory.GetCurrency(currencyId);
        Debug.Log($"Player Balance [{cost.currencyType}] = {currBalance}");
        if (currBalance < cost.amount)
        {
           throw new Exception($"Insufficient balance [{cost.currencyType}]");
        }
     }
     // We know the player has enough balance for the item, so we will debit the player the cost and grant the item //
     Debug.Log("Granting item to player...");
     // debit the player the cost //
     foreach (VirtualGoodCost cost in itemDetails.costs)
     {
        // debit the currency costs //
        string currencyId = "currency." + cost.currencyType;
        await Services.Inventory.AddCurrency(currencyId, -cost.amount);
     }
     // grant the item //
     await Services.Inventory.AddItem(itemDetails.Id);
     return "item-granted";
  }
  catch (Exception e)
  {
     // Log Warning or Error //
     Debug.LogWarning(e.Message);
     return "insufficient-balance";
  }
}

And now you can call this Microservice function using the following code.

VirtualGoodsServiceClient _vgServiceClient = new VirtualGoodsServiceClient();
string respCode = await _vgServiceClient.GrantVirtualGood(vgIdString);
Debug.Log($"Response Code: {respCode}");

AccelByte

In AccelByte, Virtual Goods are known as Items. When compared to GameSparks, AccelByte has more configuration options on these Items which might be useful to you.

For example, while GameSparks allows you to create Virtual Goods as goods or consumable coin-packs, with AccelByte, you can configure Items to represent:

There are more details on how to set up each one of these types here, so we won't go into specifics on each item-type.

Virtual Goods Setup

Unlike GameSparks, creating a Virtual Good requires a few extra steps before we can begin. First you need to make sure you have some Virtual Currencies setup. We cover that flow in another topic here. You will also need to create a store. This store is where we add Virtual Goods, currency-packs, etc. We can create a store in the Admin portal by going to the Stores and clicking Create right beside Draft stores as shown below.

Next, we need to create categories to place certain Virtual Goods or Items in a meaningful category based on type of item. You can call these categories whatever you want, but it might be handy to use the same tags you used on your GameSparks Virtual Goods, if they were in use.

We can create categories as shown below.

We can now create virtual items in the stores. Click on Add to create an item.

A small window will pop-up where you can supply item name and item type.

Example: Coin Pack

We will briefly cover an example of some common GameSparks functionality developers use Virtual Goods for, namely, coin-packs.

These are basically a way for the player to gain currency in order to obtain more Virtual Goods. To configure this kind of Virtual Good, set the item-type to Coins and the Currency Code should be one of the Virtual Currencies we configured in the previous topic.

Once the item-type is selected you will see a new list of parameters appear.

Purchase limit is similar to the Max Quantity parameter in GameSparks Virtual Goods, this is the maximum amount the player can own at one time.

Note - Purchasing Virtual Goods differs from GameSparks in that you purchase AccelByte items through the Order Service API. For this topic we will deal with granting items to the player, purchases (IAPs) are covered in another topic here.

To set a price for the item you can scroll to the bottom of the form to the Default Region Data section. Here is where you can set the price for your item. In this case you can do something like convert coins to gems.

Leaving the price as zero entitles the player to the item for free.

Note - Remember that you have selected the Coins item-type, then we will get real currencies in the Currency Code field.

Example: In-Game Items

We can also select In-Game items as item-type. When you choose this item-type there are different parameters than for the currency-pack example above, and an important one we should select is the Entitlement Type.

Durable entitlements are like any other Virtual Good in GameSparks. Once you have a durable item you can use it forever.

Consumable entitlements however, can only be used a certain amount of times, so selecting that option will also present you with a User Count field.

Item Bundles

Bundle is another item-type you can select when creating your item. Similar to GameSparks, this item-type can be used to group multiple items together through one entitlement.

Promo-Codes & Key-Groups

The last item-type we will cover is codes. These are used to deliver certain items based on key-groups you create for your game. These are basically the items you can gift to players if they have promo-codes. We won't cover these in this topic, but there is more information about them here.

Purchasing Virtual Goods/Items

With AccelByte, we can use In-Game Virtual Currencies or a 3rd party payment service to order Virtual Goods. Both options follow the same procedures up until the point of making an order. When the order is created successfully, the player will have to pay through payment service or pay funds from our Wallet.

In GameSparks, we can use BuyVirtualGoodsRequest to buy Virtual Goods from a 3rd party store, but we should create an order to make any 3rd party payment in AccelByte.

In-App purchases and 3rd party Virtual Goods are covered in the next topic here.

Getting Virtual Good Definitions

In comparison to GameSparks, which only allows us to get all the Virtual Goods for our player, with AccelByte, we can retrieve either a single Virtual Good, or multiple Virtual Goods.

Every Virtual Good or Item you create through the portal will have Item ID as shown below.

We can supply this Item ID in order to query the Virtual Good along with region, language.

This will enable us to extract useful information about the Virtual Good configuration on the client

AccelBytePlugin.GetItems().GetItemById("567ff00f66404a5d906d30bc7b10f45b", "IE", "en",OnGetItem);

Similarly, we can get multiple items based on specific criteria with the below function. In Item criteria, we can specify region, language, item type as shown below.

ItemCriteria itemCriteria = new ItemCriteria
{
     region = "IE", 
     language = "en",
     itemType = ItemType.INGAMEITEM
};

AccelBytePlugin.GetItems().GetItemsByCriteria(itemCriteria, OnGetItems);

Order Creation

In this example, we have created an item called ‘Sword’ which costs 100 Coins (Virtual Currency). We can see the newly created sword item in the MasterMarket store below.

First, we need to retrieve the item’s information with the help of the GetItemById() method which is discussed in the above section.

Below is an example of how to get our item’s details in Unity...

abItem = AccelBytePlugin.GetItems();

//Sword’s Item Id
string itemId = "85b1b66e9fb74880a029a37dbe661834";

string region = "IE";
string language = "en-US";

//Call to retrieve Item info
abItem.GetItemById(itemId, region, language, GetItemByIdCallback);

/// <summary>
/// Retrieved virtual item information
/// </summary>
private void GetItemByIdCallback(Result<PopulatedItemInfo> result)
{
        itemResult = result;
        Debug.Log("Populated ItemInfo" + itemResult.Value.name);
}

Now that we have the item information, we need to build and order before actually sending the order.

We can use the item information from the previous step to make an order. We need a currency code, discountedPrice, itemId, price, quantity.

int quantity = 1;

//AccelByte.Models.OrderRequest
OrderRequest orderRequest = new OrderRequest
{
       currencyCode = itemResult.Value.regionData[0].currencyCode,
       discountedPrice = itemResult.Value.regionData[0].discountedPrice * quantity,
       itemId = itemResult.Value.itemId,
       price = itemResult.Value.regionData[0].price * quantity,
       quantity = quantity
};

And now we can create an order from this order request.

abOrder.CreateOrder(orderRequest, CreateOrderCallback );

/// <summary>
/// Once item is ordered and it will return order information
/// </summary>
/// <param name="result"> Order information</param>
private void CreateOrderCallback(Result<OrderInfo> result)
{
        Result<OrderInfo> createOrderResult = null;
        createOrderResult = result;
}

You can confirm your player received the item by going to the player-manager.

For this example the user has 200 coins in the wallet before the order and does not have items.

After creating the order you can see the order details listed in the player’s account. The wallet is also updated because the Sword item cost is 100 coins.

If you want to read more about AccelByte’s internal virtual item granting process, you can find it here

Nakama

Nakama does not have a comparable feature to GameSparks Virtual Goods, however there are several ways we can replicate this feature ourselves which we will discuss in this topic.

Because Nakama does not provide a way to configure Virtual Goods through a portal or something similar, we will need to create our own configuration files to replace your existing GameSparks Virtual Goods.

We are going to transition some simple Virtual Goods examples which we will be using throughout these tutorials.

These Virtual Goods are inventory items which cost a certain amount of Virtual Currency (coins and gems for this example) each.

They have no other special properties but you will see through the following examples that you can easily extend these Virtual Goods to work as bundles or credit currency instead of debiting, etc, so we won't cover all those use-cases here.

The structure of a Virtual Good on GameSparks looks something like this.

{
    "@id": "/~virtualGoods/diamond_armor",
    "W8StoreProductId": null,
    "WP8StoreProductId": null,
    "amazonStoreProductId": null,
    "bundledShortCodes": null,
    "currencyCostMap": {
      "COINS": 1300
    },
    "description": "diamond_armor",
    "disabled": false,
    "googlePlayProductId": null,
    "iosAppStoreProductId": null,
    "maxQuantity": null,
    "name": "diamond_armor",
    "propertySet": null,
    "psnStoreProductId": null,
    "segmentData": [],
    "shortCode": "diamond_armor",
    "steamStoreProductId": null,
    "tags": null,
    "type": "VGOOD",
    "xboxOneStoreProductId": null
  }

You can find a list of your own Virtual Goods configurations using the GameSparks REST APIs.

There is a guide in our Cloud-Code topic here on how to import these configuration JSONs into Nakama so we can use them from the Nakama Storage Engine.

For this tutorial we are not going to repeat the process for importing these files here so we advise you to check out the section on Transitioning GameSparks Configuration in the Cloud-Code topic here.

After importing our Virtual Goods into Nakama our collection looks something like this.

Now we are ready to work with some code examples.

Server Functionality

The first thing you will need to do is create a new TypeScript file.

For this example we will just be adding Virtual Goods, but in future you may need to group similar functionality for Virtual Goods together so in this topic we will put all the Virtual Goods functionality into a single file.

We are going to create a class which can be accessed from other scripts and add our first function to replicate the GameSparks API “buyVirtualGood”.

This function is simple. It needs to load the Virtual Good configuration with the given short code, debit the player the required cost and then deliver the Virtual Good.

class GSVirtualGoods {

   /**
    * Adds a new Virtual Good to the player's account and debits the balance
    * @param shortCode
    * @param userId
    * @param logger
    * @param nk
    * @returns {string} - result
    */
   static buyVirtualGood(shortCode: string, userId: string, logger: nkruntime.Logger, nk: nkruntime.Nakama): string {
       logger.info(`Granting ${userId}, Virtual Good: ${shortCode}`);
       // first we need to load the Virtual Good config //
       let vgsCursor: nkruntime.StorageObject[] = nk.storageRead([{
           collection: "GSData_VirtualGood",
           key: shortCode,
           userId: "00000000-0000-0000-0000-000000000000"
       }]);
       if (vgsCursor.length > 0) {
           let vg: any = vgsCursor[0].value;
           // debit the player the cost of the VG //
           for (var currencyKey in vg.currencyCostMap) {
               logger.info(`Debiting ${vg.currencyCostMap[currencyKey]} ${currencyKey}`);
               try {
                   nk.walletUpdate(userId, { [currencyKey]: -vg.currencyCostMap[currencyKey] });
               }
               catch (e) {
                   // Handle the error response if the player does not have enough balance //
                   logger.error(e.message);
                   return JSON.stringify({ "error": e.message });
               }
           }
           // grant the player the VG //
           // we need to read the player's account and update the vgs array //
           let playerAccount: nkruntime.Account = nk.accountGetId(userId);
           let playerMetaData: any = playerAccount.user.metadata;
           // Init the VG if it hasnt been added yet //
           if (!playerMetaData['vgs']) {
               playerMetaData['vgs'] = {};
           }
           if (!playerMetaData['vgs'][shortCode]) {
               playerMetaData['vgs'][shortCode] = 0;
           }
           playerMetaData['vgs'][shortCode]++;
           nk.accountUpdateId(userId, null, null, null, null, null, null, playerMetaData);
           logger.info(`Granted ${userId}, Virtual Good: ${shortCode}`);
           // handle success response //
           return JSON.stringify({ "success": true });
       }
       // handle error response //
       return JSON.stringify({ "error": `shortCode ${shortCode} not found` });
   }
}

Next, we need to create an RPC request which can call this function. We will put this request in the same script as our GSVirtualGood class.

/**
* Grants the player the amount for the given Virtual Good and debits them the cost
* @param context
* @param logger
* @param nk
* @param payloadString
*/
function rpcBuyVirtualGood(context: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, payloadString: string): string {
   logger.info("Data In: " + payloadString);
   const payloadData: any = JSON.parse(payloadString);
   const vgShortCode: string = payloadData.shortCode;
   return GSVirtualGoods.buyVirtualGood(vgShortCode, context.userId, logger, nk);
}

And finally, you will need to declare this RPC in the main.ts script’s InitModule function.

   initializer.registerRpc("buyVirtualGood", rpcBuyVirtualGood);

Reminder - Remember to add file references to the tsconfig.json file and rebuild and redeploy the server before testing

Testing

In the next section we will take a look at how to call this function from Unity but we don't need to build our client code in order to test this. Once your server is redeployed, you can test this from the Developer Console.

To do this, select the “API Explorer” option from the left-hand side menu of the Developer Console. Select the “buyvirtualgood” API and enter a user ID for one of your players.

Remember you will need to enter the short code of your Virtual Good in the request body field.

If everything is set up correctly you should see a successful response.

And we can double-check the player received the Virtual Good by checking their account from the “Accounts” window of the administrator portal.

Now that we have the buyVirtualGood() function working we would like to point out that you will need another function called “addVirtualGood” which will be mostly the same but it will not debit the player for the cost of the Virtual Good.

We will need this for other features like Achievements covered in the topic here where you want to add a Virtual Good to the player’s account but it does not consume currency but is instead awarded to the player.

/**
* Delivers the VG to the player but does not debit them
* @param shortCode
* @param userId
* @param logger
* @param nk
*/
static addVirtualGood(shortCode: string, userId: string, logger: nkruntime.Logger, nk: nkruntime.Nakama): string {
   logger.info(`Granting ${userId}, Virtual Good: ${shortCode}`);
   // first we need to load the Virtual Good config //
   let vgsCursor: nkruntime.StorageObject[] = nk.storageRead([{
       collection: "GSData_VirtualGood",
       key: shortCode,
       userId: "00000000-0000-0000-0000-000000000000"
   }]);
   if (vgsCursor.length > 0) {
       // grant the player the VG //
       // we need to read the player's account and update the vgs array //
       let playerAccount: nkruntime.Account = nk.accountGetId(userId);
       let playerMetaData: any = playerAccount.user.metadata;
       // Init the VG if it hasnt been added yet //
       if (!playerMetaData['vgs']) {
           playerMetaData['vgs'] = {};
       }
       if (!playerMetaData['vgs'][shortCode]) {
           playerMetaData['vgs'][shortCode] = 0;
       }
       playerMetaData['vgs'][shortCode]++;
       nk.accountUpdateId(userId, null, null, null, null, null, null, playerMetaData);
       logger.info(`Granted ${userId}, Virtual Good: ${shortCode}`);
       // handle success response //
       return JSON.stringify({ "success": true });
   }
   // handle error response //
   logger.error(`VG ${shortCode} no found...`);
   return JSON.stringify({ "error": `shortCode ${shortCode} not found` });
}

Alternative Server Approach

In the examples above we are storing Virtual Goods on the player’s metadata. There is a size limit of 16kb for player metadata so if your game needs something more complex than GameSparks Virtual Goods or you would prefer to create something more like an inventory system we should look at using the Nakama Storage Engine instead of player metadata.

This is very simple so let us take a look at how we might approach this in Nakama.

First we need to read the player’s current inventory object for the item with the given short-code and then we can update it and save it back.

// update inventory engine //
let playerVG: any = {};
let vgCursor: nkruntime.StorageObject[] = nk.storageRead([{
   collection: "Player_VirtualGood",
   key: shortCode,
   userId: userId
}]);

// init the player's item if it doesn't exist //
if (vgCursor.length === 0) {
   playerVG = { shortCode: shortCode, amount: 0, lastUpdated: Date.now() }
} else {
   playerVG = vgCursor[0].value;
}
playerVG.amount++;
// save this item back for the player //
nk.storageWrite([{
   collection: "Player_VirtualGood",
   key: shortCode,
   userId: userId,
   value: playerVG,
   permissionRead: 1, // owner read
   permissionWrite: 1 // owner write
}]);

All we are really doing here is updating the count and the lastUpdated field. You won examples might be different but this will work fine for a basic custom inventory.

If you test this code example using the “addVirtualGood” RPC function you will see the "Player_VirtualGood" Collection appear in the Storage list of the Developer Portal.

And we can see the data stored by clicking on the object.

Complex Storage

In the case where neither of these above options will work for you or you need something that allows for complex queries you can also check out the Nakama SQL API. You can also reach out to the Nakama team directly and they will work with you to help make sure you are getting the best solution and performance for your needs.

Unity Example

We have covered some examples of calling RPC requests from Unity in other topics.

It is a very simple call but the two things to remember are that the request payload must be formed manually using a C# Dictionary, and that the request method is asynchronous so it should be called from an async method.

/// <summary>
/// Purchases a Virtual Good for this player and debits them the cost
/// </summary>
/// <param name="session"></param>
/// <param name="shortCode"></param>
private async void BuyVirtualGood(ISession session, string shortCode)
{
   Debug.Log($"Buying Virtual Good {shortCode}");

   // construct request //
   string rpcId = "buyVirtualGood";
   Dictionary<string, string> updatedWallet = new Dictionary<string, string>()
   {
       { "shortCode" , shortCode }
   };
   var payload = Nakama.TinyJson.JsonWriter.ToJson(updatedWallet);
   IApiRpc responseData = await nakamaClient.RpcAsync(session, rpcId, payload);
   Debug.Log(responseData.Payload); // << response codes
}

Testing this code, you should be able to see the RPC’s response payload printed out to the console as a JSON string.

Using this basic recreation of GameSparks Virtual Goods in Nakama you should be able to follow on from the examples shown in this topic and replicate all the functionality you need.

Something that is not included in this topic is how Nakama handles In-App Purchases (IAPs).

For more information on IAPs you can check out our topic on 3rd Party Virtual Goods here.