Get SpatialOS

Sites

Menu

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

In SpatialOS terms, an object from a game world is usually represented as an entity with a set of relevant components.

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.

You can define interest at the configuration level, or at run-time. Features such as entity interest, streaming queries and component delivery are specified in each worker’s configuration. You can use entity queries and sending component interest overrides in worker programs.

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:

  1. A component update with an event is sent by a connected worker with the required access.
  2. The component update is received by connected workers with interest.
  3. 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 are essentially a remote procedure call (RPC).

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
  • Reliability (you want to guarantee an interaction eventually succeeds)- retry strategies, authority loss imminent notification, response forwarding
  • Security (you want to avoid invalid or malicious interactions) - access control lists, callee checks caller attributes

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:

// Door identifiers
enum DoorType {
    Unknown = 0;
    Entrance = 1;
    Kitchen = 2;
    Bedroom = 3;
    Bathroom = 4;
}

// Contains the state of a door
type Door {
    DoorType door_id = 1;
    bool open = 2;
}

// Contains an identifier for the door within the house entity
type DoorActionRequest {
    DoorType door_id = 1;
}

// A simple response type which can indicate failure
type Response {
    bool failed = 1;
}

component DoorController {
    // Component ID, unique within the project
    id = 1337;

    // Stores the state of doors
    Door entrance = 1;
    Door kitchen_door = 2;
    Door bedroom_door = 3;
    Door bathroom_door = 4;

    // Used by other entities to request opening a door
    command Response open_door(DoorActionRequest);
}

The interaction proceeds as follows:

On the player’s client:

  1. A player is close to a door and the client displays a prompt showing it’s possible to open it.
  2. The player presses a button to open the door.
  3. Door opening animation starts on client.
  4. 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:

  1. 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.
  2. 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.
  3. 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.

  4. 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.

Several user-defined types are used for requests, responses, and event types:

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;
}

Player state is split between two components:

package player;
// player/abilities.schema

import "companion/data.schema"

// Server-side player abilities and command validation
component CompanionController {
    // Component ID, unique within the project
    id = 8055;

    // Used for finding the companion for commands
    EntityId companion_id = 1;

    // Ability cooldowns
    uint64 last_recall_time = 2;
    uint64 last_telepathy_time = 3;
}

// Client-side player controls
component PlayerAbilities {
    id = 8057;

    event CompanionRecall recall;
    event CompanionTelepathy telepathy;
}

Companion entities have a component which defines the commands:

package companion;
// companion/companion.schema

import "companion/data.schema"

// A component on companion entities
component Companion {
    id = 8056;

    data CompanionState;

    command Response recall(CompanionRecall);
    command CompanionState telepathy(CompanionTelepathy);
}

There are a few interesting additions to this interaction:

  • 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.

Was this page helpful?

Thanks for letting us know!

Thanks for your feedback

Need more help? Ask on the forums