b. Synchronizing movement

Now that you have baseline movement, you can integrate the Transform Synchronization module. This will ensure that movement simulated by a worker is actually sent to SpatialOS and observed by other workers!

What does the Transform Synchronization module do?

The Transform Synchronization module enables replication of a GameObject’s Transform state to other workers. Instead of re-using the Position component, the module uses its own TransformInternal component which contains additional fields to location such as rotation and velocity. If these fields were on separate components, we would lose the guarantee that all relevant fields are updated atomically.

SpatialOS uses the Position component as part of load balancing and relative QBI queries, which makes each Position update generally more costly than a normal component update. We can therefore use TransformInternal at our desired update rate, with Position being infrequently updated for load balancing purposes.

The TransformInternal module also contains the compressed representations of location, rotation and velocity. If TransformInternal has a high update rate and the Position has a low update rate, you would have a net bandwidth saving compared to updating just Position at a high frequency!

To learn more, read the Transform Synchronization documentation.

The best thing about this is that once the module is set up, you would only need to worry about modifying your standard Unity Transform or Rigidbody! All the TransformInternal handling, replication and compression is abstracted away by the Feature Module.

Set up the Transform Synchronization module

First things first, you need to update the blank project’s assembly definition to include the Transform Synchronization module. Open Assets > BlankProject > BlankProject.asmdef and add Improbable.Gdk.TransformSynchronization.

Next, navigate to Assets > BlankProject > Scripts > Workers and open the UnityClientConnector script. Inside the HandleWorkerConnectionEstablished method, add TransformSynchronizationHelper.AddClientSystems(Worker.World);. This adds all relevant transform synchronization-related systems to a client-worker after a successful connection to SpatialOS. The method should now look something like this:

protected override void HandleWorkerConnectionEstablished()
{
    PlayerLifecycleHelper.AddClientSystems(Worker.World);
    GameObjectCreationHelper.EnableStandardGameObjectCreation(Worker.World, entityRepresentationMapping);
    TransformSynchronizationHelper.AddClientSystems(Worker.World);
    ...
}

Next, call PlayerLifecycleHelper.AddServerSystems(Worker.World); from the UnityGameLogicConnector.

protected override void HandleWorkerConnectionEstablished()
{
    Worker.World.GetOrCreateSystem<MetricSendSystem>();
    PlayerLifecycleHelper.AddServerSystems(Worker.World);
    GameObjectCreationHelper.EnableStandardGameObjectCreation(Worker.World, entityRepresentationMapping);
    TransformSynchronizationHelper.AddServerSystems(Worker.World);
    ...
}

Update entity templates

Now that the workers are set up, you need to add the components used by the Transform Synchronization module to the Player and Sphere entity templates. Open the EntityTemplates script at Assets > BlankProject > Scripts > Config.

Inside CreatePlayerEntityTemplate, use the TransformSynchronizationHelper to add the TransformInternal component to the EntityTemplate.

The AddTransformSynchronizationComponents method only requires the target EntityTemplate and the attribute of the worker you would like to give write-access to. As Player movement is client-authoritative, you provide the clientAttribute. You also provide the initial spawn point of the Player entity.

public static EntityTemplate CreatePlayerEntityTemplate(EntityId entityId, string workerId, byte[] serializedArguments)
{
    ...
    PlayerLifecycleHelper.AddPlayerLifecycleComponents(template, workerId, serverAttribute);
+    TransformSynchronizationHelper.AddTransformSynchronizationComponents(template, clientAttribute, position);
    ...
    return template;
}

Specifying a general worker type for a component’s write-access tells the Runtime that an instance of the given worker type should be authoritative over that component on an entity. This is great when you have several server-workers and want to let the Runtime handle authority changes as the player moves in the world.

Setting write-access to `UnityClient` means that the Runtime will do nothing as you have not specified a load balancing strategy for this particular worker type. You need to specify the name of the specific worker instance that the Runtime should assign authority to so that clients are assigned authority over their components.

To learn more, read the documentation on the EntityAcl component’s component_write_acl.

In addition to position, AddTransformSynchronizationComponents has two more optional parameters: rotation and velocity. To vary things up, you might decide to change the rotation of each sphere that spawns, so add a Quaternion rotation as an argument to the CreateSphereTemplate.

Inside the method body, call AddTransformSynchronizationComponents but include both rotation and position. Remember to use serverAttribute instead of clientAttribute this time, because sphere movement is server-authoritative.

