© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023
B. BorsGame Backend Developmenthttps://doi.org/10.1007/978-1-4842-8910-5_6

6. Economy

Balint Bors1  
(1)
Munich, Germany
 

A well-designed virtual game economy improves player engagement and is a very popular way to generate revenue.

In a virtual economy, there are virtual items to buy, typically with virtual currencies. You can also let your players purchase virtual currencies, items, features, or practically anything using real money. This is a great way to monetize your game and improve player engagement.

This chapter focuses on the realization of such an infrastructure to enable a virtual economy and in-app purchasing (IAP) in your game.

Components

Let’s look more closely at the idea of a virtual economy and see what basic components you have to implement for this feature:
  • Items. Anything that a player can buy in your game.

  • Catalog. A list of all the items that a player can buy.

  • Virtual currencies. Your game can support multiple currencies that are used to buy virtual items from the catalog. This is very game specific. This chapter’s example uses the fictitious MyGame Coin (MC).

  • Player inventory. When a player buys an item from the catalog, it will be added to their personal inventory.

  • Stores. External third-party stores that allow in-app purchases and purchasing of virtual items for real money (not to be confused with PlayFab’s Store definition).

Figure 6-1 shows the main functions you should implement for this virtual economy:
  • Get Catalog. You want to allow players to see a list of items that they can purchase. You’ll store this in the game backend in a data structure and the game client will show in it in game-specific representation.

  • Get Inventory. Shows items purchased from the catalog that are stored in the backend. The same applies to the amount of virtual currencies the player owns.

  • Purchase Item. The players can start a purchase, and as a result, the chosen item will appear in the player’s inventory and the amount of virtual currency will be updated. If the player buys something for real money, that triggers the involvement of an external store.

Three blocks represent game clients including a catalogue with 3 inputs and player inventory with 2 inputs, a game backend with 3 inputs, and external stores with 3 inputs.

Figure 6-1

Components and functions of a virtual economy

In the following sections, you learn how to implement the backend components with both PlayFab and pure Azure services. You also learn how the game client can invoke the main functions to realize the virtual economy.

PlayFab

PlayFab provides a rich functionality to implement a game economy. It also simplifies this integration with multiple external stores, which can otherwise be a tedious task.

Economy GUI

Before going into the economy code, you need to create a simple window that will show the catalog and inventory items and the state of the virtual currency, and will allow players to start a purchase.

Note

This control panel is only required to demonstrate how to use the backend functionalities. It’s best to build your own custom UI for managing catalogs and inventory items that fits your game.

Go to ControlPanel.cs and extend it with the following code:
public class ControlPanel : MonoBehaviour
{
    public const int PLAYFAB_ECONOMY = 7;
    string textArea = " ";
    string economyTextAreaTitle = "";
    string itemToPurchase = "";
    string virtualCurrencyLabel = "";
    void OnGUI()
    {
        if (selection == PLAYFAB_ECONOMY)
            GUILayout.Window(0, new Rect(0, 0, 300, 0), PlayFabEconomyWindow, "PlayFab Economy");
    }
    void OptionsWindow(int windowID)
    {
        if (GUILayout.Button("PlayFab Economy"))
        {
            selection = PLAYFAB_ECONOMY;
        }
    }
    void PlayFabEconomyWindow(int windowID)
    {
        if (economyTextAreaTitle != "")
            GUILayout.Label(economyTextAreaTitle);
        textArea = ShopUI.GetTextArea();
        textArea = GUILayout.TextArea(textArea, 200);
        virtualCurrencyLabel = ShopUI.GetVirtualCurrencyLabel();
        if (virtualCurrencyLabel != "")
            GUILayout.Label(virtualCurrencyLabel);
        GUILayout.Space(10);
        if (GUILayout.Button("Get Catalog Items"))
        {
            playFab.GetComponent<PlayFabEconomy>().GetCatalogItems();
            economyTextAreaTitle = "Catalog Items";
        }
        if (GUILayout.Button("Get Player Inventory + Virtual Currency"))
        {
            playFab.GetComponent<PlayFabEconomy>().GetInventory();
            economyTextAreaTitle = "Player Inventory";
        }
        GUILayout.Space(10);
        if (GUILayout.Button("Purchase this item (number):"))
        {
            playFab.GetComponent<PlayFabEconomy>().PurchaseItem(itemToPurchase);
        }
        itemToPurchase = GUILayout.TextField(itemToPurchase, 100);
        GUILayout.Space(10);
        if (GUILayout.Button("Buy from store"))
        {
            playFab.GetComponent<PlayFabEconomy>().BuyFromStore(itemToPurchase);
        }
        GUILayout.Space(10);
        if (GUILayout.Button("Cancel"))
        {
            selection = ROOTMENU;
        }
    }
}
Note that the PlayFabEconomy and the ShopUI classes are missing. You will create those in the next steps. Create a new class called ShopUI.cs, which manages and fills the text area:
using System.Collections.Generic;
using PlayFab.ClientModels;
public static class ShopUI
{
    private static string textArea = " ";
    private static string virtualCurrencyLabel = "";
    public static void UpdateTextArea(List<ItemInstance> items)
    {
        string ta = "";
        int number = 0;
        foreach (ItemInstance item in items)
        {
            number++;
            ta += number + " | " + item.DisplayName + " ";
        }
        for (int I = items.Count; I < 5; i++)
        {
            ta += " ";
        }
        textArea = ta;
    }
    public static void UpdateVirtualCurrency(string VC_Name, int VC_Amount)
    {
        virtualCurrencyLabel = "You have " + VC_Amount + " " + VC_Name;
    }
    public static void UpdateTextArea(List<CatalogItem> items)
    {
        string ta = "";
        int number = 0;
        foreach (CatalogItem item in items)
        {
            number++;
            ta += number + " | " + item.DisplayName + ".....";
            if (item.VirtualCurrencyPrices.Count != 0)
            {
                ta += item.VirtualCurrencyPrices["MC"] + " MC";
            }
            else
            {
                ta += "n/a";
            }
            ta += " ";
        }
        for (int I = items.Count; I < 5; i++)
        {
            ta += " ";
        }
        textArea = ta;
    }
    public static string GetVirtualCurrencyLabel()
    {
        return virtualCurrencyLabel;
    }
    public static string GetTextArea()
    {
        return textArea;
    }
}

