Hi!
One of the challenges I see again and again for creators is having lots of NPCs
So I built a framework that does a pretty good job of letting you have hundreds (or thousands?) of NPCs in your game world.
I know theres a lot of other great replication frameworks out there, but this one just focuses on making NPCs and lots of them.

The code is uncopylocked here:
Features:
- Fully custom replication
- Low Hz update and Think Rates, making it very conservative on bandwidth and cpu
- Culling (per player, or whatever you like really)
- Easy to use API
- Custom events per NPC
- Can still edit the NPC model on the server (add weapons etc)
Some benchmarks:
The demo place has 300 NPCs spawn in with a 500 unit culling radius, and takes about 13kb/s RECV
Admittedly they’re pretty low fidelity (2hz updates) but it’ll send more updates if something sudden happens like jumping or animation changes.
Anyhow, check out a quick demo usage:
local npcCullRadius = 500
local npcs = {}
for j=1, maxNpcs do
config.walkAccel = 10
config.walkSpeed = 16
config.thinkHzRate = 10 --Technically this can be as low as the replication rate
config.replicationHzRate = 2
local box = 50 + j
local npcRecord = npcModule:SpawnNpc(game.ServerScriptService.LightweightNpcs.Rigs.Bob, config, Vector3.new(math.random(-box,box),10,math.random(-box,box)), 0)
DoSillyColors(npcRecord :: any)
--data is just custom scratchpad per npc, for convenience
npcRecord.data.nextMove = 0
npcRecord.onThink:Connect(function(dt)
if (tick() > npcRecord.data.nextMove) then
npcRecord.data.nextMove = tick() + 0.5 + math.random( )
if (math.random() > 0.5) then
--idle
npcRecord:Move(Vector3.zero)
npcRecord:PlayAnimation("idle", false, 1, 1)
else
--run
local ang = math.random() * math.pi * 2
npcRecord:Move(mathUtils:PlayerAngleToVec(ang))
npcRecord:PlayAnimation("run", false, 1, 1)
end
end
if (randomlyJump) then
if (math.random() < 0.1 and npcRecord.isOnGround == true) then
npcRecord:Jump()
npcRecord:PlayAnimation("jump", false, 1, 1)
end
end
end)
npcRecord.onCullCheck:Connect(function()
--Basic culling, but you could technically mask NPCs to individual players
--or anything else you want to do here
for key,player in game.Players:GetPlayers() do
if player.Character and player.Character.PrimaryPart then
local bubbleVec = npcRecord.position - (player.Character.PrimaryPart :: any).Position
if (bubbleVec.Magnitude < npcCullRadius) then
npcRecord:SetPlayerVisibleFlag(player, true)
else
npcRecord:SetPlayerVisibleFlag(player, false)
end
end
end
end)
npcRecord.onCrashland:Connect(function()
--don't technically need to do anything here
--but because we want a new animation we'll go to idle and then trigger a new move
npcRecord:PlayAnimation("idle", false, 1, 1)
npcRecord.data.nextMove = 0
end)
npcRecord.onFellOffMap:Connect(function()
print("Fell off map:", npcRecord.npcId)
end)
npcRecord.onCleanup:Connect(function()
print("Cleaned up:", npcRecord.npcId)
end)
npcRecord.onBumpIntoWall:Connect(function(instance)
--send an event to clients if we bump into something
if (npcRecord.instance) then
--This costs RECV, but flashes the NPCS white for fun
npcRecord:AddClientEvent({id = "Bump"})
end
npcRecord:Move(Vector3.zero)
npcRecord:PlayAnimation("idle", false, 1, 1)
end)
table.insert(npcs, npcRecord)
