Comment on page
Field of View
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 ticksend
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.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. |
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 |
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 ®istry,
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 ®istry,
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 ®istry,
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);
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.
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.There are two systems responsible for driving the FOV logic:
receive
processes FOV requests and responses and should be executed before cell logicsend
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);
};
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
The
construct_predicate
function is provided as a convenience:// Convenience:
construct_predicate<comp_a>([](
const ::entt::registry ®istry,
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 ®istry,
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 ®istry,
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.