Set up query-based interest

GDK for Unreal

If you're using the GDK for Unreal, this page does not apply to you. See the GDK documentation on game client interest management.

Introduction

To enable query-based interest (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.

Relative constraints have float precision instead of double. At the edge of a 2000 x 2000 km map, this results in a 6 cm imprecision (approximately).

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

   ...
}

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; latency jitter 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.

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

2020-05-01 Page created without editorial review: Moved in-depth information from parent page onto this page.

Updated about a year ago


Set up query-based interest


Suggested Edits are limited on API Reference Pages

You can only suggest edits to Markdown body content, but not to the API spec.