Generic Netcode

Documentation Objective

Explain how to use the generic netcode library to implement your own muxer

Overview

The generic netcode is a library which provides an easy-to-use default implementation of the muxer netcode interface. The library handles the logic of sending client updates and interest management (net relevancy). Users are only required to provide definitions of the interest management policies and communication protocol. If more direct control over those aspects of the netcode is necessary the Netcode interface can be implemented directly.


Concepts

The generic_netcode uses the distance between the player and an entity to decide how often an updates for that entity should be sent to the client. This makes it possible to substantially reduce the amount of data sent to a client.

If a client has ever seen an entity, the generic_netcode ensures that if the entity dies in the simulation that the client will be notified of this. These notifications are referred to as death messages and are assumed to be representable by an entity with some property set. Whether or not this update is timely is dependent on whether the entity is within the interest range of the client. Death messages are sent in such a way to ensure that a client cannot mistakenly believe that a nearby entity is still alive when it has since died.

When entities move outside the interest management radius of a client, messages can also be sent to inform the client of this. These notifications are referred to as drop messages. A drop message informs the client that it may not receive a further update for this entity for an unspecified period of time. This functionality can be useful for clients if they want to remove the entity from rendering, or avoid disable interpolation of the entity’s movement.

Usage

Requirements on Entities

In order to perform net-relevancy, the generic netcode specifies certain requirements on the C++ type that is used to represent an entity. Specifically, if an entity type named entity_type is assumed, the following functions must be defined:

uint64_t get_entity_id(const entity_type &entity)

Given an entity, this function must return an identifier for the entity which must be a uint64_t. This identifier is used to track entities across different time-steps and must be unique across all entities active in the simulation.

std::optional<uint64_t> get_owner_id(entity_type &entity)

Given an entity, this function returns an identifier that specifies an “owner” for this entity. The generic netcode will send information to a client based on proximity to entities that clients own. Hence, this function should only return a value for entities that correspond to a camera view for a client (e.g. it should return a value for a player-controlled ship, but probably not for torpedoes fired from that ship). Non-player entities should return std::nullopt.

vec3f get_position(const entity_type &entity)

Given an entity, this function should return the position of the entity. It is valid for this function to return either a vec2f or vec3f depending on the dimensionality of the simulation space.

void synthesize_dead_entity(const uint64_t id, entity_type &entity)

Given the supplied entity which is in an undefined state, this function modifies the entity in such a way that it corresponds to id and informs the client that this entity has died and has been removed from the simulation. This function is used by the generic netcode to construct messages to inform clients that an entity no longer exists in the simulation.

void synthesize_drop_entity(entity_type &entity)

Given the supplied entity, which is a valid copy of an entity previously received from the simulation, this function modifies the entity in such a way as to notify the client that the muxer will no-longer send timely updates for that entity. This is used by the generic netcode to notify the client that an entity has moved outside the client’s interest range, and that the client may not have up-to-date information for that entity. It is possible for this function to have no effect if drop message functionality isn’t required.

bool is_entity_dropped(entity_type &entity)

Returns true if the supplied entity is marked as dead. This generic muxer can infer entity death by detecting if an entity has not been sent from the simulation for a few ticks, but may also use this function to infer entity death when explicitly signaled by the simulation.

Defining an Interest Policy

The generic netcode is parameterised by an interest policy which enables specification of the update rates for different entities. The policy is defined by the generic_interest_policy in the aether/generic-netcode/interest_policy.hh header.

Parameters are as follows:

  • scheduling_granularity_hz - determines bucket size used for entity scheduling and should not be changed from the default.
  • per_worker_metadata_frequency_hz - determines how often the information regarding the workers is sent to the clients. Usually this information is only relevant in the client for visualization and debugging purposes.
  • no_player_simulation - this should be set if the simulation has no players. In this case no interest management is performed and all entities are sent to all clients. If this flag is set to false on a simulation with no players, clients may receive no data as all as there are no owned entities with which to determine a range of interest.
  • rings - a vector of interest management rings. Each ring is a tuple of a radius, a delay in milliseconds between updates for an entity and a gradient_type. The gradient_type may be constant in which case the same update rate is used across the ring, or linear in which case the update rate will change linearly from that specified for the previous ring to the value for the current ring on the outer edge. The vector must be sorted by radius.

Definition and usage

The generic netcode is implemented by thegeneric_netcode class defined in aether/generic-netcode/generic_netcode.hhwhere each of the public functions correspond to the implementation of one of the netcode interface functions.

template<typename Marshalling>
class generic_netcode {
private:
...
public:
generic_netcode(const generic_interest_policy &policy = generic_interest_policy(),
const marshalling_type &_factory = marshalling_type());
generic_netcode(const generic_netcode&) = delete;
void new_connection(void *muxer, void *connection, uint64_t id);
void new_simulation_message(void *muxer, uint64_t worker_id, uint64_t tick, const void *data, size_t data_len);
void notify_writable(void *muxer, uint64_t id);
void drop_connection(void *muxer, uint64_t id);
};

The member functions in generic_netcode are called from binding functions that are required to link against the muxer library (libmuxer.a). These forwarding functions can be found in muxer/muxer.cc in the template project.

void *new_netcode_context() {
return new netcode();
}
void destroy_netcode_context(void *ctx) {
delete static_cast<netcode*>(ctx);
}
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);
}
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);
}
void netcode_drop_connection(void *ctx, void *muxer, uint64_t id) {
auto nc = static_cast<netcode *>(ctx);
nc->drop_connection(muxer, id);
}
void netcode_notify_alarm(void *ctx, void *muxer, uint64_t token) {
}
void netcode_notify_writable(void *ctx, void *muxer, uint64_t id) {
auto nc = static_cast<netcode *>(ctx);
nc->notify_writable(muxer, id);
}