-public static EntityTemplate CreateSphereTemplate(Vector3 position = default)
+public static EntityTemplate CreateSphereTemplate(Quaternion rotation, Vector3 position = default)
{
    ...
+    TransformSynchronizationHelper.AddTransformSynchronizationComponents(template, serverAttribute, rotation, position);

    var query = InterestQuery.Query(Constraint.RelativeCylinder(ServerCheckoutRadius));
    ...
    return template;
}

Once you’re happy with the templates, regenerate the snapshot so that the sphere entities will have a TransformInternal component on them.

Transform strategies

A transform strategy configures the rate at which you send transform updates and how to receive transform updates for a given worker type. By default, there is one available send strategy:

RateLimitedSendStrategy

Sends a transform and position update whenever a configurable period of time has elapsed, provided that the entity has moved.

And two available receive strategies:

InterpolationReceiveStrategy

Stores a buffer of received transform updates and creates intermediate transform values that interpolate between the updates and places them on the buffer. This sacrifices some latency to achieve visual smoothness.

DirectReceiveStrategy

Applies updates as they are received, sacrificing visual smoothness for lower latency.

Having two different receive strategies provides flexibility on an object’s visuals. As presentation is important for players, you may opt for interpolated movement on clients. However a server may prefer the direct receive strategy to get to the canonical state of the world sooner.

Each transform strategy is defined as a scriptable object, so let’s first create a TransformStrategies directory at Assets > BlankProject before setting them up.

Set up transform strategies for the sphere

As sphere movement is being handled by server-workers, you need to create a send strategy for them. To do this, select Assets > Create > SpatialOS > Transforms > Send Strategies > Rate Limited from the menu. Name this object a UnityGameLogicSendStrategy and then select it.

As our server-workers are of the “UnityGameLogic” type, enter that in the “Worker Type” field. The default configuration sets the Transform updates rate to 30Hz and position update rate to 1Hz. This means that the TransformInternal component will be updated up to 30 times a second, with the Position component at most once a second.

Note that the rates specified are “maximums” because updates won’t need to be sent unless there is a change of transform state.

When there’s more than one server-worker, you need to ensure UnityGameLogic workers can receive transform updates too. Select Assets > Create > SpatialOS > Transforms > Receive Strategies > Direct and rename the object to UnityGameLogicReceiveStrategy. This strategy has no configuration besides the worker type, so enter “UnityGameLogic”.

Of course all client-workers need to receive transform updates, but to make it smoother we’ll use the interpolation receive strategy. Select Assets > Create > SpatialOS > Transforms > Receive Strategies > Interpolation and rename the object to UnityClientReceiveStrategy. This time, provide “UnityClient” as the worker type and leave the buffer sizes on default settings.

Finally navigate to the sphere Prefab at Assets > BlankProject > Prefabs > Entities, and add the Transform Synchronization script to it. This is where you configure what strategies the Transform Synchronization module should use to replicate sphere movement. After adding the three strategies you just defined, it should look like this:

Test sphere movement

Now that you’ve set up the project, worker connectors and Prefab - sphere movement should Just Work™!

Restart your local deployment. When ready, play the development scene in your Editor. Spheres now move on both the client and server! You should also notice that spheres can collide with each other and the collision is replicated.

Setup transform strategies for players

Players have both a client-side and server-side GameObject representation, and you need to add strategies for both.

For UnityClient:

  • Re-use the client receive strategy, to receive updates from players that have entered your clients view.
  • Create a new client send strategy, for clients to use when updating their own transform and position.
  • Add the Transform Synchronization script to Player prefabs.
Transform Synchronization script for client prefab

Transform Synchronization script for client prefab

Transform Synchronization script for gamelogic prefab

Transform Synchronization script for gamelogic prefab

For the UnityGameLogic representation you don’t need a send strategy as only the clients are sending updates, however you do need a receive strategy. Re-use the direct receive strategy you made earlier here.

Test player movement

Since the snapshot and schema are unchanged, you can hit play in the Editor without restarting your local spatial instance. Now you should see that your player’s movement is being replicated!

With the Inspector open you should see the entities move around less frequently than in the Editor, in accordance with the frequency you set for Position updates. This is because the Inspector only looks at the Position component to display the location of entities in your world.

Updated about a year ago


b. Synchronizing movement


Suggested Edits are limited on API Reference Pages

You can only suggest edits to Markdown body content, but not to the API spec.