Entity pipeline
The entity pipeline is the logic which a worker uses to create and manage their representations of entities.
A SpatialOS deployment communicates with connected workers to instruct them when an entity needs to be added/removed from the worker’s view. For example, when an entity moves into or out of a worker’s checkout area. The entity pipeline handles:
- choosing how to represent an entity locally - in Unity, we use a prefab to represent an entity
- dealing with changes in an entity’s components (one being added or removed) and changes in the properties of those components (values being changed, events being triggered)
In most cases, you won’t need to interact with the entity pipeline: the default implementation should suit most people’s needs.
However, if you need to customize any of this process (how entities are created, changed, and removed), you can use the Entity Pipeline APIs and overide the default pipeline with an implementation of your own: either by modifying the default implementation or by writing an implementation from scratch.
What the entity pipeline does
Representing entities
Different workers may represent the same entity in different ways. Unity workers represent entities using GameObjects, based on a specified prefab; that isn’t true for other types of workers.
Entities normally have a Metadata
component with an entity_Type
string property that, for Unity workers, can be
used to specify which prefab to associate with the entity.
Placing entities in the scene
All entities have a Position
component that is their canonical position in the world. It is a vector of three double
values, and it’s used to govern which entities a worker checks out, load-balancing, and more.
In most cases, entities need to be positioned on a worker in a way that meaningfully represents their Position
in the simulated world. However, there are some cases where you’d want to implement custom logic for transforming
positions - for example, if the world is very large or very small. Or you might want a secondary representation for
entity position.
Usually you’d want to use the Position
component, but you can also define additional components to represent an entity’s position if you need to.
Handling component-related changes
When entities change, workers get instructions to make the relevant change: eg AddComponent
, RemoveComponent
,
ComponentUpdate
, AuthorityChange
. By default, UnitySDK handles these instructions, and you can use the code
generated by codegen from your schema to respond to these changes - for example, you can register methods to be
called when a particular component changes.
However, you can also build a bespoke process for processing those intructions.
Customizing the entity pipeline
Architecture
In order to understand how you can customize the pipeline, this section explains what parts it has.
The entity pipeline comprises a sequence of blocks that implement the IEntityPipelineBlock
interface:
Each pipeline block is added to the pipeline in the order that pipeline instructions (“ops”) should be processed. Every pipeline blocks supports the following instructions:
AddEntity
: An entity is added to the worker.RemoveEntity
: An entity is removed from the worker.AddComponent
: A component is added to an entity.UpdateComponent
: A component’s properties were updated or component events were triggered.RemoveComponent
: A component is removed from an entity.ChangeAuthority
: The worker has gained or lost write access to a component of an entity.CriticalSection
: The worker has entered or left a critical section. Used for marking atomic collections of instructions.
A worker begins to receive ops once it is connected to SpatialOS. The entity pipeline passes the received ops to
the first block of the pipeline, which may perform some action as a result. It may then pass the ops to the
next pipeline block (NextEntityBlock
) for further processing.
This means you can create a pipeline implementation that can wait for a certain condition to be satisfied before
an entity is spawned. Blocks may process their internal state (possibly emitting ops as a result) inside the body
of a ProcessOps()
method, which is called periodically (typically once per frame).
The default entity pipeline
The default entity pipeline consists of the following blocks:
CriticalSectionPipelineBlock
: Buffer all ops that are part of a critical section and release them to the next block as soon as the critical section ends.ThrottledEntityDispatcher
: Limit the number of entities that can be spawned in a single frame and defer entity spawning to the next frame if necessary.LegacyEntityCreator
: Spawn and delete entities - Create aGameObject
in the current scene according to theentity_type
property in theMetadata
component of an entity.LegacyComponentPipeline
: Process component related ops (AddComponent
,RemoveComponent
,ComponentUpdate
,AuthorityChange
) and apply changes to code-generated readers/writers.EntityComponentUpdater
: Process component related ops (AddComponent
,RemoveComponent
,ComponentUpdate
,AuthorityChange
) and apply changes to code-generated monobehaviour components.
Example of a custom entity pipeline
Suppose you want to build a simple entity pipeline that spawns a sphere into the scene when an entity is added.
First, add this very simple pipeline block:
internal class SimpleEntityPipeline : IEntityPipelineBlock { public void AddEntity(AddEntityOp addEntity) { var sphere = GameObject.CreatePrimitive(PrimitiveType.Sphere); Object.Instantiate(sphere, new Vector3(), Quaternion.identity); } // ... }
- In Bootstrap.cs, register the block with the pipeline:
private void Start() { // ... EntityPipeline.Instance.AddBlock(new SimpleEntityPipeline()); // ... }
That’s all that’s needed for a minimal implementation. The entity pipeline receives an
AddEntityOp
for every entity checked out by the worker and relays it to the first block: in this case, theSimpleEntityPipeline
which creates the game object.This works, but it doesn’t reflect the state of the simulated world: all the entities are spawned at the origin.
To spawn the entities at their actual location, you need to receive the
Position
component associated with the entity.Extend the pipeline block to collect both types of ops:
internal class SimpleEntityPipeline : IEntityPipelineBlock { private readonly Queue<AddEntityOp> entitiesToAdd = new Queue<AddEntityOp>(); private readonly IDictionary<EntityId, PositionComponentData> positions = new Dictionary<EntityId, PositionComponentData>(); public void AddEntity(AddEntityOp addEntity) { entitiesToAdd.Enqueue(addEntity); } public void AddComponent(AddComponentPipelineOp addComponentOp) { if (addComponentOp.Component.ComponentId == Position.ComponentId) { positions[addComponentOp.EntityId] = ((PositionComponent.Impl) addComponentOp.ComponentObject).Data; } } // ... }
- The object should be instantiated when both the
AddEntityOp
andAddComponent
op forPosition
have been received. Implement this logic in theProcessOps()
method:
public override void ProcessOps() { while (entitiesToAdd.Count > 0) { var entity = entitiesToAdd.Peek(); var entityId = entity.EntityId; PositionData position; if (positions.TryGetValue(entity.EntityId, out position)) { var gameObject = Object.Instantiate(sphere, position.coords.ToVector3(), Quaternion.identity); positions.Remove(entityId); entitiesToAdd.Dequeue(); } } }
- The object should be instantiated when both the
The spheres should now appear in the correct location.