Query-based interest (QBI) (beta)
Query-based interest (QBI) is a way of specifying the entity components that worker types or instances want 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.
QBI is a more precise, granular alternative to chunk-based interest. You set it up using a component, improbable.Interest
, on each entity. This component contains a list of component IDs that map to queries. 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.
You set up these queries based on the updates that a worker instance needs to receive about entities in the world that it doesn’t have write access authority over, but that may affect the entities it does have write access authority over.
Each query needs:
- a
QueryConstraint
, which can contain any combination of constraints. - 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.
Component filters
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 something like this:
health
- Constraint: relative sphere constraint AND
weapon
component constraint - Result type:
weapon
component
- Constraint: relative sphere constraint AND
player
- Constraint:
player
component constraint - Result type:
position
component
- Constraint:
This means that:
- whichever worker instance has write access authority over the entity’s
health
component will have interest inweapon
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 theposition
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 query-based interest
To enable query-based interest for an entity, add the standard schema library component improbable.Interest
(see schema definition) to that entity. You use this to specify a list of component IDs that map to 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.
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 constraints, but you can compose multiple constraints using the and_constraint
and or_constraint
fields.
The available constraints are listed below.
type ComponentInterest {
type Query {
QueryConstraint constraint = 1;
// Either full_snapshot_result or a list of result_component_id
// should be provided. Providing both is invalid.
option<bool> full_snapshot_result = 2;
list<uint32> result_component_id = 3;
}
type QueryConstraint {
// Only one constraint should be provided. 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
andPlayerInfo
component updates for entities with thePlayerInfo
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
andMinimapRepresentation
component updates for entities with theMinimapRepresentation
component (ID =2002
) within a 50 meter × 50 meter box of my player’s current position.
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
}
}
}
};
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 }
}));