Microservices - Content

Create and deploy server-authoritative C# [CLCP-Microservices-05]

Beamable Microservices supports references to various built-in and custom data types. Some situations require additional consideration.

🚧

Warning!

Here are hints to help explain some of the trickier concepts:

  • Microservices does not support passing/returning any child of UnityEngine.Object. This is by design
  • Microservices does not support passing/returning any child of Beamable.Common.Content.ContentObject. This is a serialization limitation which may be resolved in the future. See below for a workaround

Easy Setup :green-heart:
Passing/returning no values requires no particular setup.

namespace Beamable.Server
{
   [Microservice("MyCustomMicroservice")]
   public class MyCustomMicroservice : Microservice
   {
      [ClientCallable]
      public async void MyMethod01()
      {

      }
   }
}

Intermediate Setup :large-blue-circle:
Passing/returning built-in C# primitive data types requires simply adding the appropriate "using" statements.

using System.Threading.Tasks;

namespace Beamable.Server
{
   [Microservice("MyCustomMicroservice")]
   public class MyCustomMicroservice : Microservice
   {
      [ClientCallable]
      public async Task<int> MyMethod01(string myMessage)
      {
         return 10;
      }
   }
}

Advanced Setup :black-circle:

Passing/returning custom C# primitive data types requires both...

using System.Threading.Tasks;
using Examples.MyCustomContentMicroserviceExample.Shared.MyCustomContent;

namespace Beamable.Server
{
   [Microservice("MyCustomMicroservice")]
   public class MyCustomMicroservice : Microservice
   {
      [ClientCallable]
      public async Task<MyCustomData> MyMethod01()
      {
      }
   }
}

Why Asmdef?

During the maturity of the Unity product, the concept of Asmdef was added. Asmdefs continue to be an optional feature for some Unity projects. In this situation it is required.

See Learning Fundamentals for more info on Asmdefs.

Unity C# Code execution is designed with key principles

  • A typical Unity project may have none, some, or all of its C# code sitting within Asmdefs
  • C# code not inside an Asmdef can access any code
  • C# code inside an Asmdef can access only code inside an Asmdef. The rules are set by configuring each Asmdef
  • An Asmdef cannot make any circular references

Beamable C# Microservices is designed with key principles

  • Microservices run in a unique server environment instead of the typical Unity client environment
  • Microservices live in an Asmdef so limit which code a Microservice can access. This is safer and more appropriate scoping

Example

Here are examples which cover common programming needs.

📘

Beamable SDK Examples

The following example code is available for download at GitHub.com/Beamable_Microservices_Examples

1. Asmdef Setup

The requirement here is for each Asmdef to reference only the other Asmdef(s) it must reference. Depending on the complexity of your project, you may have more or less total Asmdefs than are shown here.

🚧

Do Not "Use GUIDs"

Unity's Assembly Definition References have an optional flag for "Use GUIDs". This is not currently supported by Beamable Microservices, so it is advised to leave this box unchecked.

The Project's Asmdefs

The Project's Asmdef References

The interrelation between some of the Asmdefs is shown here. This is only part of the story. It is strongly recommended to download this project and view each of these 4 Asmdefs within the Unity inspector for more info.

FromTo: AutoGenTo: RuntimeTo: ServerTo: Shared
AutoGen✔️
Runtime✔️✔️
Server✔️
Shared

2. Code Setup

Data Design

Let's assume a C# Microservice method is designed to return a MyCustomDataContent data type. However, due to the limitations discussed above, that is not currently possible.

The workaround is to populate MyCustomDataContent with a MyCustomData data type which holds all the data of interest. The Microservice can properly fetch the content yet return only data within. The proper data design is shown here.

using System;
using Beamable.Common.Content;

namespace Examples.MyCustomContentMicroserviceExample.Shared.MyCustomContent
{
    [ContentType("my_custom_content")]
    
    public class MyCustomContent : ContentObject
    {
        public MyCustomData MyCustomData = new MyCustomData();
    }
    
    [Serializable]
    public class MyCustomData : System.Object
    {
        public int Attack;
        public int Damage;
    }
}

Calling MyCustomContentMicroservice

Here the example calls to MyCustomContentMicroservice which returns the MyCustomData.

