Links
Comment on page

Field of View

Introduction

Simulate comes with field of view (FOV) functionality that enables us to inspect regions inside the simulation. Entities wanting to use this capability should acquire the FOV component fov_entt_c which contains some properties that users should configure. Based on these, the engine generates FOV requests that are delivered to and processed by cells responsible for hosting the region of interest. These target cells generate responses which are then coalesced and the final information is made available via one of the components.
The engine also provides two FOV systems (we are adhering to the ECS model):
  • receive which should be executed by the user at the start of the cell tick
  • send which should be executed near the end
FOV functionality can be accessed via a single header: aether/fov.hh
Note: This feature is only compatible with EnTT (aether/entt.hh). If you are not using EnTT the same functionality can be achieved using spatial messages.

FOV component

template <size_t Dimension, typename... PredicateComponents>
struct fov_entt_c {
using vector_t = typename aether::vec<Dimension, float>;
using shape_t = typename aether::message::inside_shape<vector_type>::shape_type;
using predicate_t = typename predicate_t<PredicateComponents...>::inner;
// Configured by user:
uint64_t id;
shape_t shape;
std::optional<predicate_t> predicate;
refresh_updates refresh_updates;
// Managed by engine:
std::vector<::entt::entity> relevant;
};
The component type is instantiated based on template parameters:
  • dimension - determines the set of compatible shapes e.g. circle, rectangle, and trapezoid under 2D
  • predicate input components - 0 or more components used to determine the predicate type
Users not interested in predicates should use one of fov_entt_c<2> or fov_entt_c<3>.
Description of each property:
Property
Description
id
Unique identifier assigned to each FOV component by the user.
shape
Shape used when searching for entities.
predicate
Predicate used when searching for entities (optional).
refresh_updates
Controls how often FOV requests are generated.
relevant
Set of visible entities maintained by the engine.

Shape

The shape is used to determine the set of target cells for each FOV request. Target cells are those which are (at least partially) covered by the shape. The predicate (discussed later) is executed for each entity found inside the shape. It is safe to update the shape property of the FOV component during runtime.
The dimension count specified when instantiating the FOV component determines shape_t:
  • std::variant<geometry::circle, geometry::rectangle, geometry::trapezoid> (2D)
  • std::variant<geometry::sphere, geometry::cuboid, geometry::frustum> (3D)
The 2D shape list is in alphabetical order, while the 3D shape list mirrors its 2D counterpart. Shapes are based on the spatial messaging API. Please consult relevant documentation.
Shape (2D)
Parameters
Circle
origin, radius
Rectangle
origin, rotation, width, length
Trapezoid
origin, rotation, view_angle, near_distance, far_distance
Shape (3D)
Parameters
Sphere
origin, radius
Cuboid
origin, rotation, width, length, height
Frustum
origin, rotation, view_angle, near_distance, far_distance, aspect_ratio

Predicate

