Worked Example
Spatial messaging is a fundamental feature of Simulate, in which messages are sent from workers to workers based on the location of those workers in space. However, this is an over-approximation: the workers that receive a message will more than likely cover an area greater than just the area to which the message is relevant. Consequently, your user code must perform further filtering on the worker as part of its work to make sure any messages are matched up with the entities that are interested in them. This is a tutorial-style introduction to writing that code.
For the purposes of this tutorial, we will assume your simulation is based on EnTT. However, similar functionality can be implemented for any cell state with a notion of entity, including the Simulate Native ECS.
Simulate provides a message inbox and a message outbox for the worker on the
aether::entity_based_user_state_base
class, from which your cell state should inherit. In this tutorial we will implement a simple per-entity message-queue component and a system that dispatches spatial messages to the entities that fall within the destination area of the message by pushing them into the entity’s message queue. In a real application, such a system might read the message and immediately update the entity, which would be less wasteful of memory, but here we can imagine that a later system reads the messages off the queue and performs any associated updates.First, let’s define our message queue component. We can implement this as a simple struct that contains a
std::deque
of messages. We use the Simulate agent_ prefix
convention to indicate a component. Let’s fix the payload of our messages as a simple uint8_t
indicating how many entities are in the area.struct volume {
aether::vec3f center;
float radius;
};
struct volume_data {
std::uint8_t number_of_entities;
};
struct agent_message_queue {
std::deque<std::pair<volume, volume_data>> messages{};
};
We'll also need an
agent_position
component to keep track of where the entities are.struct agent_position {
aether::vec3f position;
};
Let's assume we have a
cell_state
type for our cell state, which extends aether::entity_based_user_state_base
with an Simulate-compatible EnTT store called store.class cell_state : aether::entity_based_user_state_base {
aether::entt::store store;
public:
void initialise_world(const aether_state_type &) override;
void cell_tick(const aether_state_type &, float delta_time) override;
// ...
};
The
initialise_world
function is part of the Simulate contract for a cell state type, and we will use it to create ENTITY_COUNT
entities, to each of which we will attach a message queue to function as a target for our message-delivery system. We’ll also attach a standard agent_position
component so we know where they all are, though we’ll just put them at the origin for now — you might want to write another system that updates their locations later.void cell_state::initialise_world(const aether_state_type & aether_state) {
for (std::size_t i = 0; i < ENTITY_COUNT; ++i) {
entt::entity entity = store.registry.create();
store.registry.emplace<agent_message_queue>(entity, {});
store.registry.emplace<agent_position>(entity, {0, 0, 0});
}
}
Simulate provides a message inbox to the user, which is attached to
aether::entity_based_user_state_base
under the name incoming_message_buffer
. As part of our cell_tick
, we need to read from the buffer. This is done by calling the get_message_reader
member function to get a reader; said reader can then be asked to get_next (message)
, which returns a std::optional<aether::message::message>
, and the resulting message, if any, can be decoded to a POD type using aether::message::message::payload_as_pod
. Get_area_around returns an instance of aether::message::area_around
. If we find an area_around
on the message, we iterate over all our entities with a position and a message queue, and if the position falls within the sphere we push the message onto the back of the entity’s message queue.void cell_state::cell_tick(const aether_state_type &, float delta_time) {
// create a reader
auto reader = incoming_message_buffer.get_reader();
// iterate over all messages in the incoming message buffer
while (auto message = reader.get_next()) {
aether::message::message message = std::move(message.value());
// check whether the message has an associated area_around
if (auto area_around = message.get_area_around<vec3f>()) {
aether::message::area_around area_around
= std::move(area_around.value());
auto center = area_around.getCenter();
auto radius = area_around.getRadius();
// iterate over all entities with a position and message queue
store.registry.view<agent_position, agent_message_queue>().each(
[&] (agent_position & position, agent_message_queue & message_queue) {
// check whether the entity is contained within our sphere
if ((position.position - center).norm() <= radius) {
volume_data data;
// try to decode the message, and on success put it on the back
// of the entity's message queue, tagged with its associated
// volume
if (message.payload_as_pod(data))
message_queue.emplace_back(volume{center, radius}, data);
}
});
} else {
// process other message types
}
}
// ... perform simulation work ...
}
This algorithm is of course suboptimal, being in O(n²), and in cases of high traffic it would be better to use a data structure more optimized for spatial queries, such as an R-tree.
Now we can receive spatial messages and process them, we can send them as part of our
cell_tick
. Let’s say that every tick we want to send a message informing every entity within a sphere of radius 5.0f
about the origin that there are twelve entities present. All we have to do is set the writer's destination to an instance of aether::message::area_around
with set_destination
, we can now send spatial messages the same way we would send any other message. Our data is POD, so we can just push it with push_bytes
. We call writer.send()
when we’re done to indicate that we have written a whole message.void cell_state::cell_tick(const aether_state_type &, float delta_time) {
// ...
auto writer = outgoing_message_buffer.get_writer();
volume_data data;
data.number_of_entities = 12;
writer.set_destination(aether::message::area_around{{0, 0, 0}, 5.0f});
writer.push_bytes(&data);
writer.send();
// ...
}
Always use this approach when dealing with sending messages between entities that are within the same cell, since it doesn't involve a network request.
Last modified 1yr ago