Unity

Documentation Objective

This section describes how to implement an Aether simulation client using Unity.

Getting started

Unity can be downloaded here. Version 2019.4 is supported.

General architechture

To build a Unity client for the simulation you should be familiar with the general aether-muxer-client

Aether SDK provides an option to install the Aether Plugin for Unity, which enables using Unity for implementing a simulation client. This guide describes setting up the project step by step. In addition, a complete project which uses the Aether Plugin for Unity is available in the Physics demo which can be selected during installation. Once installed, the UnityClient project can be found in %USERPROFILE%/Hadean/Unity/aetherplugin.tgz.

The Aether Plugin for Unity supports Unity 2019.4 LTS. Any Unity version supporting .NET standard 2.0 should be compatible but has not been tested.

Prerequisites

  • Aether SDK with the Aether Plugin for Unity installed
  • Unity 2019.4 LTS
  • Basic understanding of the client-server architecture used in Aether simulations: aether-muxer-client

Adding the Aether Unity Plugin to a unity project.

  1. Open or create a unity project.
  2. Add the Aether plugin to the project using Window -> Package Manager menu. Select the + menu -> Add package from tarball and select the path where sdk installed the plugin, by default: %USERPROFILE%/Hadean/Unity/aetherplugin.tgz hadean screen snippet
  3. Once added the plugin will be visible in the package list and in the package menu, as in the screenshot below (the version of the plugin will match the version of the SDK). hadean screen snippet

Using the client API from Unity

The Unity Client API is very similar to the C++ API described in the repclient documentation, with small changes to make it integrate better with C#.

Here's an overview of an example class used to connect to the Aether simulation.

using AetherPlugin;
public class Simulation : MonoBehaviour
{
private Repclient _repclient;
// Awake is called before the first frame update - we initialize here
void Awake()
{
// 1. create a connection
_repclient = Repclient.Connection("aether-sdk.mshome.net", "8881");
// 2. authenticate with a player id and a token
_repclient.AuthenticatePlayerIdWithToken(1, "00000000000000000000000000000000");
}
// Update is called once per frame
void Update()
{
// 3. read the received data in the update loop
while (_repclient.IsConnected()) {
byte[] repData = _repclient.ReceiveMessage();
if (repData == null || repData.Length == 0) return;
// 4. decode the data according to the used protocol (see "Using FlatBuffers for marshalling the data")
var message = DecodeMessage(repData);
// use the message
foreach (Entity e in message.Entities) {
// ...
}
}
}
// send a client event to the simulation
void SendEvent()
{
// 5. encode the event data according to the used protocol (see "Using FlatBuffers for marshalling the data")
byte[] eventData = EncodeEvent();
// 6. send the event
_repclient.SendEvent(eventData, false);
}
}

Defining a protocol using FlatBuffers