Now that you have the GUI to display the items and related control buttons, let’s move on to implementing the core functionality.

Catalog and Items

Figure 6-2 shows how to create a new catalog in PlayFab. Go to PlayFab’s Game Manager, then choose ENGAGE ➤ Economy ➤ New Catalog.

A screenshot of My game studio displays uploads JSON and new catalogue options. Two arrows point towards the option of a new catalog and economy tab.

Figure 6-2

Create a new catalog in PlayFab

Name your catalog and click Save Catalog. This will create a catalog with one item. To delete the primary catalog, remove all the items.

Now add some new items to the catalog, as shown in Figure 6-1. Here are some options to choose from:
  • Durable or Consumable. If your item is consumable, you can buy it repeatedly. You can also determine how many times the players can consume it and, optionally, determine the time between buying and consuming. For example, you can buy virtual currency for real money. Players can buy durable items only once. For example, removing advertisements.

  • Is stackable, tradable. If an item is stackable, even if the player buys it several times, it will appear only once in the player inventory. If it’s tradable, the player can hand the item over to other players. Stackable items are not tradable.

  • Prices. You can determine the price of the item after you define a virtual currency. You will do this step later.

  • Item class, tags, description, custom data… You can specify many additional attributes for your items. Define those fields according to your game’s needs.

Now that you have filled the catalog, you can go back to Unity and use the PlayFab API. You’ll write the client code to access the catalog. Create a new file called PlayFabEconomy.cs and copy the following code:
using System.Collections.Generic;
using UnityEngine;
using PlayFab.ClientModels;
using PlayFab;
public class PlayFabEconomy : MonoBehaviour
{
    List<CatalogItem> catalogItems = new List<CatalogItem>();
    public void GetCatalogItems()
    {
        PlayFabClientAPI.GetCatalogItems(new GetCatalogItemsRequest()
        {
            CatalogVersion = "MyGame Catalog",
        },
        result =>
        {
            catalogItems.Clear();
            foreach (CatalogItem item in result.Catalog) catalogItems.Add(item);
            ShopUI.UpdateTextArea(catalogItems);
        },
        error =>
        {
                Debug.Log(error.GenerateErrorReport());
        });
    }
}

This will read all the catalog items and display them to the users using the ShopUI class.

Virtual Currency

It is time to create your virtual currency. You can do this in PlayFab’s Game Manager, as shown in Figure 6-3.

A screenshot of My game studio displays currency and adds a new currency option on the Dashboard. Three arrows point to the new currency, currency, and economy option.

Figure 6-3

Create a virtual currency

Let’s create the currency called MyGame Coin, with the currency code MC. Set the initial deposit to 100, which is what you’ll grant to every user once.

Figure 6-4 shows where you can find the actual amount of virtual currency for a specific player.

A screenshot of My game studio displays virtual currency options in a player's option. A dashboard shows the code, display name, and account name.

Figure 6-4

Virtual currencies of a player

Now that you have an official virtual currency, you can add some prices to your items in the catalog. Go back to your catalog (choose Economy ➤ MyGame Catalog) and, from Prices, add your prices in MC to each item.

Player Inventory

Next, you’ll list the items a player owns. First, add some items to a player (for free) from the catalog, as shown in Figure 6-5.

A screenshot of My game studio displays a grant item in the inventory option. 3 arrows point towards the new players, grant items, and inventory options.

Figure 6-5

Grant items to a player

To read the actual items of a player, go to the PlayFabEconomy.cs class and add the following code to it. This will not only read the contents of the player’s inventory, but will also list the available virtual currencies for buying additional items:
List<ItemInstance> playerInventoryItems = new List<ItemInstance>();
    public void GetInventory()
    {
        PlayFabClientAPI.GetUserInventory(new GetUserInventoryRequest(),
        result =>
        {
            playerInventoryItems.Clear();
            foreach (ItemInstance item in result.Inventory) playerInventoryItems.Add(item);
            ShopUI.UpdateTextArea(playerInventoryItems);
            ShopUI.UpdateVirtualCurrency("MC", result.VirtualCurrency["MC"]);
        },
        error =>
        {
            Debug.Log(error.GenerateErrorReport());
        });
    }

This example hard-codes the MC virtual currency, but of course you can align this code with your actual game. Note that the GetUserInventory API call will provide both the inventory items and the current state of the player’s wealth.

Purchasing an Item

Letting players buy items is the main goal. PlayFab makes this really simple. You simply have to invoke the PurchaseItem method with the catalog name, item ID, price, and virtual currency type.

Extend the PlayFabEconomy.cs with the following code, which allows the players to buy a specific item using MC:
    public void PurchaseItem(string itemToPurchase)
    {
        int itemNumber = int.Parse(itemToPurchase) – 1;
        PlayFabClientAPI.PurchaseItem(new PurchaseItemRequest()
        {
            CatalogVersion = "MyGame Catalog",
            ItemId = catalogItems[itemNumber].ItemId,
            Price = (int)catalogItems[itemNumber].VirtualCurrencyPrices["MC"],
            VirtualCurrency = "MC"
        },
        result =>
        {
            Debug.Log("Purchase completed.");
        },
        error =>
        {
            Debug.Log(error.GenerateErrorReport());
        });
    }