Predicates let us define the logic for deciding which entities (and which of their components) are included with the response. The predicate type is instantiated based on an input component list (template parameter). In order to be used, a predicate’s type must match predicate_t found inside the FOV component. Predicates can be safely updated during runtime.
This is an optional feature. In order to disable it, just use fov_entt_c<2> or fov_entt_c<3> (i.e. no input components have been specified). Otherwise, specify at least one input component. Please see the section on input components for detail.
Parameters
Predicates are executed once for each entity found inside the shape. With each invocation, the engine provides access to some state. The predicate signature needs to adhere to the following layout:
Parameter
Description
const ::entt::registry&
The EnTT registry of the current cell passed as const reference. Users can access anything including information associated with other entities.
const ::entt::entity
The current entity ID.
const comp_1&, ..., const comp_n&
One const reference for each input component (template parameter), appearing in the same order as the input component list.
These references are merely provided as a convenience. Since users have read access to the registry, they can inspect any component belonging to any entity within the current cell.
[const] state_1, ..., [const] state_n
One parameter per discrete user state. Each parameter can be const but cannot be a reference (please read section on user state references). Passing user state is completely optional.
Input components
Predicates are used to select entities based on the information stored inside of their components. Consider the scenario where a strict subset of our entities is assigned an additional component: color_c. Our predicate examines this component in order to make a decision, and it would be undefined behaviour to process an entity without color_c. Input components refers to the set of components that each entity must have in order to be considered.
When no input component is specified, the predicate type evaluates to null_predicate_t and users should not create instances of this type (set predicate property to std::nullopt). In the general case (non-empty set), the predicate type evaluates to predicate<PredicateComponents...>.
The engine provides the construct_predicate utility function:
predicate<comp_1> example = construct_predicate<comp_1>([](
const ::entt::registry &registry,
const ::entt::entity entity,
const comp_1 &obj
) -> selection {
return select_components<everything>{};
});
Entity ID
Components
1
comp_1
2
comp_2
3
comp_1, comp_3
Consider a cell with three entities (above) running a predicate (also above). Entities 1 and 3 both have at least comp_1 and are thus included with the response, while entity 2 is discarded since it does not have comp_1.
Our predicate returns an object of a special type: select_components<everything>. This tells the engine to include every component assigned to the entity. In this case, both comp_1 and comp_3 will be included with the response for entity 3. The selection type is quite powerful and allows us to return different combinations of component (as discussed in the next section).
Output components
The predicate system allows us to select which components to include with the FOV response. The input and output component sets are completely independent. The same predicate can contain control paths returning different component selections. This means, we can return a different set of component per entity depending on the decisions made. This gives us the ability to only include the information we’re interested in and opportunity to optimise network performance.
Each predicate returns an object of the same type: selection. A single instance keeps track of the components selected for a single entity. You can return special objects:
  • select_components<everything>{}
  • select_components<C1, C2, C3, ..., CN>{}
  • select_components<void>{}
We have already discussed <everything> which selects each component assigned to the current component. We can also specify the exact components to return using <C1, C2, C3, ..., CN>{}. The set of output components doesn't need to be a subset of components assigned the current entity (engine serializes as much as possible), but it cannot be empty. We can tell the engine to discard the current entity by returning <void>.
User state
In order to make decisions, predicates tend to rely on information contained inside the current cell’s registry. Occasionally, we may wish to provide additional information to the predicate. This can be done using an optional feature known as user state. This simply involves adding extra parameters to the predicate.
Consider we have a predicate representing the logic for radars with dynamic strength which can be any value in range [0, 100]. The strength can change during runtime. It would be impractical to create a predicate per strength value. Instead, an extra parameter can be appended to the existing parameters (registry, entity ID, components..., strength).
Only the current cell’s registry can be accessed by the predicate. The entity responsible for initiating the FOV request may reside in a different cell in which case it is simply inaccessible. Any state necessary for the execution of the predicate (e.g. radar_c::strength) must be explicitly copied to each target cell.
auto predicate = construct_predicate<component_x>([](
const ::entt::registry &registry,
const ::entt::entity entity,
const comp_x&,
const size_t state
) -> selection {
if (state == 343) // ...
else // ...
}, static_cast<size_t>(1));
Let’s look at an example (above). This predicate accepts size_t as additional state which can be used inside the predicate to make decisions. When constructing the predicate, we provide a value for the state parameters. It is important that the type of the parameter matches the type of the value. In this case, the type of our literal is int which needs to be converted to size_t.
Warning: Predicates use std::placeholder underneath and there is a limit to the maximum number of parameters. This is defined by the number of placeholders in your C++ implementation. But also note that predicates work more efficiently when used with few components (see section on optimisation). Later in this guide, we examine what's happening under the hood when constructing predicates.
The lambda containing the logic can be constructed separately which makes it convenient to construct stateful predicates on the fly:
auto radar_logic = [](
const ::entt::registry &registry,
const ::entt::entity entity,
const camoflauge_c&,
const material_c&,
const mesh_c&,
const size_t strength
) -> selection {
// ...
};
auto actual_predicate = construct_predicate<camoflauge_c, material_c, mesh_c>(
radar_logic, current_entity.strength);

