Microservices - Code
Create and deploy server-authoritative C# [CLCP-Microservices-03]
The purpose of this feature is to allow game makers to create and deploy server-authoritative C# functionality within their games.
This feature eliminates the need to build, run, and scale a game server. Game makers can create server-authoritative logic in C# inside the Unity editor.
Related Guides & Video
A common use case for the feature is covered in the guides. See Adding Microservices for more info.
Example
Here a new c# server-side Microservice is created and exposes a method to be called by any c# client-side scope.
The source-code for both server-side and client-side sits local in your Unity project for efficiency and ease-of-use.
Creating a Microservice
namespace Beamable.Examples.Features.Microservices
{
[Microservice("MyMicroservice")]
public class MyMicroservice : CustomMicroservice
{
[ClientCallable]
public int GetLootboxReward()
{
Console.WriteLine("GetLootboxReward()");
// if (IsAllowed), then reward 100 gold
return 100;
}
}
}
Calling a Microservice
using Beamable;
using Beamable.Server.Clients; // don't forget this namespace
namespace Beamable.Examples.Features.Microservices
{
public class MyMicroserviceExample : MonoBehaviour
{
protected async void Awake ()
{
var ctx = BeamContext.Default;
await ctx.OnReady;
var result = await ctx.Microservices().MyMicroservice().GetLootboxReward();
// Result:100
Debug.Log ($"Result:{result}");
}
}
}
Examples
Example: Client-Authoritative
Imagine an in-game treasure chest ("loot box") containing a random prize for the player. The player is offered the loot box as a reward for progress in the game, opens the box, and receives the prize.
In a simple game with little or no back-end technology, the prize is offered, validated, and rewarded on the client-side. This is straight-forward to code, but is relatively insecure to any malicious hackers in the gaming community.
Example: Server-Authoritative
Now imagine some portion of the loot box code is moved to the server.
Now the the prize is offered, validated, and rewarded on the server-side. This is less straight-forward to code, but is relatively secure against any malicious hackers in the gaming community.
The benefits of a server-authoritative architecture are great. There are a few major ways to set it up.
A development team could write a custom back-end server setup. This is considerable time and effort, even for experienced teams. The team could choose an off-the-shelf solution. Beamable's Microservice feature provides power and ease of use.
Example: Beamable Microservice
Let's see how Beamable can help.
Imagine some portion of loot box code is moved to the server as a Beamable Microservice.
The prize is offered, validated, and rewarded on the server-side as a Microservice. The Microservice code sits in your Unity project along-side your client side code and is written in the familiar and powerful C# language. It is straight-forward to code and it is secure against any malicious hackers in the gaming community.
Handling Errors
When a call to a Beamable service fails, it will throw a RequesterException
. The exception can be caught using standard C# try/catch practices. In the example below, the call to SetCurrency
will fail, and the catch
block will capture a RequesterException
that exposes details about the failure.
using Beamable.Common.Api;
using Beamable.Server;
using System.Threading.Tasks;
namespace Beamable.Microservices {
[Microservice("ErrorDemo")]
public class ErrorDemo: Microservice {
[ClientCallable]
public async Task < string > ServerCall() {
try {
// do something that will trigger a failure, like trying to reference a piece of content that doesn't exist.
await Services.Inventory.SetCurrency("does-not-exist-so-it-will-trigger-a-400", 0);
} catch (RequesterException ex) {
return $ "{ex.Method.ToReadableString()} / {ex.Payload} / {ex.Prefix} / {ex.Status} / {ex.Uri} / {ex.RequestError.error} / {ex.RequestError.message} / {ex.RequestError.service}";
}
return "okay";
}
}
}
Share Exception code with Unity
The
RequesterException
is the same type of exception that is thrown on the Unity client SDK in the event of a Beamable networking error. You can catch the same type and use the same error handling logic.
Advanced
This section contains any advanced configuration options and workflows.
Beamable & Docker
Beamable Microservices uses the industry standard Docker technology. Docker simplifies and accelerates your workflow, while giving developers the freedom to innovate with their choice of tools, application stacks, and deployment environments for each project. See Docker's documentation for more info.
Beamable handles all coordination and configuration of Docker including the beamableinc/beamservice
base image hosted on Docker's DockerHub.
Designing Microservices
Here are some optional, suggested Beamable data types to use along with Microservices. The Microservice can take Beamable content as input and pass Beamable player state as output. Of course, game makers may choose whatever pattern helps the specific needs of their games.
Here is a partial list of examples.
Input | Output | Example |
---|---|---|
Currency | Inventory | An RPG game client sends a request for the hero to purchase new armor. The server validates the transaction, deducts player currency and updates the player inventory |
ContentObject | Stat | A Boss-Battle game client sends the Boss ContentObject and the Hero's Sword ContentObject. The server updates the BossHealth Stat according to the damage inflicted |
List of Player Moves | Winning Player | A checkers game can validate and declare winner. The client sends a complete list of all the players' moves and the server processes the result |
Stat | Leaderboard Score | A platformer game client notifies that the player has completed level 3. The server validates that the user has previously completed level 1 and 2 (as required) and updates the player's high-score |
Organizing Microservices Code
As your project grows, you will have a larger amount of C#MS code and, as such, you might want to split this code into multiple C#MSs in order to organize it.
However, doing so has implications you should take into consideration when deciding things for your project. In many cases, it is possible to simplify C#MS and reduce the quantity without sacrificing functionality.
-
Having multiple deployed C#MSs incurs costs as each of them is a separate instance.
-
While you can call C#MSs from other C#MSs to reuse code, this also turns your system into a distributed system. This adds complexity.
-
Having multiple C#MSs also increases C#MS build/deploy times which can be a concern when developers want to iterate quickly.
So, what can you do to share and organize code without incurring costs or adding complexity to your C#MS?
You can leverage C# partial classes, which we support when detecting ClientCallables
. The following snippets will define a single C#MS called MyPartialMs
with two ClientCallables
: DoSomethingRelatedToA()
and DoSomethingRelatedToB()
.
// MyPartialMs.A.cs
[Microservice("MyPartialMs")]
public partial class MyPartialMs : Microservice {
[ClientCallable]
public void DoSomethingRelatedToA(){
}
}
// MyPartialMs.B.cs
public partial class MyPartialMs {
[ClientCallable]
public void DoSomethingRelatedToB(){
}
}
The end result here is that you have two ClientCallables
in the same MyPartialMs
but in separate files. The same thing works for utility functions and other similar things.
What should I use different C#MSs for?
Basically, the decision for this has to do with expected traffic hitting that feature/service and how it's resources are expected to scale. When reasoning about this, here are a couple of things to keep in mind:
-
Requests Per Second that a feature can be expected to have. You can calculate this in a "back-of-the-napkin" way by estimating the number of non-locally cached requests the features make per-player every second and then multiply that by the expected number of concurrent players utilizing the feature.
-
You should also think about how spikes in feature activity happen. A feature that spikes really fast to really high numbers might be a good canditate for being a stand-alone C#MS as its spikes may cause longer response times for features shared in it as the servers scale up. If a feature is expected to have a slow increase in requests-per-second, it should be able to scale without affecting other features even if they are in the same C#MS.
To keep things a bit simpler, here are general rules of thumb:
- Group as many features with low request-per-second profiles as you can in a single service. This simplifies your development process and make auto-scaling of the service more efficient. Especially if they don't spike quickly.
- Split out features that are expected to have heavy request-per-second profiles and want a lot of server resources (CPU/Memory) into their own C#MSs. Especially, if their usage spikes in a short amount of time.
- Keep payloads as small as you can. While we support larger payloads, it'll end up causing the C#MS to have to scale sooner. Try to keep these well below 10kb.
- Code organization should not factor into this decision if you want the maximum bang for your buck while using Beamable C# Microservices.
If the above is true and you still wish to share code between two different microservices, architect your code and functions so that the parts that are worth sharing can be pulled into a separate AssemblyDefinition which both services reference. Keep in mind that you should only do this if the complexity is worth the code reuse --- over-using AssemblyDefinitions can increase overall project complexity for not that much gain.
All-in-all, this is a very game-specific decision. As such, our goal is to provide guidelines and tools to help you make that decision. Currently, you can see CPU/Memory utilization metrics via the Beamable Dev Portal's C#MS section and, in the future, we may track more specific metrics to allow you to make better decisions.
Designing Microservice Methods
When designing each microservice method, a key question is 'Who is allowed access?'. Game makers control this access with Microservice method attributes.
Microservice Method Attributes
Name | Detail | Accessible Via? |
---|---|---|
[AdminOnlyCallable] | Method callable by any admin user account | Microservices OpenAPI |
[ClientCallable] | Method callable by any user account | Microservices OpenAPI and Unity C# Client |
Debugging Microservices
Development of Beamable Microservices within the Unity client is straight-forward and transparent. It is the ideal environment for development including debugging tools.
See Microservices » Debugging for more info.
Deploying Microservices
During development, the Microservice is run locally within a developer-specific sandbox, safely isolated from the production environment.
Using the Beamable Microservices Manager Window in the Unity Editor, the game maker can promote/deploy the Microservice to production. Choose the destination Realm and press the 'Publish' Button.
As needed, the Microservice can be rolled-back there as well.
Unity Publish Process
When Microservices and Microstorages are published from Unity, each services goes through a set of considerations and procedures. After each service has been finalized, a Service Manifest is written to the Beamable Realm that describes the desired deployment state. If the Service Manifest is not written, then no services will be updated.
Microservices may go through 3 phases before the Service Manifest is written.
- Build - produces a Docker image for the Microservice
- Verification - validates the Docker image can start and connect to Beamable. The Microservice will start during the verification step, but it will never run custom startup hooks.
- Upload - save the Docker image to Beamable's Docker registry
If a Microservice is disabled, and it has never been published before, then it will be skipped.
If a Microservice is disabled, and it has been published before, then the service won't be built, validated, or uploaded, but it will be included in the final Service Manifest.
If a Microservice is archived, then it will not be built, validated, or uploaded, but it will be included in the Service Manifest as archived.
If a Microservice is enabled, then it will be built, validated, and uploaded.
Microstorages only need to be built, validated, or uploaded when publishing. They only exist as entries in the Service Manifest. Microstorages don't execute custom code, so there is no need for custom images.
Deployment Size Profile
Each Microservice is given a size profile
upon deployment.
Name | Detail | Benefit | Default |
---|---|---|---|
Small | Deployment is not scalable Note: This is ideal for development | More economic | |
Medium | Deployment is partially scalable | ||
Large | Deployment is scalable (e.g., Geographic scalability & Load scalability) | More flexibility | ✔️ |
Managing Deployed Microservices Via Portal
With the Portal, game makers can view and manage the deployed Microservices.
Name | Details |
---|---|
1. Status | Shows the current and deploying Microservices |
2. Metrics | Shows the metrics |
3. Logs | Shows the logs |
4. Docs | Shows the OpenAPI docs. See Debugging (Via OpenAPI) for more info |
5. Deployments | Shows the historic deployments of Microservices |
6. New Deployment | Allows game makers to make a new deployment |
7. View | Allows game makers to view new deployment |
Making Beamable Calls From A Microservice
Each custom Microservice created extends the Beamable Microservice
class.
This gives the custom Microservice access to key member variables:
Context
- Refers to the current request context. It contains info including what PlayerId is calling and which path is runRequester
- It can be used to make direct (and admin privileged) requests to the rest of the Beamable PlatformServices
- The powerful Microservice entry-point to Beamable'sStatsService
,InventoryService
, and more
Example
Below are two versions of the same method call with varied implementations. Notice that #2 handles more functionality on the Microservice-side and is thus more secure.
#1. Method Without Beamable Services
Here the eligibility of reward is evaluated and the amount is calculated. This assumes the client side will handle the rewarding of the currency.
[ClientCallable]
public int GetLootboxReward()
{
// if (IsAllowed), then reward 100 gold
return 100;
}
#2. Method With Beamable Services
Here the eligibility of reward is evaluated, the amount is calculated, and the currency is rewarded.
[ClientCallable]
public async Task<bool> GetLootboxReward()
{
// if (IsAllowed), then reward 100 gold
await Services.Inventory.AddCurrencies(
new Dictionary<string, long> {{"currencies.gold", 100}});
// Optional: Return success, if needed
return true;
}
Making External HTTP Calls
This example code will make an external HTTP call from the Beamable Microservice.
[ClientCallable]
public async Task<string> GetCommitDescription()
{
var service = Provider.GetService<IHttpRequester>();
var commit = await service.ManualRequest(Method.GET,"https://whatthecommit.com/index.txt", parser: s => s);
BeamableLogger.Log(commit);
return commit;
}
Custom HttpClient Implementation
You can also implement your own services that will handle calling other sites.
Learn more: https://makolyte.com/csharp-how-to-make-concurrent-requests-with-httpclient/
Custom Build Actions
As of Beamable 1.12.0, it is possible to run custom code anytime a Microservice is built. The primary use case is to copy custom files into the Microservice's Docker container. To run custom code on Microservice build, you will need to
- Create an assembly definition,
- Inside the new assembly, create a new implementation of
IMicroserviceBuildHook<T>
, - Register the implementation with Beamable's Dependency System.
In the following example, there is a Microservice called HookTest
. The build hook will copy a file from Assets/test.txt
into the container.
assemblyDefinition.asmdef
{
"name": "Custom.Editor.BuildHooks",
"references": ["Beamable.Microservice.HookTest",
"Unity.Beamable.Editor.Server",
"Unity.Beamable.Server.Runtime",
"Unity.Beamable.Server.Runtime.Shared",
"Unity.Beamable.Editor",
"Unity.Beamable.Runtime.Common"
],
"autoReferenced":false,
"includePlatforms":[
"Editor"
]
}
Hook.Cs
using Beamable.Microservices;
using Beamable.Server.Editor;
using UnityEngine;
public class BuildHook : IMicroserviceBuildHook<HookTest>
{
public void Execute(IMicroserviceBuildContext ctx)
{
Debug.Log("EXECUTING CUSTOM BUILD HOOK!");
ctx.AddFile("Assets/test.txt", "data/test.txt");
}
}
BeamContextSystem]
public class RegisterHooks
{
[RegisterBeamableDependencies(-1, RegistrationOrigin.EDITOR)]
public static void Register(IDependencyBuilder builder)
{
builder.AddSingleton<IMicroserviceBuildHook<HookTest>, BuildHook>();
}
}
Now from within the HookTest
Microservice, the "test.txt" file can be read by running File.ReadAllText("data/test.txt)
Microservice Serialization
Unity's built-in features use Unity's serialization.
However, within a Beamable's Microservice, game makers must rely instead on Beamable's custom Serialization. This serialization is strict and has limitations.
Types
Supported Types | Unsupported Types |
---|---|
• Microservice Serialization Supported Types | • All other types |
Request Details
When a [ClientCallable]
method executes, there is an available class field, Context
, that contains information about the HTTP request responsible for initiating the [ClientCallable]
.
Handling Request Timeouts
It is possible that a request may timeout while it is executing. Long running loops are one example. In the code below, the while
loop never completes, so the method never returns a value, so the request will timeout.
[ClientCallable]
public int Example();
{
while (true) { }
return 1;
}
However, despite the request timing out and the client receiving an HTTP 504 error, the Microservice will still be processing the loop. This is a major performance problem, because the CPU will never yield control to other request executions. Essentially, anytime a request is caught in a non terminating loop, the entire Microservice performance profile suffers dramatically. It is a best practice to ensure loops will always terminate. In Beamable 1.9, the Context
variable has a property, Context.IsCancelled
, that returns true
if the request has timed out. There is also a Context.ThrowIfCancelled()
method that will throw a task cancellation exception. It is a best practice to always include the ThrowIfCancelled()
method in long running loops, as exemplified below.
[ClientCallable]
public int Example();
{
while (true)
{
// if the request has not been cancelled, this is a no-op.
Context.ThrowIfCancelled();
}
return 1;
}
Access Caller Information
It is common to require a player id for several operations that take place in a Microservice. The Player Id can be acquired through the Context
property as the UserId
. The given Player Id will be the Player id for the player that initiated the request. If no Authorization
header was given to the HTTP call, such as calling a [Callable]
method, then the UserId
may be 0, implying there is no player associated with the call.
[ClientCallable]
public void RequestDetails()
{
var playerId = Context.UserId;
}
Account Id
Sometimes, Beamable features require an Account Id instead of a Player Id. If you need to access the Account Id of the caller, you use the
GetAccountId
function available from the Auth Service.var accountId = await Services.Auth.GetAccountId();
Access Unity Version via Headers
In Beamable 1.3+, the HTTP headers can be found via the Context.Headers
property. The Headers
is a Dictionary<string, string>
, where the keys represent HTTP header names, and the values represent HTTP header values.
The Unity Beamable SDK sends a few special headers that describe the game's environment, including the game's version, Unity's version, Beamable's version, and Unity's runtime target.
Header | Code | Description | Examples |
---|---|---|---|
X-KS-BEAM-SDK-VERSION | csharp var headerPresent = Context.Headers.TryGetBeamableSdkVersion(out var version); | The Beamable SDK version that the request originated from. | 1.3.0, 1.2.10 |
X-KS-USER-AGENT-VERSION | csharp var headerPresent = Context.Headers.TryGetClientEngineVersion(out var version); | The version of the engine that sent the request. | 2019.3.1, 2021.3.1 |
X-KS-USER-AGENT | csharp var headerPresent = Context.Headers.TryGetClientType(out var version); | The name of the engine that sent the request. | Unity, UnityEditor |
X-KS-GAME-VERSION | csharp var headerPresent = Context.Headers.TryGetClientGameVersion(out var version); | The version of the game that sent the request. | 1.0, 0.1 |
These headers are sent automatically from Beamable 1.3+. If a request is sent from elsewhere, such as Portal, or a custom script, the headers may not be present.
UseLegacySerialization
A breaking change is introduced in Beamable SDK v0.11.0. If you began your project with an earlier version of the SDK, see Beamable Releases Unity SDK Version 0.11.0 for more info. Otherwise, simply disregard this issue.
Updated about 1 month ago