If everything went fine, the item should appear in the player’s inventory and the amount of virtual currency should decrease by the price of the item. Drag-and-drop the PlayFabEconomy component to the PlayFab game object in Unity and verify that it works. Comment out the BuyFromStore option, as you will implement it in the next section.

Bundles, Containers, Drop Tables, Stores

PlayFab provides several extra features to satisfy different game requirements. Bundles allow you to group items and grant them in a bundle to a player. A container is a group of items that’s closed until the player opens it. With drop tables, you can reward players with items. Stores are subsets of catalogs, where you can override prices for the items.

In-App Purchasing

So far, you have implemented a solution to buy virtual items for virtual currencies. What if players could buy something for real money and that would cover your efforts in making a game? That’s sounds good, but you need some additional components.

First, you need a real store that can manage purchasing with real credit cards. Each platform has its own store. For example, if you develop an Android game, you’ll use the Google Play Store.

Second, Unity IAP helps you interface with those different stores. So you only have to set up your stores and then can easily communicate with them through Unity IAP.

Make sure that your catalog is in sync with the store’s catalog. You have to add your relevant items to the store as well; otherwise, the store cannot refer to them when the purchasing request arrives.

After all, purchased items only make sense in your game. If the purchase was successful, the store provides a receipt. After validating the receipt, you have to make sure the item appears in the player’s inventory.

To implement this, first turn on in-app purchasing in Unity. It is under the Services panel. It is also under the Edit ➤ Project Settings… ➤ Services ➤ In-App Purchasing menu.

Next, create a class in UnityIAPManager.cs. This uses the UnityEngine.Purchasing namespace and implements the IStoreListener interface. This way, you can receive Unity IAP events. You have to implement three functions:
  1. 1.

    Initialize Unity IAP. Here, configure Unity IAP and make it ready to execute purchases. Before calling the Initialize method, you upload the items from the catalog.

     
  2. 2.

    Initiate a purchase. It is only possible if you earlier initialized the Unity IAP. If this is successfully completed, it comes back with the receipt.

     
  3. 3.

    Validate a receipt. You have to make sure that the player actually bought the product and paid for it. PlayFab API provides a simple way to validate receipts from different stores. For example, with Google Play Store, you use PlayFabClientAPI.ValidateGooglePlayPurchase.

     
Note

Unity IAP supports multiple stores (Google Play Store, Microsoft Store, Amazon Store, Apple Store, etc.), but if you want to use those stores, you need to register your game first. Instead, you can create a Fake Store for developing activities. The drawback is that receipt validation is not possible with this Fake Store scenario.

Copy the following code into the UnityIAPManager.cs file:
using UnityEngine;
using UnityEngine.Purchasing;
using System.Collections.Generic;
using PlayFab.ClientModels;
public class UnityIAPManager : IStoreListener
{
    private IStoreController controller;
    public UnityIAPManager(List<CatalogItem> catalog)
    {
        InitializeUnityIAP(catalog);
    }
    public void InitializeUnityIAP(List<CatalogItem> catalog)
    {
        var module = StandardPurchasingModule.Instance();
        var builder = ConfigurationBuilder.Instance(module);
        foreach (var item in catalog)
        {
            builder.AddProduct(item.ItemId, ProductType.NonConsumable);
        }
        UnityPurchasing.Initialize(this, builder);
    }
    public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
    {
        Debug.Log("Unity IAP is initialized. It is ready to make purchases.");
        this.controller = controller;
    }
    public void OnInitializeFailed(InitializationFailureReason error)
    {
        Debug.Log("Initialization error: " + error);
    }
    public void PurchaseItem(string productId)
    {
        controller.InitiatePurchase(productId);
    }
    public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs e)
    {
        Debug.Log("Purchase completed. ProductId: " + e.purchasedProduct.definition.id + " Receipt: " + e.purchasedProduct.receipt);
        // TODO : receipt validation + grant item (only with real store / receipt)
        return PurchaseProcessingResult.Complete;
    }
    public void OnPurchaseFailed(Product i, PurchaseFailureReason p)
    {
        Debug.Log("Purchase failed. Reason: " + p);
    }
}
Insert the Unity IAP initialization call into the requesting catalog items. Also, implement the BuyFromStore method to start a purchase. Go to PlayFabEconomy.cs and add the following code:
   UnityIAPManager unityIAPManager;
    public void GetCatalogItems()
    {
        PlayFabClientAPI.GetCatalogItems(new GetCatalogItemsRequest()
        {
            CatalogVersion = "MyGame Catalog",
        },
        result =>
        {
            unityIAPManager = new UnityIAPManager(catalogItems);
        },
        error =>
        {
            Debug.Log(error.GenerateErrorReport());
        });
    }
   public void BuyFromStore(string itemToPurchase)
    {
        int itemNumber = int.Parse(itemToPurchase) - 1;
        unityIAPManager.PurchaseItem(catalogItems[itemNumber].ItemId);
    }

Now you should be able to buy items from the store by providing their number and clicking the Buy from Store button on the Control Panel. As a result, you get a receipt, which you can validate and add the item to your inventory.

In the next part, you will implement the same functionality with pure Azure services. That solution might be more flexible, but PlayFab provides a lot of features (receipt validation, coupons, etc.) that you have to implement by yourself.

Azure

You can use a similar architecture to the one used with other game backend features, when implementing a game economy in Azure. However, you need to take a slightly different approach than with other backend features.

Here, you need to store data for catalogs and player inventories. A player typically owns multiple items, and a catalog can contain several products. This requirement fits a document-oriented (NoSQL) database much better than a relational database.

