What is the most efficient way to render client sided mobs?

Main Question:
What is the best way to

  1. Communicate server positions to client displays and
  2. Move the server and client positions in the most efficient way (BodyMovers, CFrames, Lerping, ect…).

I’ve looked at multiple sources such as the following…

Some topics I’ve found suggest to try doing fully client sided mobs and use the server to validate their positions, though the issue I’ve run into with this method is that it only really works for single players since you cannot have multiple mob AIs on each client and expect it to behave the same.

Another ideas is having the positions of the mobs stored in a server sided script, however this would require excessive RemoteEvent firing to update client sided things.

Currently I create a part which is the size of the mob’s model extents size and then load in the actual mob models on each client. I then use lerp to move the server sided blocks around and have the client constantly lerp ~30% toward the server block’s CFrame every renderstep. This provides a fairly okay connection between the server and the client however it lags astronomically. I’ve also taken further measures to not lerp/show client sided mobs that are x distance away or off the screen of the player. It has even gotten so bad that if I don’t let the client load in first the renderstep function will cause everything else to starve and never load the client.

Any suggestions?

Mob Ai (very basic for testing)

    local subject = script.Parent

    while true do
    	local randomCFrame = CFrame.new(Vector3.new(math.random(-100,100), subject.Position.Y, math.random(-100,100)))
    	local newCFrame = CFrame.new(randomCFrame.Position, (subject.Position - randomCFrame.Position).Unit * 1000)
    	
    	local count = 0
    	subject.CFrame = CFrame.new(subject.Position, newCFrame.Position)
    	repeat
    		subject.CFrame = subject.CFrame:lerp(newCFrame, count)
    		count = count + 0.0001
    		wait()
    	until (subject.Position - newCFrame.Position).Magnitude <= 5
    end

Client

local CollectionService = game:GetService("CollectionService")
local RunService = game:GetService("RunService")
local _mobs = game.ReplicatedStorage:WaitForChild("_mobs")

local Player = game.Players.LocalPlayer
local Camera = workspace.CurrentCamera
local Character = Player.Character or Player.CharacterAdded:Wait()	-- Not actually using this, just yielding the code for added event
local HumanoidRootPart = Character:WaitForChild("HumanoidRootPart")

local CurrentMobs = CollectionService:GetTagged("Mob")
local DisplayedMobs = {}

local MAX_DISTANCE = 100

function addMobVisual(mob)
	local index, info = find(mob, DisplayedMobs, "mob")
	if not index then
		local visual = _mobs:FindFirstChild(mob.Name):Clone()
		if not visual then warn(mob.Name.." model not found") return end
		
		visual:SetPrimaryPartCFrame(mob.CFrame)
		visual.Parent = workspace
		
		local entry = {mob = mob, visual = visual}
		table.insert(DisplayedMobs, entry)
	else
		if info.mob.CFrame ~= info.visual.PrimaryPart.CFrame then
			if info.visual ~= nil then
				info.visual:SetPrimaryPartCFrame(info.visual.PrimaryPart.CFrame:lerp(info.mob.CFrame, 0.35))
			end
		end
	end
end

function checkDuplicate(mob, list)
	for i,v in pairs(list) do
		if v == mob then
			return true
		end
	end
	
	return false
end

function find(obj, list, prop)
	for i,v in pairs(list) do
		if not prop then
			if v == obj then
				return i,v
			end
		else
			if v[prop] == obj then
				return i,v
			end
		end
	end
	
	return false
end

function getNearbyMobs()
	local nearby = {}
	local toofar = {}
	
	for i,v in pairs(CurrentMobs) do
		local vector, onScreen = Camera:WorldToScreenPoint(v.Position)
		if (v.Position - Player.Character.HumanoidRootPart.Position).Magnitude <= MAX_DISTANCE and onScreen then
			table.insert(nearby, v)
		else
			table.insert(toofar, v)
		end
	end
	
	return nearby, toofar
end

function watchMobs()
	for i,v in pairs(CurrentMobs) do
		local vector, onScreen = Camera:WorldToScreenPoint(v.Position)
		if (v.Position - Player.Character.HumanoidRootPart.Position).Magnitude <= MAX_DISTANCE and onScreen then
			spawn(function()
					addMobVisual(v)
			end)
		else
			local index, info = find(v, DisplayedMobs, "mob")
			if index then
				info.visual:Destroy()
				DisplayedMobs[index] = nil
			end
		end
	end
end

CollectionService:GetInstanceAddedSignal("Mob"):Connect(function(mob)
	if not checkDuplicate(mob, CurrentMobs) then
		table.insert(CurrentMobs, mob)
	end
	
	if (mob.Position - Player.Character.HumanoidRootPart.Position).Magnitude <= MAX_DISTANCE then
		if not find(mob, DisplayedMobs) then
			addMobVisual(mob)
		end
	end
end)

wait(5)

RunService.RenderStepped:Connect(function()
	watchMobs()
end)
8 Likes

Instead of updating every mob’s position every wait() on the server, just store the newCFrame (and the time that the mob is changing position to validate positions on the server if needed), and send that to the players. On the players’ side, if they are within range of the mob, include them in the mobs that get updated every RenderStepped call.

1 Like

So you’re saying to store positions in a script and send the data over via RemoteEvents?

Just going to answer this in case anyone else in the future wonders the same thing

What I found to be the most efficient is the following:

Server

  • Portray mobs as coded objects (metatables preferably) that store their CFrame in a Server Sided script only.
  • Move them by creating movement functions that act on their stored CFrames
  • I found that Tween gives more customizability than Lerp in terms of movement

Connection

Client

  • Take a copy of the mob model and CFrame it to the server sent CFrame every RenderStep. I use lerp for this
mob.visual:SetPrimaryPartCFrame(mob.visual.PrimaryPart.CFrame:lerp(mob.goalCFrame, 0.35))
  • Dont show the visual if the mob is too far or offscreen
local vector, onScreen = Camera:WorldToScreenPoint(MOB_POSITION)

Other Notes

Thats basically it. With this method I can currently get around 100-200 mobs while sustaining 60fps. Only downside is you’ll need to make your own physics for falling, climbing, ect…

24 Likes

Why every heartbeat instead of every PreRender deltaTime?
Oh I didn’t see this was 4 years old.

store everyone enemy inside a table and load model in client side by using remote event + loop