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:
- It is moving to a player at all times, unless there are none.
- When within
ATTACK_RANGEto a player, the entity enters theis_attackingstate. The entity plays the attack animation.
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:
is_pushed: State that determines if they are being pushed, used for changing movement related behaviour (entity collisions). Replicates on change.spawn_position: Self explanatory, only replicates once.last_hit_source_id: Used to award kill stats, replicates on change.HEALTH: Self explanatoryserver_position: The position of the entity on the host. Used for periodic syncing. Replicated periodically.closest_target_node: Used for various optimizations.
Note that thetarget_position, which is the position the entity moves towards, is batched and replicated seperately by an rpc.
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
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 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:
- Get nearest target position and node
- Set entity closest target node
- Now, if not server (host), return. The below logic is ran purely on the server side now.
- Offset the target position by a random vector, if not within close proximity to player
- Offset the target position if necessary if colliding with entities. This seperation force is calculated by querying the static body for collisions, and applying an offset.
- Add it to array that will be batched replicated after the loop.
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:

And an example of a fully working entity is almost the same, just with the addition of a rig:

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:
- Check health and emit and health events if needed.
- Set spawn position if not set yet.
- Perform syncing logic if it is time (only happens at fixed intervals). Snaps or lerps entities depending on their displacement from the server position.
- Determine optimization logic, which is primarily two things: Slowed Ticking (ticking entities at slowed down rates, and/or activating MultiSwarm). The conditions are many, some examples are: Is the entity attacking? Is the entity on the screen? (Frustrum Culling), is the entity blocked (Occlusion Culling), Is the entity far?, and is the entity inside a very crowded horde.
- Play appropriate animation (move/attack)
- Perform raycast logic for climbing or floor height.
- Calculate move speed. Differs when currently being pushed by other entities.
- Rotate to look at current target.
- Set position.
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.