In a document-oriented database, you can store each catalog in a separate document. In this example, you store all the items of the MyGame Catalog in a document:
{
        "_id" : ObjectId("62b9b009ea2970a0481ce3d4"),
        "CatalogId" : "MyGame Catalog",
        "Items" : [
                {
                        "ItemId" : "shield",
                        "DisplayName" : "Shield",
                        "Price" : 4,
                        "Currency" : "MC"
                },
                {
                        "ItemId" : "helmet",
                        "DisplayName" : "Helmet",
                        "Price" : 8,
                        "Currency" : "MC"
                },
                {
                        "ItemId" : "weapon",
                        "DisplayName" : "Weapon",
                        "Price" : 9,
                        "Currency" : "MC"
                }
        ]
}

You organize these documents into collections. For example, you can put different catalog documents in the Catalog collection.

Azure supports the implementation of such a document-oriented database through the Azure Cosmos DB account. Actually, you will use the MongoDB API to access and manipulate data.

As Figure 6-6 shows, you can use API Management Services to interface your clients with the Azure Function app. Only authenticated players can call the Economy API. The Azure Function App then queries and manipulates the data on the Cosmos DB.

A process chart of the My game catalog shows player identity, followed by A P I management, azure function application and finally, azure COSMOS D B.

Figure 6-6

Economy on Azure

Building the Economy Backend

Again, you can reuse some shared resources, which were also required in other chapters of this book. Create a new subfolder called economy. You have to create a new economy module and provide the shared resources to it in the main.tf file:
module "economy" {
  source                     = "./economy"
  api_management_name        = module.shared.api_management_name
  shared_resource_group_name = module.shared.shared_resource_group_name
  app_service_plan_id        = module.shared.app_service_plan_id
  storage_account_name       = module.shared.storage_account_name
  storage_account_access_key = module.shared.storage_account_access_key
}
Then, go to the economy folder and create a new resource group (in resourcegroup.tf), where you will put the new economy-related resources:
resource "azurerm_resource_group" "economy" {
  name     = "economy-resources"
  location = "West Europe"
}
Create a new file called functionapp.t, and add the Azure Function App resource to it. It refers to the shared App Service Plan and Storage Account. This will create an empty Azure Function App where you will upload your functions:
variable "app_service_plan_id" {
  description = "AppService Plan ID"
}
variable "storage_account_name" {
  description = "Storage account name"
}
variable "storage_account_access_key" {
  description = "Storage account access key"
}
resource "azurerm_function_app" "economy" {
  name                       = "economy-azure-functions"
  location                   = azurerm_resource_group.economy.location
  resource_group_name        = azurerm_resource_group.economy.name
  app_service_plan_id        = var.app_service_plan_id
  storage_account_name       = var.storage_account_name
  storage_account_access_key = var.storage_account_access_key
  version                    = "~4"
}
data "azurerm_function_app_host_keys" "economy" {
  name                = azurerm_function_app.economy.name
  resource_group_name = azurerm_resource_group.economy.name
}
Now create the Azure Cosmos DB API for MongoDB. During the development phase, you can turn on the enable_free_tier option. This example uses the weakest consistency level, which you should change when moving to production. Create a new file called cosmosdb.tf and copy the following code into it:
resource "azurerm_cosmosdb_account" "economy" {
  name                = "mygame-cosmos-db"
  location            = azurerm_resource_group.economy.location
  resource_group_name = azurerm_resource_group.economy.name
  offer_type          = "Standard"
  kind                = "MongoDB"
  enable_free_tier    = true
  capabilities {
    name = "MongoDBv3.4"
  }
  capabilities {
    name = "EnableMongo"
  }
  consistency_policy {
    consistency_level       = "Eventual"
  }
  geo_location {
    location          = azurerm_resource_group.economy.location
    failover_priority = 0
  }
}
resource "azurerm_cosmosdb_mongo_database" "economy" {
  name                = "mygame-cosmos-mongo-db"
  resource_group_name = azurerm_resource_group.economy.name
  account_name        = azurerm_cosmosdb_account.economy.name
}

Execute the terraform script (terraform init, terraform apply). Note that you should comment out the api_management_name and shared_resource_group_name variables, which you will define later with the API Management Service. This will build the required infrastructure components in Azure.

In the next step, you implement the functions that allow the access and manipulation of data in the database.

Creating the Economy Functions