The protocol](/Guide/Technical-documentation/Protocol/) documentation contains an overview of different approaches to defining a client-server communication protocol. For a project with a Unity client, a protocol defined using [FlatBuffers is a good starting point, as it makes it easy to define the data format in one place without having to worry about keeping the C++ and C# definitions synchronized (besides re-running the code generator). It's worth noting that Aether and the Unity plugin are themselves independent of the used protocol, so FlatBuffers can be replaced with a different approach if needed.

Here's a minimal example of a FlatBuffers protocol file:

// define a structure used for storing coordinates
struct Vec3 {
x: float;
y: float;
z: float;
}
// define a very simple entity
struct Entity {
position: Vec3;
}
// define each message coming from the simulation to contain the current state of all entities
table SimulationMessage {
entities: [Entity];
}
// define a very simple event to be sent to simulation
table ClientEvent {
position: Vec3;
}

For this guide we'll assume the protocol is saved in a file named protocol.fbs.

Adding FlatBuffers to a Unity project

Once the protocol is defined, the Unity project needs to be configured to use the FlatBuffers protocol. This involves several steps:

  1. Get FlatBuffers.dll and flatc
  2. Add FlatBuffers.dll to the Unity project
  3. Generate the the C# source code for protocol files
  4. Add the generated files to the project
  5. Automate the process (optional)
1. Get FlatBuffers.dll and flatc

Flatc is the FlatBuffers compiler which generates the code for encoding and decoding data using FlatBuffers protocol. FlatBuffers.dll contains classes necessary to use the generated C# code.

There are multiple ways of obtaining these:

  • Use the binaries shipped with the example Physics Demo project, by default installed in %USERPROFILE%\Hadean\Aether Engine Examples\PhysicsDemo\Simulation\Protocol.
  • Building the binaries from source: flatc and FlatBuffers.dll
  • Download binaries using NuGet: flatc and FlatBuffers.dll
2. Add FlatBuffers.dll to the Unity project
  1. Create a FlatBuffers directory to hold FlatBuffers related files in Assets/ directory of the Unity project. hadean screen snippet
  2. Drag and drop FlatBuffers.dll into Assets/FlatBuffers directory.

This will make the FlatBuffers API, used for encoding and decoding data, available in your Unity project.

3. Generate the the C# source code for protocol files

The protocol compiles converts the protocol definitions specified in protocol.fbs into C# classes that can then be used to encode and decode the data.

The following command will generate the files:

# -n generates C# classes
flatc.exe -n protocol.fbs

Here are the generated methods that we're going to use in the example:

SimulationMessage.cs (details omitted for brevity):

public struct SimulationMessage : IFlatbufferObject
{
public static SimulationMessage GetRootAsSimulationMessage(ByteBuffer _bb);
public Entity? Entities(int j);
public int EntitiesLength;
};

Vec3.cs (details omitted for brevity):

public struct Vec3 : IFlatbufferObject
{
public static Offset<Vec3> CreateVec3(FlatBufferBuilder builder, float X, float Y, float Z);
};

ClientEvent.cs (details omitted for brevity):

public struct ClientEvent : IFlatbufferObject
{
public static void StartClientEvent(FlatBufferBuilder builder);
public static void AddPosition(FlatBufferBuilder builder, Offset<Vec3> positionOffset);
public static Offset<ClientEvent> EndClientEvent(FlatBufferBuilder builder);
};
4. Add the generated files to the project

Drag and drop the files generated in the previous step into the Assets/FlatBuffers directory in the Unity project.

The result should look like the following:

hadean screen snippet

5. Automate the process

The steps 3. and 4. of the above process need to be performed every time the protocol definition in protocol.fbs is changed. Therfore it's a good idea to automate the process, which can be done using the script below:

# download flatc and FlatBuffers.dll
nuget restore -PackagesDirectory "${PSScriptRoot}\packages" ${PSScriptRoot}
# create FlatBuffers directory in the assets directory of the Unity project
$unity_assets_path = "${PSScriptRoot}\UnityProject\Assets\FlatBuffers"
New-Item -ItemType "directory" -Path $unity_assets_path -Force
# get flatc from the downloaded package
$flatc_bin = Get-ChildItem "${PSScriptRoot}\packages" -Filter flatc.exe -Recurse | % { $_.FullName }
# generate csharp files
& $flatc_bin -n -o $unity_assets_path "${PSScriptRoot}\protocol.fbs"
# copy FlatBuffers.dll to the assets directory of the Unity project
Copy-Item "${PSScriptRoot}\packages\ek96.FlatBuffers.1.11.0\lib\netstandard2.0\FlatBuffers.dll" -Destination $unity_assets_path

Using FlatBuffers for marshalling the data

Once the protocol has been defined and all the classes and libraries are visible in Unity, the data marshalling can be implemented.

using FlatBuffers;
public class Simulation : MonoBehaviour
{
List<global::Entity> DecodeMessage(byte[] data)
{
ByteBuffer bb = new ByteBuffer(data);
var entities = new List<global::Entity>();
var message = SimulationMessage.GetRootAsSimulationMessage(bb);
for (int i = 0; i < message.EntitiesLength; i++)
{
entities.Add(message.Entities(i).Value);
}
return entities;
}
byte[] EncodeEvent()
{
FlatBufferBuilder fbb = new FlatBufferBuilder(1);
var position = Vec3.CreateVec3(fbb, 0, 0, 0);
ClientEvent.StartClientEvent(fbb);
ClientEvent.AddPosition(fbb, position);
fbb.Finish(ClientEvent.EndClientEvent(fbb).Value);
}
}

Implementing the data marshalling on the simulation side

Steps to implement FlatBuffers marshalling on the simulation side are similar to the steps on the client side:

  1. Add the FlatBuffers header-only-library to the CMake project
  2. Generate C++ source code from the protocol definition and add them to the project
  3. Call FlatBuffers marshalling methods in order to serialize and deserialize the data.

More details on the usage of FlatBuffers on the server side are available in the protocol documentation and in the PhysicsDemo installed with the SDK.