Get SpatialOS

Sites

Menu

Overview of our Unity integration

The purpose of this page is to give you a technical overview of the Unity SDK from the inside. This page is for you if you want to understand how the Unity SDK works, 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. Take a look at the Unity SDK section which contains all the information you need to start developing a game.

Most of this page discusses implementation details which are almost certain to change because the Unity SDK is under development. Treat it as an example of a game engine integration and not as the canonical truth about the Unity 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 some of the source code for the Unity SDK in each of your Unity projects in the Assets/Improbable/Sdk directory. 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; this 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# and other language 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 - MonoBehaviours, GameObjects, 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.

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.

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.

Unity integration and Ops

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.

Hierarchy of asset loaders

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. This 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 Configuring the Unity build process 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.

Was this page helpful?

Thanks for letting us know!

Thanks for your feedback

Need more help? Ask on the forums