Step 5 - Netcode

Overview

We have almost reached a point where the demo is built, but we now want to include additional customisation to the netcode shipped with the demo.

Enabling customisation lets you optimise network bandwidth for your particular game or prototype. In this case it is in relation to the Aether Hunt 2 demo.

What we will demonstrate by the end of this step is how Simulate manages NPCs within cells (including the resources required), and how to optimise network usage during monitoring and debugging by enabling and disabling net relevancy.

Setup

Get the Source

Navigate to the AetherHunt2 demo folder

There should be a local Git repo in there. All the steps of this tutorial are included as tags on the master branch of this repo.

Check out the code for this step by running the following command git checkout step-5. This will leave the code as it was at the end of the previous step. If you wish to check out the completed state of this section run git checkout step-5-complete. Run git diff step-5 step-5-complete to inspect the diff.

Build and Run the simulation

  1. Check out “step-5” from the git repo in the example folder.

  2. Open PowerShell inside the simulation folder

  1. Build the project with cmake -B build -G Ninja; ninja -C ./build

  1. Run the simulation with aether run

Build and run the client:

  1. To run the client, open Aether_Hunt.uproject

    1. If you wish to debug the unreal client you can open the Visual Studio solution file and run debug mode from there. Caution this takes more resources

  2. Click build to build the project

  3. Click play to start the client

You will then enter the world as a player.

Press the <TAB> button to turn on cell debug view and <right click> to zoom out.

Each of the boxes represents a cell of the world and shows the power of distributed spatial simulations within Simulate. The cells will increase or decrease in size as required to manage the increased demand of resources like CPU or memory.

The quadtree data structure means a cell requires less resources as entities move out, and can in turn cover more space.

Exit game and stop the simulation:

  1. Hit the <esc> to exit the game

  2. Back on the PowerShell where the simulation is running

    1. Press Ctrl+c

    2. Press N to stop the Simulate simulation

How-To Guidelines

At the moment Connect will send all data to the players. This is sub-optimal for bandwidth usage and handling a large number of players. The topics covered below are:

  1. Limiting the data sent to each player by regularly sending information as it updates from the closest entities, doing so less frequently for further-away entities and not doing so at all for the ones furthest away.

  2. Enabling cell debug view to view all entities and data for testing and toggle on/off net relevancy.

Limiting Data Sent to Each Player

Each player currently receives all data, which is impractical due to bandwidth usage..

netcode.hh

The netcode implementation in the demo uses the interest_policy struct to define when an update should be sent to a player. The generic-netcode implementation 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.

evaluate is called when the netcode decides if the update for an entity should be sent to the player - it receives the distance to the player and the time point at which the last update was sent and returns the next time at which next one needs to be.

get_cut_off returns the cut-off distance, beyond which entities will not receive updates. The client will be notified of this by marking the entity with the ENTITY_DROPPED_TEMP flag, so the client can remove those that are out of range.

net_relevancy_disabled is the static constructor function which creates an interest policy object with a cut-off at infinity (no cut-off point), meaning updates are never throttled and act as if net relevancy was disabled.

struct interest_policy {
    template<typename TimePoint>
    std::optional<TimePoint> evaluate(const TimePoint &last_sent, const float distance) const;
    float get_cut_off() const;
    static interest_policy net_relevancy_disabled() {
        rings_type rings;
        rings.emplace_back(std::numeric_limits<float>::infinity(), 0, gradient_type::constant);
        return interest_policy{rings};
    }
    enum class gradient_type {
        constant,
        linear,
    };
    using rings_type = std::vector<std::tuple<float, std::chrono::milliseconds, gradient_type>>;
    rings_type rings;
    interest_policy(rings_type _rings) : rings(_rings) {
    }
};

First, we add a static constructor function that will define the ranges at which the updates get throttled, just below the net_relevancy_disabled definition.

We define entities within 100 metres of the player receive an update every simulation tick. Entities within the 100-200m range will receive an update every 0.5 seconds, entities within 200-300m range will receive an update every one second, and entities beyond 300m will not receive updates until either the player or the entity itself moves closer. gradient_type::linear makes the update frequency decrease linearly with the distance so entities at 250m away will receive an update every 750ms.

+  static interest_policy net_relevancy_enabled() {
+       rings_type rings;
+       rings.emplace_back(100.0f, 0, gradient_type::constant);
+       rings.emplace_back(200.0f, 500, gradient_type::linear);
+       rings.emplace_back(300.0f, 1000, gradient_type::linear);
+       return interest_policy{rings};
+   }

netcode.cc