Update frequency

The refresh_updates property is used to control the frequency at which the relevant entity list is updated (more specifically, control the rate of request generation). Relevant entities are discussed in the next section. For now, let’s examine properties we can configure:
struct refresh_updates {
uint8_t frequency;
uint64_t start_tick;
};
frequency controls how often FOV requests are generated, but the engine also checks start_tick when making a decision. Setting the frequency to 0 effectively disables FOV for this particular entity. Setting it to 1 means the engine will update the relevant entity list each tick (default). Otherwise, a request is generated if current_tick - start_tick is divisible by frequency. If the frequency is greater than 1, the relevant entity list gets wiped until a response is ready.
FOV delay
It takes two ticks to process an FOV request due to the design of our FOV systems. If a request is generated in tick 0 then the relevant entity list is updated in tick 2.

Relevant entities

Entities that are inside the shape and pass the predicate are known as relevant entities. When the FOV response is ready, the relevant_entities property of the component is populated with EnTT IDs. Each ID represents a single relevant entity that can be found inside the registry of the current cell (hosting the entity using FOV). Components selected by the predicate are also available.
The FOV shape can contain the cell in which the entity responsible for initiating the FOV request is currently residing, but it can also contain other distant cells. This means any relevant entity belonging to a distant cell must be copied to the current cell's registry. Such entities are tagged as ghosts.
Ghost entity is a core concept of the engine and not something specific to FOV. These entities are used to represent distant entities and are stored in the same registry as local entities. Ghost entities are removed at the end of each tick since any information they represent becomes stale afterwards (engine inserts new ghosts at the start of the tick). This also applies to ghost entities generated by the FOV system. As a consequence, the engine will also clear relevant_entities to avoid dangling pointers.
This can be of consequence when the update frequency is greater than 1. The solution is to cache any information we may require during the absence of a populated relevant entity list. This is best demonstrated using an example:
We have an entity using FOV with refresh_updates set to { frequency: 5, start_tick: 0 }. This means an FOV request is genrated during ticks 0, 5, 10, and so on. Due to the FOV delay, we receive responses during ticks 2, 7, 12, and so on. This particular entity is a boss NPC tracking a very large group of players spread across cells. The predicate returns information such as player health and abilities. Once information has been coalesced, the boss entity decides whether or not it should initiate an attack. The decision itself is a single Boolean value which can be cached. An appropriate place would be a cache_c component assigned to this entity.
Note that the relevant list may be empty either because no response was produced during the tick or because there are actually no relevant entities. We can use refresh_updates.frequency and refresh_updates.start_tick to remove any ambiguity.

FOV systems

There are two systems responsible for driving the FOV logic:
  • receive processes FOV requests and responses and should be executed before cell logic
  • send generates FOV requests and should be executed after cell logic
The send system requires a single template parameter:
  • FOV - the FOV component type
The receive system requires two additional template parameters:
  • unique ID component - a component which uniquely identifies each entity and implements bool operator==(const self_t& other) const
  • interesting components - subset of the components (wrapped in std::tuple) used in the simulation, only these components are copied across cells, this set must be a superset of the predicate output components
When using multiple FOV types, it is important to remember that we need to construct and execute receive and send systems per FOV type. If our FOV type is fov_entt_c<2, player_c>, potential system types are:
  • receive<fov_entt_c<2, player_c>, unique_c, std::tuple<player_c, position_c>>
  • send<fov_entt_c<2, player_c>>
Here, we are using a predicate with a single input component: player_c. We cannot determine the exact set of output components based on the type alone. However, it must be a subset of { player_c, position_c }. This means our predicate must return one of:
  • select_components<everything>
  • select_components<player_c>
  • select_components<position_c>
  • select_components<player_c, position_c> (order of components does not matter)
  • select_components<void>