Using the Azure Core Tools, create a new dotnet project:
func init EconomyFunctions
Then, add the MongoDB packages to your project. This time you will use the MongoDB packages instead of the EntityFramework. Go to the EntityFunctions folder and execute the following commands:
dotnet add package MongoDB.Driver
dotnet add package MongoDB.Bson
Request the connection string of the MongoDB by providing the parameters of your database:
az cosmosdb keys list --type connection-strings -g economy-resources --name mygame-cosmos-db
Copy the connection string into the local.settings.json file. This will allow you to test the functions locally:
{
    "IsEncrypted": false,
    "Values": {
        ...
        "MongoDBConnectionString" : "mongodb://mygame-cosmos-db:<password>[email protected]:10255/?ssl=true&replicaSet=globaldb&retrywrites=false&maxIdleTimeMS=120000&appName=@mygame-cosmos-db@"
    }
}
Now start the implementation with the data structure. MongoDB uses BSON format, which is the binary form of JSON. Create a file called Item.cs and provide the following fields for each item:
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace EconomyFunctions
{
    public class Item
    {
        [BsonElement("ItemId")]
        public string ItemId { get; set; }
        [BsonElement("DisplayName")]
        public string DisplayName { get; set; }
        [BsonElement("Price")]
        public int Price { get; set; }
        [BsonElement("Currency")]
        public string Currency { get; set; }
    }
}
You can obviously define any fields for one item. To each player, assign a list of items in PlayerInventory.cs:
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using System.Collections.Generic;
namespace EconomyFunctions
{
    public class PlayerInventory {
        [BsonId]
        public ObjectId Id { get; set; }
        [BsonElement("PlayerId")]
        public string PlayerId { get; set; }
        [BsonElement("Items")]
        public List<Item> Items { get; set; }
    }
}
You can also create multiple catalogs, each with different items. Create a Catalog.cs file, which includes all the related fields:
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using System.Collections.Generic;
namespace EconomyFunctions
{
    public class Catalog {
        [BsonId]
        public ObjectId Id { get; set; }
        [BsonElement("CatalogId")]
        public string CatalogId { get; set; }
        [BsonElement("Items")]
        public List<Item> Items { get; set; }
    }
}
Players can have different virtual currencies. You need to store the name of the virtual currency and the amount in VirtualCurrency.cs:
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace EconomyFunctions
{
    public class VirtualCurrency {
        [BsonElement("VC_Amount")]
        public int VC_Amount { get; set; }
        [BsonElement("VC_Name")]
        public string VC_Name { get; set; }
    }
}
Finally, each player can get multiple virtual currencies. Define this in PlayerVirtualCurrency.cs:
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using System.Collections.Generic;
namespace EconomyFunctions
{
    public class PlayerVirtualCurrency {
        [BsonId]
        public ObjectId Id { get; set; }
        [BsonElement("PlayerId")]
        public string PlayerId { get; set; }
        [BsonElement("VirtualCurrencies")]
        public List<VirtualCurrency> VirtualCurrencies { get; set; }
    }
}
Now that you have the data structures, you can create an Economy.cs file and add the Catalog function. This function is pretty long, so we break it into parts with some explanation:
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System.Linq;
using MongoDB.Bson;
using MongoDB.Driver;
namespace EconomyFunctions
{
    public class Economy
    {
        [FunctionName("Catalog")]
        public async Task<IActionResult> Catalog(
                [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", "delete", Route = null)] HttpRequest req,
                ILogger log)
        {
            string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
            Catalog data = JsonConvert.DeserializeObject<Catalog>(requestBody);
            var client = new MongoClient(System.Environment.GetEnvironmentVariable("MongoDBConnectionString"));
            var database = client.GetDatabase("mygame-cosmos-mongo-db");
            var collection = database.GetCollection<Catalog>("Catalog");
...
In this first part, you implement a new function called Catalog, which is executed by receiving an HTTP request. This can be GET, POST, and DELETE. The incoming request comes in JSON format, which you need to deserialize to the earlier defined data structures. Also, you need to create a collection variable referring to the catalog collection in the database. This collection contains all the catalogs in document format.
...
            string responseMessage = "";
            if (req.Method.Equals("POST"))
            {
                var catalogFilter = new BsonDocument { { "CatalogId", data.CatalogId } };
                var res = await collection.Find(catalogFilter).FirstOrDefaultAsync();
                if (res != null) // Catalog exists
                {
                    bool itemExists = false;
                    foreach (var item in data.Items)
                    {
                        var fittingVirtualCurrency = res.Items.Find(x => x.ItemId == item.ItemId);
                        if (fittingVirtualCurrency != null) itemExists = true;
                    }
                    if (itemExists) // Item exists
                    {
                        responseMessage = "Item is already existing in the catalog.";
                    }
                    else // Item not exists
                    {
                        var filter = Builders<Catalog>.Filter.Where(x => x.CatalogId == data.CatalogId);
                        foreach (var item in data.Items)
                        {
                            var update = Builders<Catalog>.Update.Push<Item>(x => x.Items, item);
                            var result = collection.UpdateOneAsync(filter, update).Result;
                        }
                        responseMessage = "There was no such item(s) in this catalog so far. Item(s) added.";
                    }
                }
                else // Catalog not exists
                {
                    collection.InsertOne(data);
                    responseMessage = "This catalog hasn't existed so far. Added " + data.CatalogId + ".";
                }
            }
...
The POST request allows players to upload catalog items. This is analog to PlayFab’s User Generated Content (UGC) option. Create a new catalog if the catalog doesn’t exist. If the catalog does exist, verify that the item is available. If it’s not, add it to the catalog. A player can add multiple items to the catalog at the same time. The data should arrive in JSON format.
...
   else if (req.Method.Equals("GET"))
            {
                string catalogId = req.Query["CatalogId"];
                var filter = new BsonDocument { { "CatalogId", catalogId } };
                var res = await collection.Find(filter).FirstOrDefaultAsync();
                if (res == null)
                {
                    responseMessage = "This catalog has no items.";
                }
                else
                {
                    responseMessage = JsonConvert.SerializeObject(res);
                }
            }
...
With HTTP GET request, you simply try to find the asked catalog in the collection and send the whole document to the player. The player provides the CatalogId in the URL.
...
else if (req.Method.Equals("DELETE"))
            {
                foreach (var item in data.Items)
                {
                    var filter = Builders<Catalog>.Filter.Where(x => x.CatalogId == data.CatalogId && x.Items.Any(i => i.ItemId == item.ItemId));
                    var update = Builders<Catalog>.Update.Pull<Item>(x => x.Items, item);
                    var result = collection.UpdateOneAsync(filter, update).Result;
                    if (result.ModifiedCount > 0)
                    {
                        responseMessage += item.ItemId + " is deleted. ";
                    }
                    else if (result.ModifiedCount == 0)
                    {
                        responseMessage += item.ItemId + " is not existing. ";
                    }
                }
            }
            return (ActionResult)new OkObjectResult(responseMessage);
        }
    }
}

You must also implement the DELETE method, which allows you to delete items from the catalog. Depending on your game, you can decide if your player can delete items from a catalog as well.

Implementing the Inventory and VirtualCurrency functions is almost the same as implementing Catalog. You can download the complete code including these functions from GitHub. We do not discuss them here to keep the chapter shorter.

Finally, it’s time to see if the program works locally:
func start
In another window, send a new catalog item:
curl -X POST http://localhost:7071/api/Catalog -d "{CatalogId: 'MyGame Catalog', Items: [{ItemId: 'sword', DisplayName:'Sword', Price:2, Currency: 'MC'}]}"
Test if it arrived:
curl http://localhost:7071/api/Catalog?CatalogId=MyGame%20Catalog
{"Id":"62bd50b4c1519dc85a319f7e","CatalogId":"MyGame Catalog","Items":[{"ItemId":"sword","DisplayName":"Sword","Price":2,"Currency":"MC"}]}

Next, you will publish the functions to Azure and make them accessible through an API.

Publishing the Functions to Azure

Start by updating your Application Settings in Azure with the connection string of the MongoDB.
az functionapp config appsettings set -n economy-azure-functions -g economy-resources --settings MongoDBConnectionString="<your connection string>"
Then, publish the functions with the help of the Azure Core Tools:
func azure functionapp publish economy-azure-functions
Send a new item to the catalog:
curl -X POST https://economy-azure-functions.azurewebsites.net/api/catalog -d "{CatalogId: 'MyGame Catalog', Items: [{ItemId: 'shield', DisplayName:'Shield', Price:4, Currency: 'MC'}]}"
Finally, check if it arrived:
curl https://economy-azure-functions.azurewebsites.net/api/catalog?CatalogId=MyGame%20Catalog
{"Id":"62bd50b4c1519dc85a319f7e","CatalogId":"MyGame Catalog","Items":[{"ItemId":"sword","DisplayName":"Sword","Price":2,"Currency":"MC"},{"ItemId":"shield","DisplayName":"Shield","Price":4,"Currency":"MC"}]}

Implement Economy API

You’ll use API Management Service again as an interface for your clients to the backend functions. It also ensures that only authenticated clients can call the function. Create an apim.tf file and add the following code:
variable "api_management_name" {
    description = "Name of the API Management service"
}
variable "shared_resource_group_name" {
  description = "Shared resource group name"
}
resource "azurerm_api_management_api" "economy" {
  name                  = "economy-api"
  resource_group_name   = var.shared_resource_group_name
  api_management_name   = var.api_management_name
  revision              = "1"
  path                  = "economy"
  display_name          = "Economy API"
  protocols             = ["https"]
  subscription_required = false
}
resource "azurerm_api_management_api_operation" "economy-post-catalog" {
  operation_id        = "economy-post-catalog"
  api_name            = azurerm_api_management_api.economy.name
  api_management_name = var.api_management_name
  resource_group_name = var.shared_resource_group_name
  display_name        = "POST Catalog"
  method              = "POST"
  url_template        = "/Catalog"
}

As an analog to the Catalog’s POST method, define additional resources for Catalog (GET, DELETE), Inventory (GET, POST), and VirtualCurrency (GET, POST).

The backend definition should point to the published functions, and the API policy expects the access token before forwarding the request to the functions.
resource "azurerm_api_management_backend" "economy" {
  name                = "economy-azure-functions-backend"
  resource_group_name = var.shared_resource_group_name
  api_management_name = var.api_management_name
  protocol            = "http"
  url                 = "https://${azurerm_function_app.economy.default_hostname}/api/"
  resource_id         = "https://management.azure.com/${azurerm_function_app.economy.id}"
  credentials {
    header = {
      x-functions-key = "${data.azurerm_function_app_host_keys.economy.default_function_key}"
    }
  }
}
resource "azurerm_api_management_api_policy" "economy" {
  api_name            = azurerm_api_management_api.economy.name
  api_management_name = var.api_management_name
  resource_group_name = var.shared_resource_group_name
  xml_content = <<XML
<policies>
  <inbound>
    <set-backend-service id="apim-policy" backend-id="economy-azure-functions-backend" />
    <validate-jwt header-name="Authorization" failed-validation-httpcode="401" failed-validation-error-message="Unauthorized. Access token is missing or invalid.">
        <openid-config url="https://gamebackend2022.b2clogin.com/gamebackend2022.onmicrosoft.com/v2.0/.well-known/openid-configuration?p=B2C_1_SignInSignUp" />
        <audiences>
            <audience>e4d1e07d-81ff-45b8-b849-91bef09b5bbd</audience>
        </audiences>
        <issuers>
            <issuer>https://gamebackend2022.b2clogin.com/fe46bdde-805c-4a42-83c6-871e0f4278e7/v2.0/</issuer>
        </issuers>
    </validate-jwt>
  </inbound>
</policies>
XML
}

Now you can enable the api_management_name and shared_resource_group_name lines and execute the terraform script (terraform apply).

Developing the Client

Now we’ll create a new file called AzureEconomy.cs in Unity and add it to the Azure game object. This will include all the logic required for calling the backend. We will use the same method as in other chapters, where we used HTTP requests to communicate with the backend.

In Unity, implement the same classes (Catalog, PlayerInventory, Item, VirtualCurrency, and PlayerVirtualCurrency) as in the backend. However, you don’t need the BSON attributes and the ObjectId. To implement this, add the following code to the AzureEconomy.cs script:
public class Catalog
{
    public string CatalogId { get; set; }
    public List<Item> Items { get; set; }
}
public class PlayerInventory
{
    public string PlayerId { get; set; }
    public List<Item> Items { get; set; }
}
public class Item
{
    public string ItemId { get; set; }
    public string DisplayName { get; set; }
    public int Price { get; set; }
    public string Currency { get; set; }
}
public class PlayerVirtualCurrency
{
    public string PlayerId { get; set; }
    public List<VirtualCurrency> VirtualCurrencies { get; set; }
}
public class VirtualCurrency
{
    public int VC_Amount { get; set; }
    public string VC_Name { get; set; }
}

Getting Items

Reading the Catalog requires a simple GET request. Add the following code to the AzureEconomy.cs file:
using System.Collections.Generic;
using UnityEngine;
using System.Net;
using System.IO;
using Newtonsoft.Json;
using System.Text;
public class AzureEconomy : MonoBehaviour
{
    Catalog catalog = new Catalog();
    PlayerVirtualCurrency playerVirtualCurrency = new PlayerVirtualCurrency();
    UnityIAPManager unityIAPManager;
    public void GetCatalogItems()
    {
        HttpWebRequest request = (HttpWebRequest)WebRequest.Create("https://shared-apim.azure-api.net/economy/Catalog?CatalogId=MyGame%20Catalog");
        request.Method = "GET";
        request.Headers.Add("Authorization", "Bearer " + GetComponent<AzureSettings>().token);
        HttpWebResponse response = (HttpWebResponse)request.GetResponse();
        StreamReader reader = new StreamReader(response.GetResponseStream());
        string result = reader.ReadToEnd();
        catalog = JsonConvert.DeserializeObject<Catalog>(result);
        unityIAPManager = new UnityIAPManager(catalog);
        ShopUI.UpdateTextArea(catalog.Items);
    }
...
Now update the UnityIAPManager class so that it accepts the non-PlayFab catalog type:
public class UnityIAPManager : IStoreListener
{
    public UnityIAPManager(Catalog catalog)
    {
        InitializeUnityIAP(catalog.Items);
    }
    public void InitializeUnityIAP(List<Item> catalog)
    {
        var module = StandardPurchasingModule.Instance();
        var builder = ConfigurationBuilder.Instance(module);
        foreach (var item in catalog)
        {
            builder.AddProduct(item.ItemId, ProductType.NonConsumable);
        }
        UnityPurchasing.Initialize(this, builder);
    }
}

Note that you need to update the ShopUI as well, but first, let’s see how to finish the main AzureEconomy class.

Getting the Player’s Inventory

The Inventory and VirtualCurrency classes work basically the same as GetCatalogItems. Add the following code to AzureEconomy.cs:
...
   public void GetInventory()
    {
        var playerId = this.gameObject.GetComponent<AzureSettings>().playerID;
        HttpWebRequest request = (HttpWebRequest)WebRequest.Create("https://shared-apim.azure-api.net/economy/Inventory?PlayerId=" + playerId);
        request.Method = "GET";
        request.Headers.Add("Authorization", "Bearer " + GetComponent<AzureSettings>().token);
        HttpWebResponse response = (HttpWebResponse)request.GetResponse();
        StreamReader reader = new StreamReader(response.GetResponseStream());
        string result = reader.ReadToEnd();
        Debug.Log(result);
        var inventory = JsonConvert.DeserializeObject<PlayerInventory>(result);
        ShopUI.UpdateTextArea(inventory.Items);
        GetVirtualCurrency();
    }
    private void GetVirtualCurrency()
    {
        var playerId = this.gameObject.GetComponent<AzureSettings>().playerID;
        HttpWebRequest request = (HttpWebRequest)WebRequest.Create("https://shared-apim.azure-api.net/economy/VirtualCurrency?PlayerId=" + playerId);
        request.Method = "GET";
        request.Headers.Add("Authorization", "Bearer " + GetComponent<AzureSettings>().token);
        HttpWebResponse response = (HttpWebResponse)request.GetResponse();
        StreamReader reader = new StreamReader(response.GetResponseStream());
        string result = reader.ReadToEnd();
        Debug.Log(result);
        playerVirtualCurrency = JsonConvert.DeserializeObject<PlayerVirtualCurrency>(result);
        ShopUI.UpdateVirtualCurrency(playerVirtualCurrency.VirtualCurrencies[0].VC_Name, playerVirtualCurrency.VirtualCurrencies[0].VC_Amount);
    }
...

Purchasing Items

If a player buys a new item, we’ll read it from the catalog and post it to the player’s inventory. To simplify the function, we’ll check if the player already owns the item, and if they do not, we’ll decrease the MC virtual currency by the price of the item. This example assumes that MC is first in the list of currencies to keep it simple.

Now implement the POST request, which changes the amount of virtual currency the player has, by adding the following code to AzureEconomy.cs:
...
    public void SetVirtualCurrency(string virtualCurrencyName, int virtualCurrencyAmount)
    {
        var playerId = this.gameObject.GetComponent<AzureSettings>().playerID;
        HttpWebRequest request = (HttpWebRequest)WebRequest.Create("https://shared-apim.azure-api.net/economy/VirtualCurrency");
        request.Method = "POST";
        request.Headers.Add("Authorization", "Bearer " + GetComponent<AzureSettings>().token);
        VirtualCurrency virtualCurrency = new VirtualCurrency()
        {
            VC_Name = virtualCurrencyName,
            VC_Amount = virtualCurrencyAmount
        };
        PlayerVirtualCurrency playerVirtualCurrency = new PlayerVirtualCurrency()
        {
            PlayerId = playerId,
            VirtualCurrencies = new List<VirtualCurrency>() { virtualCurrency }
        };
        var json = JsonConvert.SerializeObject(playerVirtualCurrency);
        byte[] data = Encoding.ASCII.GetBytes(json);
        request.ContentType = "application/json";
        request.ContentLength = data.Length;
        Stream requestStream = request.GetRequestStream();
        requestStream.Write(data, 0, data.Length);
        requestStream.Close();
        HttpWebResponse response = (HttpWebResponse)request.GetResponse();
        StreamReader reader = new StreamReader(response.GetResponseStream());
        string result = reader.ReadToEnd();
        GetVirtualCurrency();
        Debug.Log(result);
    }
Next, implement the PurchaseItem method in the AzureEconomy.cs file using the following code:
...
    public void PurchaseItem(string itemToPurchase)
    {
        int itemNumber = int.Parse(itemToPurchase) - 1;
        var playerId = this.gameObject.GetComponent<AzureSettings>().playerID;
        HttpWebRequest request = (HttpWebRequest)WebRequest.Create("https://shared-apim.azure-api.net/economy/Inventory");
        request.Method = "POST";
        request.Headers.Add("Authorization", "Bearer " + GetComponent<AzureSettings>().token);
        PlayerInventory playerInventory = new PlayerInventory()
        {
            PlayerId = playerId,
            Items = new List<Item>() { catalog.Items[itemNumber] }
        };
        var json = JsonConvert.SerializeObject(playerInventory);
        byte[] data = Encoding.ASCII.GetBytes(json);
        request.ContentType = "application/json";
        request.ContentLength = data.Length;
        Stream requestStream = request.GetRequestStream();
        requestStream.Write(data, 0, data.Length);
        requestStream.Close();
        HttpWebResponse response = (HttpWebResponse)request.GetResponse();
        StreamReader reader = new StreamReader(response.GetResponseStream());
        string result = reader.ReadToEnd();
        Debug.Log(result);
        if (result != "Item is already existing in the inventory.")
        {
            SetVirtualCurrency("MC", playerVirtualCurrency.VirtualCurrencies[0].VC_Amount - catalog.Items[itemNumber].Price);
        }
    }
...

Buying from the Store

You can simply reuse the UnityIAPManager that you implemented in the PlayFab section to enable in-app purchasing. Finalize the AzureEconomy class using the following method:
...
    public void BuyFromStore(string itemToPurchase)
    {
        int itemNumber = int.Parse(itemToPurchase) - 1;
        unityIAPManager.PurchaseItem(catalog.Items[itemNumber].ItemId);
    }
}

Economy GUI

Similar to what you did in PlayFab, you will create a simple user interface that allows you to use the functions you implemented in the previous part. First extend the control panel with the following code:
public class ControlPanel : MonoBehaviour
{
    public const int AZURE_ECONOMY = 8;
    void OnGUI()
    {
        if (selection == AZURE_ECONOMY)
            GUILayout.Window(0, new Rect(0, 0, 300, 0), AzureEconomyWindow, "Azure Economy");
    }
    void OptionsWindow(int windowID)
    {
        if (GUILayout.Button("Azure Economy"))
        {
            selection = AZURE_ECONOMY;
        }
    }
    void AzureEconomyWindow(int windowID)
    {
        if (economyTextAreaTitle != "")
            GUILayout.Label(economyTextAreaTitle);
        textArea = ShopUI.GetTextArea();
        textArea = GUILayout.TextArea(textArea, 200);
        virtualCurrencyLabel = ShopUI.GetVirtualCurrencyLabel();
        if (virtualCurrencyLabel != "")
            GUILayout.Label(virtualCurrencyLabel);
        GUILayout.Space(10);
        if (GUILayout.Button("Get Catalog Items"))
        {
            azure.GetComponent<AzureEconomy>().GetCatalogItems();
            economyTextAreaTitle = "Catalog Items";
        }
        if (GUILayout.Button("Get Player Inventory + Virtual Currency"))
        {
            azure.GetComponent<AzureEconomy>().GetInventory();
            economyTextAreaTitle = "Player Inventory";
        }
        GUILayout.Space(10);
        if (GUILayout.Button("Grant Virtual Currency"))
        {
            azure.GetComponent<AzureEconomy>().SetVirtualCurrency("MC", 100);
        }
        GUILayout.Space(10);
        if (GUILayout.Button("Purchase this item (number):"))
        {
            azure.GetComponent<AzureEconomy>().PurchaseItem(itemToPurchase);
        }
        itemToPurchase = GUILayout.TextField(itemToPurchase, 100);
        GUILayout.Space(10);
        if (GUILayout.Button("Buy from store"))
        {
            azure.GetComponent<AzureEconomy>().BuyFromStore(itemToPurchase);
        }
    }
}
The ShopUI file needs some extensions. This static class must update the text area on the GUI. In the case of Azure, you can use the Item class instead of CatalogItem:
public static class ShopUI
{
    public static void UpdateTextArea(List<Item> items)
    {
        string ta = "";
        int number = 0;
        foreach (var item in items)
        {
            number++;
            ta += number + " | " + item.DisplayName + ".....";
            ta += item.Price + " " + item.Currency;
            ta += " ";
        }
        for (int i = items.Count; i < 5; i++)
        {
            ta += " ";
        }
        textArea = ta;
    }
}

With that, you have implemented the same calls from Unity as from the command line with cURL. Add the AzureEconomy script as a new component to the Azure game object. Then test the client with the API Management Service URL. If there is an issue, you can use the local URL to see the error messages in the backend.

Summary

In this chapter, you learned how to implement a game economy using PlayFab and a custom Azure solution. PlayFab provides rich options and opportunities to integrate third-party stores. It is flexible and easy to use. If you decide to create a custom solution, prepare to write a considerable amount of custom code. But you have more flexibility and even more options than with PlayFab. NoSQL database is better than a relational database for implementing this feature.

Review Questions

  1. 1.

    What are the benefits of implementing an economy into your game?

     
  2. 2.

    What are the basic components of virtual economies?

     
  3. 3.

    What are the basic functions you should implement in a game economy?

     
  4. 4.

    What are the typical properties of items in a virtual economy?

     
  5. 5.

    What is in-app purchasing (IAP)?

     
  6. 6.

    What kind of database do you use to implement an economy?

     
  7. 7.

    What are documents and collections in MongoDB?

     
  8. 8.

    How do you add new items to a catalog or an inventory?

     
  9. 9.

    How do you enforce player authentication before using the API?

     
  10. 10.

    When would you use PlayFab and when would you implement a custom economy solution?

     
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset