Hadean Connect and netcode
How to add netcode to your simulation and use Connect to run it

Getting started

This page will build upon the simulation created on The simulation page. You can either use that page to build up your simulation, or download the completed Hello World project.
Once you have your project available, open muxer.cc in your code editor to get started.

The Connect API

Connect exposes a C API that you implement in order to handle the various events that are required to update all the connected clients with new simulation state.
Most implementations of this API delegate the work to a netcode class. The netcode is then responsible for efficiently updating and querying a representation of the simulation state to send to each client when they are ready to receive more data.

The generic netcode

To aid with quickly building your simulation, the Aether Engine SDK ships with a generic netcode. This netcode is designed around the expectation that clients want to receive updates for entities that are close to their current position more frequently than entities that are further away. If entities are too far away then this netcode will stop sending updates at all for them.
To use the generic netcode, you will need to create an instance of the generic netcode in the new_netcode_context method. This event is called when Connect starts to create a new instance of the netcode. Connect will create a new instance for each Connect thread.
#include <aether/generic-netcode/generic_netcode.hh>
#include <aether/generic-netcode/trivial_marshalling.hh>
#include <aether/generic-netcode/interest_policy.hh>
using netcode = aether::netcode::generic_netcode<marshalling_factory>;
extern "C" {
void *new_netcode_context() {
aether::netcode::generic_interest_policy interest_policy {};
interest_policy.set_has_players(true);
auto& rings = interest_policy.rings;
rings.clear();
rings.emplace_back(10.0f, 0, aether::netcode::generic_interest_policy::gradient_type::constant);
rings.emplace_back(50.0f, 500, aether::netcode::generic_interest_policy::gradient_type::linear);
return new netcode(interest_policy);
}
void destroy_netcode_context(void *ctx) { delete static_cast<netcode*>(ctx); }
// Snip...
}
As you can see, the generic netcode expects to be provided a generic_interest_policy when it is created. This interest policy allows you to tell the netcode how frequently updates should be submitted for a particular entity to a particular client. The genericinterestpolicy calculates these frequencies based on the distance between the client entity and all other entities, and compares this to the list of rings provided during initialisation.
Each ring provides a radius and desired update rate at that radius. So in the example above, if an entity is 10 units from the client entity, it should have an update sent to that client every 0ms (i.e. as fast as possible). If an entity is 50 units from a particular client entity, it should have updates sent to that client every 500ms. The rings also provide a gradient describing how the update rate should vary from the previous radius to the provided radius. In the example above our first ring is requesting a constant update rate, so any entities from 0-10 units from the client will have updates sent to that client every 0ms. The next ring asks for a linear gradient, so from 10 units to 50 units from the client the frequency between updates will vary from 0ms to 500ms linearly. As a result if an entity is 30 units from the client entity, it will have updates sent every 250ms.
If an entity is further away from the client than the outer ring, then no updates will be sent to the client for that entity.
The generic netcode has been designed to help you quickly start building a simulation and prototyping ideas. The netcode is an area that you will need to spend time tuning to your particular simulation, both to support the feature set you need and the performance you desire. To help you get started, the source code for the generic netcode is available in the Aether Engine SDK installation.
To find the generic netcode source, browse to where you installed the Aether Engine SDK Libraries, then look in the include\aether\generic-netcode folder to find the relevant files.
To modify these to your needs, simply copy them into your project and update the include paths to refer to your copies rather than the SDK installed copies.

Receiving simulation updates

The netcode cannot send updates to clients if it has not received any updates from the simulation. When an Aether Engine worker sends an update to Connect to be distributed to clients, Connect will invoke the netcode_new_simulation_message method. This allows the netcode to update its internal representation of the simulation state. When using the generic netcode this event is forwarded directly onto the generic netcode instance.
void netcode_new_simulation_message(void *ctx, void *muxer, uint64_t worker_id, uint64_t tick, const void *data, size_t data_len) {
auto nc = static_cast<netcode *>(ctx);
nc->new_simulation_message(muxer, worker_id, tick, data, data_len);
}
The provided worker_id lets the netcode know which worker this update is received for, and what tick the worker was simulating when it sent the data. Using this the netcode can ensure a consistent view of the data for clients.
One of the important steps the generic netcode does is notify Connect that it would like to receive notifications that a client is able to receive new data. This is done by calling the aether::netcode::connection_subscribe_writable method for each connection.
Without calling this method, Connect will not notify the netcode that a connection is ready to receive more messages. This is done so the netcode does not have to worry about sending redundant data to clients if it is able to push data to them faster than the simulation is sending updates to Connect.
If you need to write your own netcode, ensure that you call this method for new simulation updates.

Updating clients

When a client connects to Connect, Connect will invoke the netcode_new_connection method, allowing the netcode to initialise the connection to the client. This is a good moment to send any initial static data that the client will need to know about. When using the generic netcode this event is forwarded directly onto the generic netcode instance.
void netcode_new_connection(void *ctx, void *muxer, void *connection, uint64_t id) {
auto nc = static_cast<netcode *>(ctx);
nc->new_connection(muxer, connection, id);
}
The interaction with clients is connection based, and Connect maintains knowledge of when it is possible to send new data to clients. All messages sent are reliable and will be received in order.
When a client connection is able to receive more data, and the netcode has subscribed to be informed of this event, then Connect will invoke the netcode_notify_writable method. The netcode responds by querying its representation of the simulation state and finding all entities that are near the client entity then sending out updates as appropriate.
In practice the generic netcode actually queries the state and buckets entities based on the required update frequency, and then checks the time since the last update to see which entities should have an update sent.
When using the generic netcode, this event is forwarded directly onto the generic netcode instance.
void netcode_notify_writable(void *ctx, void *muxer, uint64_t id) {
auto nc = static_cast<netcode *>(ctx);
nc->notify_writable(muxer, id);
}
Finally when a client disconnects from Connect, Connect will invoke the netcode_drop_connection method to allow the netcode instance to clean any state being held for this connection. When using the generic netcode, this event is forwarded directly onto the generic netcode instance.
void netcode_drop_connection(void *ctx, void *muxer, uint64_t id) {
auto nc = static_cast<netcode *>(ctx);
nc->drop_connection(muxer, id);
}

Informing clients of dropped and dead entities

When entities move far enough away from a client entity, the generic netcode will stop sending updates for them. However if the netcode does not tell the client that this has happened then the client will need to keep rendering the entity in its last known state. To inform the client when this is happening, the generic netcode will send a final update for a particular entity with a flag set indicating the entity has moved out of range. To enable this you must define a couple of methods on the per_entity_data type to allow the netcode to indicate that an entity has moved out of range. When this happens, the entity is described as having been 'dropped'. To do this, we add a couple of methods to our protocol file.
protocol.hh
void synthesize_drop_entity(protocol::per_entity_data& entity) {
entity.state |= ENTITY_STATE_DROPPED;
}
bool is_entity_dropped(const protocol::per_entity_data& entity) {
return (entity.state & ENTITY_STATE_DROPPED) != 0;
}
Here we set a particular bit in the state flags for an entity to indicate that it has been dropped. Clients can call is_entity_dropped to test if a particular entity has moved out of range.
Another time a client needs to know an entity should no longer be rendered is when it has been destroyed as part of the simulation logic. Consider a rocket that has exploded - the client no longer needs to render the rocket, and should be informed that it has been removed from the simulation. In much the same way as dropped entities, the generic netcode uses a pair of methods to inform clients of entities that have been removed from the simulation entirely. These are known as dead entities.
protocol.hh
void synthesize_dead_entity(const uint64_t id, protocol::per_entity_data& entity) {
entity.state |= ENTITY_STATE_DEAD;
}
bool is_entity_dead(const protocol::per_entity_data& entity) {
return (entity.state & ENTITY_STATE_DEAD) != 0;
}
Again we set a particular bit in the state flags to indicate that this entity is now dead and has been removed from the simulation.
If an entity moves out of range of a client, and is dropped, the generic netcode will not send any further updates for that entity. This includes if that entity is then destroyed by the simulation - no dead entity message will be sent. As a result clients can typically treat these events as being interchangeable.
Now that the netcode is setup and running, we can connect a client to it to interact with our simulation.