Now that we have the net relevancy defined, let’s enable it as default for all the players by changing the connection_state constructor.

connection_state::connection_state(void *_conn_ctx, entity_store<entity_type> &store)
    : conn_ctx(_conn_ctx)
    , created(clock_type::now())
+   , interest_policy(interest_policy::net_relevancy_enabled()) // change this to net_relevancy_enabled
    , drop_entities_spatial(store) {
    player_id = aether::netcode::connection_get_player_id(conn_ctx);
}

Now let’s build the project as detailed in the setup section and use the debug view to see the changes in action. In order to get to debug view press \<TAB> and then \<right click> to zoom out. In this view:

  1. Red dots are NPCs - in this case Orcs

  2. Squares represent players

  3. The boxes show cells managing a space within the spatial simulation

    1. Colours are randomised for ease of visibility

As you can see, the densely populated areas on the top of the screen and on the left don’t have any entities visible - this means that we’ve changed the net relevancy policy successfully!

Compare that with a view you’d get with the net relevancy off:

Disable Net Relevancy for the Debug View

There’s an unfortunate side-effect to our net-relevancy changes from the previous step - we can no longer see the entire simulation in the debug view. Ideally, we would still receive all of the data while in the debug view for monitoring purposes. This can be implemented easily by switching the player’s net relevancy on and off while going in and out of the debug view.

The code provided with the demo is already equipped to handle most of this process:

  • The Unreal client sends an event of type EVENT_UPDATE_CONNECTION_FLAGS to the simulation with EVENT_CONNECT_ALWAYS_RELEVANT toggled whenever we switch to/from the debug view

  • The simulation then sends this data to the muxer in the player

  • Then finally, the muxer netcode can take the data from the simulation and switch the net relevancy

netcode.cc

Here, the netcode is notified about the changes to the player object in the connection_state::update_player method.

void connection_state::update_player(void* muxer, const AetherHuntProtocol::player& player, uint64_t tick) {
    if (!player_info.has_value()) {
        aether::netcode::connection_subscribe_writable(conn_ctx, muxer, true);
    }
    player_info = player;
    player_tick = tick;
}

We want to check if the net relevancy is changed, and if it is, we want to assign the interest_policy field to either interest_policy::net_relevancy_disabled() or interest_policy::net_relevancy_enabled(), depending on the flag received from the client.

The following code will achieve this:

 void connection_state::update_player(void* muxer, const AetherHuntProtocol::player& player, uint64_t tick) {
+   bool update_net_relevancy = false;
     if (!player_info.has_value()) {
         aether::netcode::connection_subscribe_writable(conn_ctx, muxer, true);
+         update_net_relevancy = true;
+     } else {
+         update_net_relevancy = player_info.value().connection_flags != player.connection_flags;
+     }
     player_info = player;
     player_tick = tick;
+     if (update_net_relevancy) {
+         interest_policy = (player.connection_flags & AetherHuntProtocol::EVENT_CONNECT_ALWAYS_RELEVANT) ? interest_policy::net_relevancy_disabled() : interest_policy::net_relevancy_enabled();
+     }
 }

Build and Run the simulation

Now that we’ve made the changes, let’s run the simulation again and check the results.

To build and run the simulation open PowerShell inside the simulation folder.

  1. Build the project with cmake -B build -G Ninja; ninja -C ./build

  1. Run the simulation with aether run

Build and run the client:

  1. To run the client, open Aether_Hunt.uproject

    1. If you wish to debug the unreal client you can open the Visual Studio solution file and run debug mode from there. Caution this takes more resources

  2. Click build to build the project

  3. Click play to start the client

With the debug view enabled we can see the orcs at the back of the castle (using the zoomed-out view you can see orcs at the bottom of the valley):

With the debug view disabled we can no longer see the orcs there:

Success! We’ve managed to dynamically disable the net-relevancy for the debugging purposes!

Exit the game and stop the simulation:

  1. Hit \<esc> to exit the game client

  2. Back on the PowerShell where the simulation is running

    1. Press Ctrl+c

    2. Press N to stop the Aether Engine simulation

Summary

In this section, we looked at the custom netcode written for this demo which runs on the muxers. It uses rings of various radii to gradually taper the update, sending rates of entities from the simulation to a given game client, depending on the player position. This helps to put a ceiling on bandwidth by sending more important data more frequently. We also showed how to render the cells on the client for debugging purposes, and how to dynamically alter net relevancy from the game client, so we can view all entities at the same time.

Conclusion

That concludes the Aether Hunt 2 demo. We hope you have enjoyed the walkthrough and building your own simulation. Full Simulate documentation can be found here.

Last updated