Devloping A High-Volume Multiplayer NPC System

Note: This page does not discuss the rendering optimization of an individual entity, but rather how a ton of entities can be made multiplayer compatible.

What's an Entity?

An entity in the context of my projects is basically a character. Something with an animated rig, that can move and attack. In my project, entities are fully rigged and animated, and can do two simple functions: move, and attack. Entities always move towards the closest target and will climb any obstacle in the way, no matter how tall or steep. Entities have two ways to attack. They either attack within a radius infront of them (like a melee attack), or they can cast spells, which is specific to the project I'm working on. In godot, animations are pretty expensive, so they are culled in some odd ways. See Optimizing Mass Rendering.

The way an entity determines its states is simple:

That is the scope of an entity, which is pretty basic, but the real challenges come with making them efficient in the hundreds to thousands.

Replicating NPC's in Multiplayer.

What's being replicated

At its core, replication is the synchronization of data between all peers in a network. In the context of TaikaSteam's entities, entities have three variables replicated via synchronizers:

High Volume Entities

The Issue

Realistically above, the only thing that would have to be updated very frequently is the actual position of an entity. This could be done at 60hz (60 times per second), and it'd work for very small amount of entities, but for something like 500 entities, that means 500 entities updating at 60 times per second, which is 50060=30,000 times per second. That's a lot.

The Solution

There is no one solution that fits all cases, so this specific implementation works best (and maybe only works) for TaikaSteam. The details below are not the most efficient, but balance efficiency without complicating the developer experience (does not impede creating new enemies and workflow)

Swarm

Swarm tries to be a general purpose all-encompassing entity system made up of 4 modules. Swarm aims to streamline the creation of entities by providing a robust template where creating an entity is as simple as importing a rig from blender and setting some exported variables, while the other modules handle all behaviour, replication, and logic. The four modules are: SwarmEntity, SwarmNexus, SwarmDirector, and MultiSwarm.

Swarm Director

The director is a global singleton class. The director is responsible for all Swarm Entity server logic. Entities are created via a function from the director, and are registered into an array. At fixed configurable intervals, the director peforms a big loop across a fixed number of entities.

At a rate of X times per second, the director loops through a max Y amount of entities and performs the following logic:

Swarm Entity

The Swarm Entity is the middleman between the server and visual replication on the clients. The class is simply a container to hold data used by the other modules that is unique to each entity. It holds only basic utility functions and is not ticked (processing is disabled). All entity parameters are located here. The general structure of the scene goes as:
{3A0F52EB-23B7-4DF2-9029-5A26D84E120D}.png
And an example of a fully working entity is almost the same, just with the addition of a rig:
{72F7661D-D5E3-4199-9C9C-83EFC5AD4C71}.png

SwarmNexus

The Swarm Nexus is responsible for all entity client logic (visuals). It is also a global singleton class like the SwarmDirector. Entities register themselves to the class inside their _ready() function.

It follows a similar iteration pattern as the director, and performs the following logic per tick:

MultiSwarm

MutliSwarm is automatically compatible with every Swarm Entity and is turned on by default. It is simple, but extremely powerful when the GPU is the bottleneck. When entities are not animated (when they are far enough) they are no longer rendered as a seperate entity and are batched along with other non animated entities to be all drawn at once by a MultiMeshIntance3D. This allows for really big performance improvements. The only downside is that entities cannot be animated (unless via VAT, but that is not used due to too much work). As an alternative, MultiSwarm has configurable bobbing animations via shader built in.