Sites

Menu
These are the docs for 14.2, an old version of SpatialOS. 14.4 is the newest →

Query-based interest (QBI) (beta)

Introduction

Query-based interest (QBI) is a way of specifying the entity components that worker types or instances need to receive updates about (their “interest”). Interest is a prerequisite for active read access, which the worker instance needs in order to receive updates about changes to entity components.

Using QBI instead of chunk-based interest (CBI)

We plan to deprecate and then remove chunk-based interest (CBI).

If you already have a SpatialOS project, you should move towards using only QBI, and if you have just started using SpatialOS, you should work only with QBI.

To disable CBI, see Turning off chunk-based interest.

We will give advance warning of the deprecation and removal of CBI, along with upgrade guides to help you move completely from CBI to QBI.

When a worker instance has write access authority over a component on an entity, it needs to receive updates about other components to be able to compute that component’s value.

For example, a physics worker instance might have write access authority over the position component of a player. In order to compute the next value of this component, the worker instance needs to receive information about:

  • the player’s joystick position
  • the obstacles in close proximity

QBI is a way to express the dependency between these components. In this example, if the player was represented by the entity with ID 25, you’d set up something like this:

position: {
  query: {
    constraint: {
      entity_id_constraint(25)
    },
    resultType: { 
      joystickComponent
    }
  }
  query: {
    constraint: {
      relative_cylinder_constraint(20m)
    },
    resultType: { 
      position, type
    }
  }
}

Based on these dependencies, the Runtime makes sure that the worker instance receives updates about the components that it needs in order to compute the position of the player.

How QBI works

You set up QBI using a component on each entity. This component is called improbable.Interest, and it contains a list of component IDs that map to queries.

You set up these queries based on the updates that a worker instance needs to receive about entity components that it doesn’t have write access authority over, in order to simulate the entity components that it does have write access authority over.

Each query needs:

  • a QueryConstraint, which can contain any combination of constraints. The constraints control which entities are matched by the query.
  • a result type, which controls the components that the worker instance receives updates about on the entities that match the QueryConstraint.

The combination of query constraints and result types for each entity create a query map for any worker instance with write access authority over it.

For example, an entity PlayerX might have an improbable.Interest component that looks something like this:

When a worker instance gains write access authority over one of the components in the list of IDs, it also gains interest in any components that match the queries associated with that component.

In the diagram above, for example, when a worker instance gains authority over component 41, this initiates interest in entity 5’s joystick component. We refer to component 41 as the initiating component and entity 5’s joystick component as a component of interest.

You set up the queries based on the updates that a worker instance needs to receive about entity components that it doesn’t have write access authority over, in order to simulate the entity components that it does have write access authority over.

Component filters

There are two types of component filter: static and dynamic.

  • If you’re using a mixture of QBI and CBI, you need to configure the component filters as explained on the component filters page.

  • If you’re only using QBI (not CBI), you should configure the static component filter so that it doesn’t affect you. See Component delivery and QBI. You also can ignore the dynamic component filter.

Frequency-based rate limiting

To conserve bandwidth, you can set an optional frequency field for each query to limit how frequently the Runtime sends updates for that query. You might want to do this for entities that a worker instance needs to receive only occasional updates about, such as a minimap (see example below). Frequency is measured in Hz.

If you want the Runtime to send updates as soon as possible, leave the frequency field unset. If multiple queries with different frequencies match the same entity component, the highest frequency applies.

If set, the shortest duration between the Runtime sending consecutive updates is 1/frequency. Note that this is based on the time when the Runtime sends each update; issues such as latency could cause a worker instance to receive consecutive updates closer together than 1/frequency.

If multiple changes are made to a component after the Runtime sends an update but before it is due to send the next update, these changes are combined and sent as a single update, 1/frequency after the previous update. The result is the same as if the Runtime sent all the changes individually.

If several events occur after the Runtime sends an update but before it is due to send the next update, these events are combined and sent as a single update that contains all the events, 1/frequency after the previous update.

Example of where you could use QBI

A player entity might have a health component and a player component. In order to compute the health of the player entity, the worker instance that has write access authority over its health would want to know about all nearby entities with a weapon component, because events relating to a nearby weapon component could lead to a changes in the value of the health component of the player entity. For example, the player character could get shot and injured by another a character with a gun.

At the same time, to render a map to the player that shows the positions of all other players in the game, the worker instance that has write access authority over the player component would want to know about the position component of all other players in the game.

So, to compute the health and player components for this player entity, a worker instance needs interest in the position component of other player entities and the weapon component of nearby entities. In this case, the player entity’s improbable.Interest component would contain queries that look something like this:

  • health
    • Constraint: relative sphere constraint AND weapon component constraint
    • Result type: weapon component
  • player
    • Constraint: player component constraint
    • Result type: position component

This means that:

  • whichever worker instance has write access authority over the entity’s health component will have interest in weapon components that are within a sphere of a specified size around the entity.
  • whichever worker instance has write access authority over the entity’s player component will have interest in the position of all entities.

The same worker instance might have write access authority over both health and player, in which case both of the queries form part of its interest map, and it has interest in weapon components within a sphere and also position components anywhere in the world.

Enabling and specifying QBI

To enable QBI for an entity, add the standard schema library component improbable.Interest (see schema definition) to that entity. You use this to specify a map of component IDs to lists of queries, so you can define the other components that a worker instance should receive updates for when it has write access authority over a particular component.

You can also update a query dynamically (during runtime) for an entity by sending updates to its improbable.interest component.

Entities that have the improbable.Interest component use QBI instead of chunk-based interest.

This means that, for an entity that has an improbable.Interest component, a worker instance that has write access authority over a component on that entity won’t automatically receive updates on other components in the same chunk.

Constraints

Within the improbable.Interest component you can specify:

  • absolute constraints (via the entity query API)
  • relative constraints (constraints that are relative to the position of the entity)

For example, you could use a RelativeSphereConstraint to specify a sphere of interest centered around the Position of an entity.

Each QueryConstraint message can only specify one of the available constraint types, but you can compose multiple constraints using the and_constraint and or_constraint fields.

The following constraints are available:

  • spatial: sphere, cylinder, box
  • entity type
  • component type

See a full list of the constraints in the sample below:

type ComponentInterest {
   type Query {
       QueryConstraint constraint = 1;

       // Provide either full_snapshot_result or a list of result_component_id. 
       // Providing both is invalid.
       option<bool> full_snapshot_result = 2;
       list<uint32> result_component_id = 3;

       // Provide a frequency (in Hz) to limit how frequently the Runtime sends 
       // updates to worker instances about the components that match a 
       // query. See the "Frequency-based rate limiting" section.
       option<float> frequency = 4;
   }

   type QueryConstraint {
       // Only provide one constraint type. Providing more than one is
       // invalid.

       option<SphereConstraint> sphere_constraint = 1;
       option<CylinderConstraint> cylinder_constraint = 2;
       option<BoxConstraint> box_constraint = 3;
       option<RelativeSphereConstraint> relative_sphere_constraint = 4;
       option<RelativeCylinderConstraint> relative_cylinder_constraint = 5;
       option<RelativeBoxConstraint> relative_box_constraint = 6;
       option<int64> entity_id_constraint = 7;
       option<uint32> component_constraint = 8;
       list<QueryConstraint> and_constraint = 9;
       list<QueryConstraint> or_constraint = 10;
   }

   type SphereConstraint {
       Coordinates center = 1;
       double radius = 2;
   }

   type CylinderConstraint {
       Coordinates center = 1;
       double radius = 2;
   }

   type BoxConstraint {
       Coordinates center = 1;
       EdgeLength edge_length = 2;
   }

   type RelativeSphereConstraint {
       double radius = 1;
   }

   type RelativeCylinderConstraint {
       double radius = 1;
   }

   type RelativeBoxConstraint {
       EdgeLength edge_length = 1;
   }

   ...
}

Code examples

A game with a minimap

In your simple game, you have three components: PlayerInfo, PlayerControls, and MinimapRepresentation.

A client-worker instance has write access authority over the PlayerControls component.

A server-worker instance has write access authority over the PlayerInfo and MinimapRepresentation components.

component PlayerInfo {
   id = 2000;

   int32 player_id = 1;
}

component PlayerControls {
   id = 2001;

   int32 input_value = 1;
}

component MinimapRepresentation {
   id = 2002;

   uint32 map_icon = 1;
   uint32 faction = 2;
}

There are two sets of data that your client-worker instance wants to receive updates about:

  • players within a 20 meter radius
  • minimap objects within a 50 meter × 50 meter box