When using select_components<everything>, this means serialize each interesting component that is also attached to the current entity. So in this particular case, returning everything is equivalent to returning select_components<player_c, position_c>.
The systems are executed inside the cell_tick member function:
aether::fov::systems::entt::receive<fov, unique_id, relevant_components>{}
.operator()(aether_state, store, incoming_message_buffer, outgoing_message_buffer, get_agent_position);
// ...
aether::fov::systems::entt::send<fov>{}
.operator()(aether_state, store, outgoing_message_buffer);
Executing these systems is similar to executing other systems so the information is not repeated here. The only interesting thing over here is the get_agent_position argument. This is a closure which accepts aether_state_type and agent_reference (these types are automatically available inside the user cell state implementation), and returns the position of the specified agent. Here is a possible implementation:
auto get_agent_position = [&](const aether_state_type &aether_state, agent_reference entity) {
return agent_center(aether_state, entity);
};

Miscellaneous

Optimisation

Here are some things to consider:
  • shapes are used to determine the target cells to which requests are forwarded, and using a constrained shape should result in less communication and processing overhead
  • predicates are used to control the amount of information returned from each cell and this can help with network performance, but also
    • predicates take advantage of EnTT views so similar performance considerations should apply e.g. minimising the number of input components
    • avoid using select_components<everything> and only return what's necessary
    • it may help if the control paths return similar component selections (memory access pattern)
    • only include user state necessary for making decisions
    • the predicate can be used as an opportunity to distribute computation across cells
  • update frequency should be used when it is not necessary to access information each tick
    • disable FOV when not in use by setting the update frequency to 0
    • generate less frequent requests and cache any result
    • start tick can be used to coordinate FOV requests in order to minimise the risk of multiple requests being generated during the same cell tick (two FOV components with frequencies set to 3 and 5 will both produce a request during tick 15)
  • batch requests by nominating some entities per cell to handle FOV and responses can be shared with other entities in the same cell
    • for individual cells, the percentage of entities using FOV should be kept to a minimum since this increases the load on each cell

User state references

The construct_predicate function is provided as a convenience:
// Convenience:
construct_predicate<comp_a>([](
const ::entt::registry &registry,
const ::entt::entity entity,
const comp_a&,
const size_t state
) -> selection {
return select_components<everything>{};
},
static_cast<size_t>(1));
// Under the hood:
predicate<comp_a>([](
const ::entt::registry &registry,
const ::entt::entity entity,
const comp_a &a,
const size_t state
) -> selection {
return select_components<everything>{};
},
std::placeholders::_1,
std::placeholders::_2,
std::placeholders::_3,
static_cast<size_t>(1));
When using construct_predicate, the user state parameters can be of type T or const T. It doesn't make sense to use T& because individual invocations of the predicate should not interact with each other. Even if relying on this behaviour provided some sort of interesting functionality, it would be difficult to reason about it since predicates may be executing on different cells. But of course, it does makes sense to use const T&. Unfortunately, this not possible not possible due to the current implementation of construct_predicate (there is a ticket to fix this).
You can use const T& if you don't mind directly interacting with aether::fov::predicate. This type is defined as:
template <typename... Components>
using predicate = aether::closure<selection(
const ::entt::registry &,
const ::entt::entity,
const Components &...)>;
The convenience function is provided so that users shouldn't have to worry about aether::closure. Explaining the usage of this type is outside of the scope of this guide. Instead, here is a working example:
std::optional<predicate<comp_a>> predicate = predicate<comp_a>([](
const ::entt::registry &registry,
const ::entt::entity entity,
const comp_a &a,
const size_t &state // const T&
) -> selection {
return select_components<everything>{};
},
std::placeholders::_1,
std::placeholders::_2,
std::placeholders::_3,
static_cast<size_t>(1));
Warning: Using the direct method means user state can be of type T& but this results in undefined behaviour in the context of FOV predicates.