Sites

Menu

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

As well as setting up interest, you must set up the static component filter correctly. This is a whitelist that takes precedence over CBI. You can use the dynamic component filter to override the static component filter during runtime.

For more information, see 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
  • 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 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.

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

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

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

Search results

Was this page helpful?

Thanks for letting us know!

Thanks for your feedback

Need more help? Ask on the forums