Interact with objects in the world

This page covers the design considerations when interacting with objects. Some of the most common game features require
interaction between the player and objects in the world around them. Examples include picking up a gun, chopping down a
tree, commanding an army of soldiers, or planting seeds in a pot.

Background

The rest of this design guide will use the term
components when referring to data about objects which take part in an interaction. Every interaction can be seen as a
change to a component of an entity which logically causes changes to components in other entities.

There are several features of components which make this type of interaction possible:

Interest

Access control

Events

Commands

Interest

Interest allows all components which are needed for the
interaction to be viewed by the same worker.

Out of all these features, entity interest
is the only one that is required. The specified radius is one of the most important parameters for spatial
load-balancing. If two entities A and B are within the entity interest radius (of each other), it is guaranteed
that the worker with authority over a component of entity A will contain entity B in its local view of the world
and vice versa.

Access control

Access control is important to make sure cheaters cannot perform an
invalid or malicious interaction.

It is best to give the managed workers control over
game logic and write access (also referred to as authority) over components if the effects matter to other players.
This is not always a straightforward decision, and it’s more a matter of whether you can trust the client to be able
to arbitrarily change data.

For example, the client can have authority over a component which contains the colour of their hair. The player being
able to change their hair colour would have no impact on the game’s dynamics. In fact, players will probably have
access to a user interface to configure the appearance of their character. However, in a game where hair colour
depends on player’s team, you won’t give that authority to clients.

Interest gives a way to see all the data needed to run the interaction logic.
However, with the addition of access control, the ability to change this data is partitioned in multiple
worker requirement sets,
and usually no single program (worker) can perform the interaction end-to-end by itself.

You need to think about which workers are involved in an interaction and how they will communicate to perform it.
At a low level all worker communication, is done via dispatcher callbacks and ops.
Features such as events and commands are high-level concepts which are implemented in terms of different ops.

Events

Events are one of the ways to implement the required
communication between workers. They rely on spatial locality and are propagated using component updates.
These two properties of events are central when it comes to the design of an interaction. A generic interaction looks
something like:

A component update with an event is sent by a connected worker with the required access.

The component update is received by connected workers with interest.

One (or many) of the workers that received the event have access to a component which needs to be updated in
response to this event. This is where specific game logic plays out and it’s hard to be generic. Some
interactions will require data from other components to be accessed by the worker in order to validate whether
the event should be handled normally or treated as an error or cheat. Other interactions will require the worker
to send more component updates which might in turn be handled by other workers.

Events are good for fire-and-forget interactions with components on entities
which are guaranteed to be in the same interest regions. Request-response interactions could also be implemented
by sending multiple events but this is not recommended.

Commands

Commands facilitate communication in the other direction to events and properties: they allow any worker to send a
request to the worker with write access to a specific component on a specific entity. The receiving worker can take
action and should respond to the request.

Commands are best suited when you don’t know where the target entity is, or know that it’s
likely to be far away. You can short-circuit
commands that you think will be received by the same worker that sent them, but that comes with a lot of
caveats.

Designing interactions

There are different properties you might want to achieve based on the type of interaction. Here are a few such properties with some principal ways to support them:

Atomicity (you want to guarantee that a composite interaction is either executed in full or not at all) - request-response pairs, critical sections, transactions

Implementing many of these yourself is often non-trivial and the SDK will improve to support you better in the future.

Example

For example, consider picking up an item from the ground.

In response to the player’s local action, you would assume
the interaction will be successful and start animating or add the item to their inventory. You would also want to
communicate with some worker which has write access to components of this item. This is naturally not the client worker
of your player as you don’t want a single client to control the access to items which could be picked up by any player.
In a race between two players trying to pick up an item, one will fail and will need to revert their assumed state to
the state they had before trying to pick up the item.

This is a clear case for using commands and breaking the race condition by handling the request which arrives first
with a success response type and the second one with a failure.

Note that a response containing a failure is different
from a command failing itself. The most common reason for command failure is loss of authority by the target worker.
You could try to handle this yourself for now, but there are multiple planned improvements to commands which will make
them more reliable and nicer to use. Automatic retry strategies, more reliable routing of commands, and responding to a
request with command failure are a few examples.

Using multiple events

Multiple events could be used to achieve the same functionality. An event will be received by all workers which could
possibly handle it as long as they have interest to receive component updates which include this event. Workers will
need to check authority and the one which owns the components which need to be changed will actually perform the
interaction. Then another event could be triggered to perform the response part of the interaction in a similar
fashion. As you can see in the Unity SDK some of these checks can be exposed in the form of readers and
writers to make the programming
experience nicer. However, game logic which relies on multiple events to emulate a command can easily become messy and
prone to programmer errors. Last but not least, propagating events to multiple workers could put more load on the
network if component interest is not carefully optimised.

Local interactions

All the interactions discussed so far are between components. However, there is a lot of room for interactions between
local objects in SpatialOS games. Local objects could exist on each worker independently - there is no component with
data about them in the SpatialOS world.

Example

As an example, consider shooting cannonballs in the Pirates
tutorial. This is a great example of a composite interaction
which also contains a local interaction. The shot is initiated with an event triggered by the worker which owns
ShipControls (usually a client, but could be other if ships were controlled by AI). All workers which are close to
the event - both clients and managed Unity workers receive the event and spawn a cannonball to handle it. The
cannonball is a local object. There are two distinct reasons for spawning the cannonball:

On clients, it’s created for visualisation purposes. Players receive the gameplay they expect when it comes to shooting.