These translate to two queries:

  • I need to render players within a 20 meter radius of my player. To do this, I want to receive Position and PlayerInfo component updates for entities with the PlayerInfo component (ID = 2000) within a 20m radius of my player’s current position.
  • I want to render the minimap for a square of 50 meters around my player. To do this, I want to receive Position and MinimapRepresentation component updates for entities with the MinimapRepresentation component (ID = 2002) within a 50 meter × 50 meter box of my player’s current position. I don’t need to receive instant updates about the minimap, so I can set a low frequency for this query.

Because you’re specifying interest for the client-worker instance, you tie the interest to the component that the client-worker instance has write access authority over: PlayerControls (ID = 2001).

In C#, the code for specifying this interest would be as follows:

var playerConstraint = new QueryConstraint() {
 andConstraint = new Collections.List<QueryConstraint>() {
   new QueryConstraint() {
     relativeSphereConstraint = new RelativeSphereConstraint(20) },
   new QueryConstraint() {
     componentConstraint = PlayerInfo.ComponentId
   }
 }
};

var minimapConstraint = new QueryConstraint() {
 andConstraint = new Collections.List<QueryConstraint>() {
   new QueryConstraint() {
     relativeBoxConstraint = new RelativeBoxConstraint(
       new EdgeLength(50, double.PositiveInfinity, 50)) },
   new QueryConstraint() {
     componentConstraint = MinimapRepresentation.ComponentId
   }
 }
};

var interestForPlayerControls = new ComponentInterest() {
 queries = new Collections.List<Query>() {
   new Query() {
     constraint = playerConstraint,
     resultComponentId = new Collections.List<uint>() {
       Position.ComponentId, PlayerInfo.ComponentId
     }
   },
   new Query() {
     constraint = minimapConstraint,
     resultComponentId = new Collections.List<uint>() {
       Position.ComponentId, MinimapRepresentation.ComponentId
     },
     frequency = 0.5
   }
 }
};

entity.Add<Interest>(new Interest.Data(
 new Collections.Map<uint, ComponentInterest>() {
   { PlayerControls.ComponentId, interestForPlayerControls }
 }));

The worker instance that has write access authority over the improbable.Interest component on an entity can make changes to the interest query during runtime. For example, you can set your project up so that if a player closes the minimap UI, this removes the minimap query from the interest query. For more information, see Sending an update.

A game with teams

This example demonstrates how a client-worker instance could observe the position of all players on their player’s team in a game.

Say your game has a Red team and a Blue team. Entities representing players have either the RedTeam or BlueTeam component, expressing which team they belong to:

component PlayerControls {
   id = 2001;

   int32 input_value = 1;
}

component RedTeam {
   id = 2004;
}

component BlueTeam {
   id = 2005;
}

In this example there is a server-worker instance that allocates the RedTeam or BlueTeam component to player entities to split them into teams.

There’s also a client-worker instance that has write access authority over a PlayerControls component on a player entity. You can use this component as the key in your Interest map.

Corresponding to this key is a single query for the Position component of all entities in the world with the same team component as the player entity. The component to query for is determined by checking which team component, if any, the player entity has. So, if the player entity has the RedTeam component, the query will check for all other entities with the RedTeam component:

var teamComponentId = uint.MaxValue;
if (playerEntity.Get<RedTeam>().HasValue) {
 teamComponentId = RedTeam.ComponentId;
} else if (playerEntity.Get<BlueTeam>().HasValue) {
 teamComponentId = BlueTeam.ComponentId;
}

if (teamComponentId == uint.MaxValue) {
 return;
}

var teamInterest = new ComponentInterest() {
   queries = new Collections.List<Query>() {
     new Query() {
       constraint = new QueryConstraint() {
         componentConstraint = teamComponentId
       },
       resultComponentId = new Collections.List<uint>() {
         Position.ComponentId
       }
     }
   }
 };

playerEntity.Add<Interest>(new Interest.Data(
 new Collections.Map<uint, ComponentInterest>() {
   { PlayerControls.ComponentId, teamInterest }
 }));


————
2019-12-10 Page updated with editorial review: Added information on frequency.
2019-11-15 Page updated with editorial review: Added tip, introduction, and diagram. Clarified wording.

Search results

Was this page helpful?

Thanks for letting us know!

Thanks for your feedback

Need more help? Ask on the forums