Humanoid/Enemy Optimisation

Best way to handle large amounts of NPC units? Humanoids vs custom movement

I’m currently working on a MOBA-style game and I’m trying to work out the best architecture for handling a fairly large number of minions/units at once.

At the moment my units use Humanoids and mainly move through Humanoid:MoveTo(). Each unit has its own target, aggro state, attack cooldowns, lane/path information, etc.

The actual combat logic is contained inside ModuleScripts, so I don’t have a separate attack Script inside every NPC, but each unit still obviously has its own state and needs to be updated.

With smaller amounts of units everything is fine, but once a lot of minions are active and moving/fighting at the same time I start getting things like:

  • Slight movement jitter/stuttering
  • Units bunching together
  • Movement becoming less smooth during fights
  • General performance getting noticeably worse as the unit count increases

There can potentially be dozens of units active at once, and ideally I’d like the system to comfortably handle 100+ if possible.

My current setup is roughly:

Server

  • Handles unit AI/state
  • Target acquisition
  • Attack logic/damage
  • Lane movement
  • Pathfinding when required
  • Calls Humanoid:MoveTo() for movement

Modules

  • Attack/combat manager
  • Targeting logic
  • Other shared unit behaviour

Each unit still has its own state table, cooldowns, current target, etc.

I’m mainly wondering how people normally approach this in Roblox games with lots of NPCs, especially RTS/MOBA/tower defence games.

Is using a Humanoid for every unit actually reasonable at this scale, or would it be better to move towards something like AnimationController + custom movement?

I’ve also been considering:

  • Moving NPC root parts manually instead of using Humanoid:MoveTo()
  • Disabling most Humanoid states
  • Setting server network ownership with SetNetworkOwner(nil)
  • Having one central unit manager update every NPC rather than individual loops/connections
  • Updating AI at something like 5-10hz instead of every frame
  • Staggering/batching unit updates so every NPC isn’t processing on the same frame
  • Client-side visual movement/interpolation while keeping the server authoritative
  • Using simple steering/separation instead of relying heavily on physical collisions between units
  • Only recalculating paths when actually necessary rather than continuously

I’m also curious how expensive Humanoids themselves are nowadays. Is the main issue normally the Humanoid, physics/collisions, pathfinding, replication, or simply having too many per-unit update loops?

For anyone who has built a system with 50-100+ moving NPCs, what setup ended up working best?

Would especially appreciate advice on how you split responsibilities between the server and client while still keeping combat authoritative.

For 50-100+ NPCs I’d separate server simulation from client visuals instead of having every NPC be a full server Humanoid using MoveTo() The server should own postion/state/combat/attack etc. The client renders NPC and interpolates movement. If NPCs follow predictable paths you can replicate something lightweight like path progress instead of constant CFrames. Parallel lua can help afterward but I’d reduce the amount of humanoid/physics work from the server.

1 Like

Some things you can try are the following:

  1. You could implement a spatial grid like Minecraft’s chunks. Then whenever you want something to move towards a target, instead of iterating over all targets and checking the closest one, you iterate over the NPC’s nearest chunks and only check the targets in those chunks.
  2. Instead of having every NPC calculate its own path towards a target, you could instead have one master path that every NPC follows, which is recalculated as necessary when the target is off course.
  3. I would use the MicroProfiler to check which is the most expensive part of NPC AI: MicroProfiler | Documentation - Roblox Creator Hub

Much of what you have considered are good points that you can implement.

1 Like

Just a suggestion. MoveTo is a bit expensive. Consider giving up some realism and maybe use some sort of BulkMoveTo method. You can more than likely interpolate between positions fairly optimally, and if you want to normalize everyone’s speed, you simply do something like the following

function NormLerp(a: Vector3, b: Vector3, t: number, s: number) -- Assuming s is studs/s
   return a + (b-a).Unit * t * s
end

No matter the distance, this will effectively move at a constant speed

1 Like

You want to reduce the amount of data the server has to replicate to the clients by cutting out Humanoids entirely and only sending the bare minimum data that clients need to create their own visuals. The link below is an awesome resource showing you the entire thinking process that these devs went through to achieve the same thing you want:

Notice how they don’t even mention Humanoids once in the main post, because Humanoids carry a lot of overhead just to function and are not designed to handle hundreds of entities.

2 Likes

I decided to remove Humanoids entirely and switch over to attributes and AnimationControllers instead, and the performance improvement has honestly been massive.

If anyone is running into similar performance issues with lots of NPCs or units, I’d definitely recommend testing this approach instead of relying on Humanoids for everything.

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.