On managed physics workers, visualisation doesn’t matter. However, cannonballs can interact with a ship by colliding
with it. This is designed as a local interaction which is handled by Unity’s collision detection in this case. The
authoritative physics worker will have a chance to continue the interaction. Collisions on clients or non-
authoritative physics workers which have read access don’t matter.

Local collisions between cannonballs and ships lead to more interactions. A component update changes the health of the
damaged ship. The health reaching zero is equivalent to an event being sent for sinking. Again, all interested workers
(clients which are close) get a chance to handle this by playing the sinking animation. In addition, a command is sent to the firing ship to award them points.

You can design many different interactions similarly. Always start your design process by outlining the causes and
effects of each component state mutation.

More examples

Here are a few more examples to reinforce your understanding of the design process. These are by no means the only ways
to design each given interaction, but are sensible choices for the given assumptions.

Opening a door

Players can open and close doors in a house by pressing a button. Whether a door is open or closed is naturally
synchronised for all players who are in this house. Consider opening only for the design, closing is similar.

For this design take each house as an entity. This would ensure all the parts that make up a house will always be
managed by the same worker and players will either not see the house or they will see the whole house. Doors are local
objects (possibly children of the house object), but their state is stored in a DoorController component. Each door
in a house has a unique identifier:

A player is close to a door and the client displays a prompt showing it’s possible to open it.

The player presses a button to open the door.

Door opening animation starts on client.

An open_door command is sent with the door_id to the house entity. The command timeout is set to a reasonable
value. If there is no response at the time a player can reasonably expect to walk through the opening door, the
command should time out and the local door object should be closed. Then the player can try to open the door again.

Note that the player is not able to walk through the door - it is still closed on the authoritative physics worker
which also controls the player’s cannonical position. You could let the player start moving and bounce them back in
case of failure. At this point no other player is aware of the door opening and the authoritative worker for the
DoorController is about to receive the command. The are two alternatives - the authoritative worker will either
receive the command or, if the entity is migrated to another worker in the meantime, the command will fail. You already handle the failure as described above.

Given the command is received:

The authoritative worker checks the player position to ensure the player is close enough to be allowed to open this
door. Malicious clients may be able to forge a command to a door in another room or another house.

The authoritative worker sends a response for the command:

If the player doesn’t pass the validity check, a Response is sent with the failed property set to true. The
authoritative worker doesn’t open the door.

If the player passes the validity check, a Response is sent with the failed property set to false. The
authoritative worker opens the door allowing objects to walk through.

The authoritative worker triggers a component update for the relevant door to make sure the door is visualised as
open on other clients too. The client which sent the command could ignore this update, or synchronise its opening
animation. Note that other clients might have started opening the door as well - the visualisation would need to be
synchronised for them, too. Overall, there is no difference in how the update is handled by clients - they all need
to make sure relevant synchronisation occurs.

In scenarios when the update is transient, you could use an event instead of a
property. In this case, you’d use because doors need to stay open after they’ve been opened. Without
persistence, new clients viewing this house won’t know if the doors are open or closed.

Note that the validation step could use arbitrary conditions - check if the door is locked or blocked by some items on
the other side, or check if the player has the right key in their inventory.

Finally, the client which sent the command receives a response. In case the response is failed, the door is
closed locally as described above. If the command fails before timing out due to entity migration, it is retried without player interaction with a shorter timeout.

Calling a companion

Players interact with companion characters which are driven by AI and can respond to player commands. A player sends
their raven companion to find out more about an enemy base which is out of view. At some point the player can decide to
call their companion back or to see the information a companion has gathered telepathically.

package companion;
// companion/data.schema
// Request type for recalling a companion
type CompanionRecall {
// The player position is used by the companion so that it knows where to fly back to
// Since a player might be moving, a companion might need to query and readjust
// its pathfinding as it's coming back.
bool to_player = 1;
// A player might ask the companion to meet them at a specific location
// if `to_player` is set to false.
Coordinates recall_position = 1;
}
// A simple response type which can indicate failure
type Response {
bool failed = 1;
}
// Empty request for telepathy. Could contain details about what the player needs to know
type CompanionTelepathy {
}
// The companion state which can be communicated to a player and is populated by
// the companion's internal logic as it observes the world. Used as response type
// for telepathy.
type CompanionState {
EntityId owner_id = 1;
uint32 enemies_found = 2;
list<Coordinates> places_visited = 3;
uint32 health = 4;
uint32 hunger = 5;
bool has_been_spotted = 6;
Coordinates companion_position = 7;
}

While doors could also be opened by triggering an event, commands are actually required in this case because
companions will often be very far away from players - more specifically, outside the entity interest radius. Even
though it’s possible, explicitly registering interest for the components of a companion would stretch the client
worker too far.

You will probably want to limit the telepathy ability of a player by placing it on a cooldown. This is an example of
validation which might be better performed before sending the actual command. Because a player could have multiple
companions, it will be hard for the callee to verify the last recall time just by looking at the data for a single
companion. It’s best to do this check on the server-side worker authoritative over the CompanionController before
sending a command. Remember in the example with doors client workers were always free to send the command. The player
client can trigger an event which is handled by the owner of CompanionController as a way to request the command to
be sent.

Summary

You can design rich interactions between objects in SpatialOS. Often the important decisions will be hidden in subtle
gameplay details. Don’t be afraid to experiment with different designs - aim for quick prototyping and always evaluate
several prototypes rejecting the bad ones.