Overview of our Unity integration
The purpose of this page is to give you a technical overview of how the open-source Unity SDK from the inside. This page is for you if you want to understand how an integration can work, for example if you’re implementing your own game engine integration; it won’t be useful if you’re just using the Unity SDK to build a game
Most of this page discusses implementation details which are almost certain to change. Treat it as an example of a game engine integration and not as the canonical truth about that SDK. Focus on enriching your mental picture of SpatialOS.
After reading this you will be able to:
- Explain the different ways generated code interacts with game objects
- Explain how game objects associated to entity prefabs are loaded based on the asset loading strategy
- Come up with the high-level design elements if asked to integrate a game engine you are familiar with as a SpatialOS worker type
Before you start
Since this page covers some fairly advanced content, you should first know about Using the C# SDK and Using the Unity SDK. Some pages you should definitely be familiar with are:
Some experience coding with both will also help you grasp this page better. If you feel like you don’t have enough practical experience or the code snippets at some point stop making sense, head back to the Tutorials or ask a question on the forums.
You have access to the source code for the Unity SDK. When you need to understand something in more detail than it is explained in here, the best thing to do is have a look at the source code.
The Entity pipeline
The entity pipeline is the glue between Ops (operations) sent by SpatialOS, schema-generated code, and engine-specific logic for creating and managing game objects and rendering logic. It processes most of the received Ops and puts the local worker in a state synchronized with the game world so that local computation can occur and ultimately result in sending Ops back to SpatialOS. It’s fairly customisable; the Entity pipeline page describes the responsibilities of each block in the default implementation.
The entity pipeline is initialised by the ConnectionLifecycle
component right
before starting the connection to SpatialOS. This component manages the tasks
required to take a worker through its bootstrap lifecycle. During
initialisation the entity pipeline registers dispatcher callbacks in two
different ways:
Some block instructions are called by the corresponding kind of dispatcher callback directly for the first block in the pipeline:
AddEntity
RemoveEntity
CriticalSection
Other block instructions are registered by the entity pipeline with the dispatcher through the generated readers and writers for every component defined in your schema:
AddComponent
RemoveComponent
UpdateComponent
ChangeAuthority
The following two sections give an in-depth overview of how dispatcher
callbacks are handled from generated readers and writers (for example
Position.Reader
and ShipControls.Writer
) and by generated MonoBehaviours
which rely on the entity pipeline instead. In the current implementation this
separation is not that clear because the initial implementation of the entity
pipeline only catered for generated readers and writers. Generated
MonoBehaviours were added later and had to fit the existing design.
Local entities
Most workers that do something useful need a local representation of the part of the SpatialOS world they have access to.
Entity objects as local representations of an entity
It is important to make the distinction between an entity - a core SpatialOS concept for representing sets of related components, and an entity object - an implementation detail in the Unity SDK which associates an entity as seen by the worker with an underlying game object and the corresponding Unity components.
The View
class provided by the C#, C++ and Java SDKs is a container for entities
and their components where all of the information is synchronised from (but not
to) SpatialOS. The Unity SDK has the concept of local entities - another type of
container for all entity objects that are observed by the worker (see
ILocalEntities
). In comparison to the View, the set of local entities
used by the Unity SDK contains entity objects with data which is local to the
instance of Unity and only useful in the context of the Unity game engine -
MonoBehaviour
s, GameObject
s, etc. The default set of local entities used with by the
entity pipeline is called LocalEntities.Instance
.
Representing entities as entity objects and managing the view of local entities is a design decision which makes sense in the context of Unity. In contrast, the Unreal engine integration directly uses the
View
from the C++ API and and has a bidirectional mapping between EntityId and Actor.
What handles entity-related Ops
Now that you know what an entity object is, the crux of this section is
straightfoward. The entity pipeline adds and removes entity objects to and from
the set of local entities in response to the corresponding Pipeline Ops (see the
LegacyEntityCreator
block for the default implementation). A Pipeline Op is a
thin wrapper around normal Ops used as a layer of abstraction by the Unity SDK.
Then it uses an entity template provider and a prefab factory to instantiate
the underlying game objects, as described below in The Asset pipeline.
Handling component-related Ops
There are two different ways the Unity SDK works with components defined in schema. The original way is component readers and writers; generated MonoBehaviour components were added later. This section explains how the two methods fit into the overall structure of the SDK.
Component updates with readers and writers
For every component defined in your schema there is a corresponding generated reader and writer. Multiple handlers are registered with the dispatcher callbacks:
ComponentUpdated
AuthorityChanged
- Generated property callback handlers such as
CanBeIgnitedUpdated
- Generated event callback handlers such as
NavigationFinishedTriggered
This is represented by the blue arrow in the diagram below.
As a result, if you’re directly using component readers and writers in your Unity components, any callbacks you register with them sit next to the entity pipeline callbacks (which are registered by the Unity SDK) and don’t depend on the pipeline logic in any way. For example, this:
[Require] private Position.Reader PositionReader;
void OnEnable()
{
PositionReader.ComponentUpdated.Add(OnPositionUpdated);
}
adds the callback directly to the corresponding EventCallbackHandler
in the
generated SpatialOsPositionReaderWriter.cs
. For most Unity projects the
generated code for readers and writers is in
workers/unity/.spatialos/generated/readers_writers
. You can find this out in
the Unity Code Generation step of the spatialos.unity.worker.build.json
- it
should look similar to this:
{
"name": "Unity Code Generation",
"description": "Generates Unity MonoBehaviours, readers and writers from the project schema.",
"arguments": [
"invoke",
"unity-mono",
".spatialos/bin/CodeGenerator.exe",
"--",
"--json-dir=.spatialos/json",
"--unity-component-output-dir=Assets/Improbable/Generated/Components",
"--unity-editor-component-output-dir=Assets/Improbable/Editor/Generated/Components",
"--reader-writer-output-dir=.spatialos/generated/readers_writers"
]
}
Note that the Unity SDK uses a custom code generator. When building your own engine integration, you can implement your own logic which extends or completely replaces what default generation creates for you. Have a look at Building a custom code generator for details on how this is done.
Component updates with generated MonoBehaviour components
In the code snippet above you can also see the directories for generated Unity
components. The generated MonoBehaviours don’t directly use readers and writers
- instead, they have an entityObject
field. This holds a reference to the
internal representation of an entity which is associated with a given Unity
game object. Entity objects are managed by the entity pipeline; the
EntityComponentUpdater
block in particular passes all incoming component-
related Pipeline Ops. The entity object then passes this further down to the
right component.
The main classes and interfaces related to this interaction are:
IEntityObject
EntityComponentUpdater
SpatialOsComponentBase
and its generated children (e.g.SpatialOsPositionComponent
)
You can see that this interaction (red arrows in the diagram below) has a few more levels of indirection. It closely resembles the entity-component model of the Unity engine itself. In contrast, development with readers and writers is at a lower level of abstraction as it doesn’t make any assumptions about the game engine.
Adding and removing components with readers and writers
As explained on the
MonoBehaviour lifecycle
page, MonoBehaviours on game objects which are
associated with an entity object are enabled and disabled by the
LegacyComponentPipeline
block. This relies on the [Require]
syntax and
makes sure that a MonoBehaviour is enabled only when data for all required
readers and writers is available and disabled otherwise.
It’s also worth mentioning how authority changes are handled in this section.
The AuthorityChangedNotifier
pipeline block delegates AuthorityChange Ops to
the entity objects which have to clear any component writers that can no longer
be used, or setup component writers when authority is gained.
Adding and removing components with generated MonoBehaviour components
In contrast, the state of generated MonoBehaviours is managed by the
EntityComponentUpdater
block. Because the Unity SDK has control over the
generated code it can simply set the IsComponentReady
property whenever the
state of the SpatialOS component associated to a Unity component changes. See
SpatialOsComponentBase
for the implementation details of how components are
disposed.
When it comes to authority change, the EntityComponentUpdater
block handles
that, too. Since generated MonoBehaviour components are much more closely
related to SpatialOS components, there is a lot less happening behind the
scenes. A component is not disabled when authority changes - instead the
HasAuthority
boolean flag is set. If attempts are made to send component
updates without having authority, an error is logged and no update gets sent.
Summary
This side-by-side comparison should show you the way you integrate with SpatialOS is flexible and can always be tailored to suit what a game developer needs. The key takeaway from this section is that many of the design decisions in an integration are driven by the design of schema generated code.
Some developers prefer using readers and writers while others like the generated MonoBehaviours more. If you’re creating an integration, try to find out how the developers who will be using it imagine the ideal integration. Then see if you can generate code to make this possible.
The Asset pipeline
When an entity object is added to the worker’s set of local entities, a game object needs to be created. This section looks at how that is done. As a reminder of how entity objects are created, jump back to Local entities.
Entity template providers
Delving into more detail, you can see the LegacyEntityCreator
block of the
pipeline uses an entity template provider (IEntityTemplateProvider
).
// In LegacyEntityCreator.cs
private void MakeEntity(EntityId entityId, string prefabName,
Action<EntityId, string> onSuccess)
{
entitiesToSpawn[entityId].IsLoading = true;
templateProvider.PrepareTemplate(prefabName,
_ =>
{
onSuccess(entityId, prefabName);
},
// ...
)
}
private IEntityObject InstantiateEntityObject(EntityId entityId, string prefabName)
{
using (new SaveAndRestoreScene(entitySpawningScene))
{
var loadedPrefab = templateProvider.GetEntityTemplate(prefabName);
var underlyingGameObject = prefabFactory.MakeComponent(loadedPrefab, prefabName);
// ...
}
}
Note that an entity template here doesn’t refer to what we usually mean when we use this term in the Glossary. Entity template here is an instance of a game object which will usually be cloned to instantiate the underlying game object for an entity object.
The template provider prepares the right game object for the entity being created at this stage of the pipeline. As you will see in this section “prepare” might involve different things based on how the template provider is configured. Some of the conceptual tasks include but are not limited to:
- Finding the asset (could be in memory cache, on disk locally or in the cloud)
- Making sure the required Unity components to start using the game object are available and in the required state
The template provider used by the Unity SDK is called
DefaultTemplateProvider
. It creates a hierarchy of
decorators called asset
loaders (IAssetLoader
and IAssetDatabase
) that implement the various
strategies for loading assets. Have a look at the three interfaces mentioned so
far:
namespace Improbable.Unity.Entity
{
// Summary:
// An IEntityTemplateProvider can look up a GameObject to use as a
// template for a prefabName.
public interface IEntityTemplateProvider
{
GameObject GetEntityTemplate(string prefabName);
void PrepareTemplate(string prefabName, Action<string> onSuccess, Action<Exception> onError);
}
}
namespace Improbable.Assets
{
public interface IAssetDatabase<TGameObject> : IAssetLoader<TGameObject>
{
bool TryGet(string prefabName, out TGameObject prefabGameObject);
}
public interface IAssetLoader<TAsset>
{
void LoadAsset(string prefabName, Action<TAsset> onAssetLoaded, Action<Exception> onError);
}
}
You can see how the method signatures become more and more specific so that at
the end consumers of IEntityTemplateProvider
can just exchange a prefab name
for a game object once the template has been prepared. Consumers (such as the
LegacyEntityCreator
) don’t need to care about how the assets associated to
this game object were loaded and as you’re about to see there are three
different ways and possibly many more are supported with the asset pipeline
architecture.
Strategies for loading assets
Based on the configuration settings and environment a DefaultTemplateProvider
is running in, the asset loading strategies are:
- In-Editor and using local prefabs
- Using local assets from an asset bundle
- Streaming assets from a cloud deployment
Each of these strategies has a corresponding basic implementation of
IAssetLoader
which is then decorated. This is illustrated by the following
diagram. Each block is a class which is wrapped by construction in one of the
blocks below it.
It’s easy to map these strategies to different workflows and this is what the
DefaultTemplateProvider
does based on how the worker was started:
- Connecting a worker by starting a scene in the Unity Editor during
development will use the Unity
AssetDatabase
. - Connecting a worker to a local deployment for local testing will use local asset bundles.
- Connecting a worker to a cloud deployment for scale testing and in production will use streaming by default.
Additionally, a command-line argument can override any of these options.
There are some other parts of the asset pipeline not described in this section, such as the prefab factory and asset-preloading strategies. You can explore the SDK source code to learn more about how they work.
To learn about how entity prefabs end up in the assembly at the first place, have a look at Exporting entity prefabs into the assembly. That page also has advice on how to implement custom asset loading strategies.
Editor extensions
Last but not least, the Editor tools for developing in Unity are a big part of the Unity SDK. They come precompiled so that if your builds fail, the editor menus and windows will still work. Several of the main functionalities of the Editor extensions are:
- A simple build system on top of the
spatial
CLI - Default player build configuration
- Default exporting of entity prefabs
- UI for the Spatial OS window and menus
The build configuration and exporting of entity prefabs are fully customizable. Have a look at the Configuring the Unity build process page to find out more. It’s also great to extend the Editor even further when you have a scenario which can improve your development workflow. An interesting example is using the Snapshots API to edit snapshots in a Unity scene.