private async void SetupBeamable()
{
    var beamableAPI = await Beamable.API.Instance;
    
    Debug.Log($"beamableAPI.User.id = {beamableAPI.User.id}");
    
    _myCustomContentMicroserviceClient = new MyCustomContentMicroserviceClient();
    
    // #1 - Call Microservice
    MyCustomData myCustomData = 
        await _myCustomContentMicroserviceClient.LoadCustomData();
        
    // Hardcoded guess. Feel free to update via Content Manager
    bool isSuccess = myCustomData.Attack == 22; 
    
    // #2 - Result 
    Debug.Log ($"LoadCustomData() myCustomData.Attack = {myCustomData.Attack}, Success={isSuccess}");
}

Implementing MyCustomContentMicroservice

Here the Content service returns the MyCustomContent and the Microservice returns just the MyCustomData within.

using System.Threading.Tasks;
using Examples.MyCustomContentMicroserviceExample.Shared.MyCustomContent;

namespace Beamable.Server
{
   [Microservice("MyCustomContentMicroservice")]
   public class MyCustomContentMicroservice : Microservice
   {
      [ClientCallable]
      public async Task<MyCustomData> LoadCustomData()
      {
         MyCustomData myCustomData = null;

         // Check All Content
         var clientManifest = await Services.Content.GetManifest();
         foreach (var entry in clientManifest.entries)
         {
            // Find Matching Type
            if (entry.contentId.Contains("my_custom_content"))
            {
               // Load MyCustomContent
               MyCustomContent myCustomContent =
                  await Services.Content.GetContent<MyCustomContent>(entry.ToContentRef());

               myCustomData = myCustomContent.MyCustomData;
            }
            
         }
         
         // Return data
         return myCustomData;
      }
   }
}

Content examples

Here are examples which cover common programming needs.

📘

Beamable SDK Examples

The following example code is available for download at GitHub.com/Beamable_Microservices_Examples

Calling MyCurrencyMicroservice

The key methods here return a bool to determine the success of the operation. As an alternative game makers can choose to return additional information such as the affected currencyId or the new currency amount:

private async void SetupBeamable()
{
    var beamContext = BeamContext.Default;
    await beamContext.OnReady;

    Debug.Log($"beamContext.PlayerId = {beamContext.PlayerId}");
    
    _myCurrencyMicroserviceClient = new MyCurrencyMicroserviceClient();

    // #1 - Call Microservice
    bool isSuccess1 = await _myCurrencyMicroserviceClient.AddToActiveCurrency();
    Debug.Log ($"AddToActiveCurrency() isSuccess = {isSuccess1}");
    
    // #2 - Call Microservice
    bool isSuccess2 = await _myCurrencyMicroserviceClient.RemoveFromActiveCurrency();
    Debug.Log ($"RemoveFromActiveCurrency() isSuccess = {isSuccess2}");
    
}

Implementing MyCurrencyMicroservice

Here 1 is added to the current currency amount.

❗️

Security

  • Notice that AddToActiveCurrency() accepts no arguments. This is more limiting but also more secure
  • Game makers may pass arguments to Microservice methods, but be wary of the security implications of an alternative such as AddToActiveCurrency(contentId, amountToAdd) which could be called repeated by malicious clients
[ClientCallable]
public async Task<bool> AddToActiveCurrency()
{
   // Add Value
   long amountDelta = 1;
  
   // Change the amount of currency
   bool isSuccess = false;
   string currencyId = await GetActiveCurrencyId();
   long amountOld = await GetActiveCurrencyValue();

   if (!string.IsNullOrEmpty(currencyId))
   {
      long amountNew = amountOld + amountDelta;
      await Services.Inventory.SetCurrency(currencyId, amountNew);

      // Validate
      long amountConfirmed = await Services.Inventory.GetCurrency(currencyId);
      isSuccess = amountConfirmed == amountNew;
   }

   return isSuccess;
}

Content Preloading

As of Beamable 1.8.0, every microservice instance downloads the entire content manifest before it starts accepting HTTP traffic. This feature can be disabled by disabling the EnableEagerContentLoading flag on the MicroserviceAttribute. This code snippet would create a microservice that would not pre-download content. Be aware that this means the first time the Microservice needs to fetch content can result in long request times.

[Microservice("example", EnableEagerContentLoading = false)]
public class Example : Microservice
{
}