Enemy run on server not moving smoothly on client's side

I’ve been working on this for the last few days and I was about to give up so I decided to post this here and see if someone could identity a problem or send me to an approach that could fix it. Any help appreciated!

  1. What do you want to achieve?

I am trying to make an custom enemy lemming ai that moves towards the player once in range, and if possible I want to use Roblox physics because then I don’t need use cases for collisions. The hope is to re-create the enemy rigidbody of something like Swordburst 1 or Swordburst 2’s enemies. (outdated I know, but that is still my goal)

  1. What is the issue?

While I’ve gotten every mobility option to work, I believe it’ll always be stuck at 30 fps because it solely runs on the server, which leaves the visual of the enemy looking choppy and laggy:

If the quality isn’t too clear I apologize, but what is happening in the video is the enemy is moving at basically the proper speed, but the rate it’s movement happens is slower than the player’s refresh rate, which makes visual afterimages when the enemy tries to move.
- Edit: After watching the video it’s only possible to see the issue at 1080p

  1. Is there code to provide?

Yes, here is an example of my use of physics. I have a different segment for every other solution I’ve tried but only provided LinearVelocity for now.
Similarly I cutout everything else in my run service out except for the code related to the issue.

function Move(direction, speed)
	if (RigidBody:FindFirstChild("Collider"):FindFirstChild("LinearVelocity")) then
		
		local collider = RigidBody:FindFirstChild("Collider");
		
		local linearVelocity = collider:FindFirstChild("LinearVelocity");
		
		if (direction ~= Vector2.zero) then

			if (direction.Magnitude > 0) then -- Normalize Direction
				direction = direction.Unit;
			end
			local movement = direction * Vector2.new(speed, speed);
			
 			
			linearVelocity.PlaneVelocity = movement;
 			-- ^^^ setting the movement only when an actual change would occur made no difference
			
			if (collider:GetAttribute("ForceVelocity")) then

				forceVelocity = collider:GetAttribute("ForceVelocity");
				
				local localDirection = collider.CFrame:VectorToObjectSpace(-forceVelocity);

				linearVelocity.PlaneVelocity = movement + Vector2.new(localDirection.Z, localDirection.X);

			end
		else
			
			linearVelocity.PlaneVelocity = Vector2.zero;
		end
		
		return;
	end

end

RunService.Heartbeat:Connect(function(deltaTime)
	
	if (#playersInRange > 0 and #Abilities ~= 0) then
		
		-- check if ability permissions are active
		if (canMove) then
			-- move
			
			Move(Vector2.new(1, 0), walkSpeed);

		end
		
	end

end)

Additionally here is the hierarchy of my enemy object:

  1. What solutions have you tried so far? Did you look for solutions on the Creator Hub?

I’ve tried altering my code to use linearVelocity (example above), bodyVelocity, AssemblyLinearVelocity, alternating CFrame steps, ForceImpulses, basically any movement option I could think of that would create the system I am looking for.

I tried looking through a few posts on the creator hub, but nothing that really fixed the server issue, mainly just found better handles on moving the physics.

As I said at the beginning any help would be greatly appreciated, but I believe I’ve exhausted all the solutions I could find. I apologize if my code isn’t as optimized as it should be—been using Unity C# for the last year and only returned to Lua recently.

You may have to force the network ownership of the enemy onto the server.
Try calling collider:SetNetworkOwner() at the start of the game/whenever you create the enemy.

Just tried it, but it didn’t change anything sadly. Do I need to specify something in the parameters or should it just auto set the server as the network owner once the method is called?

local function Start()
	
	RigidBody.Collider:SetNetworkOwner();

end Start();

You can only get truly smooth interactions by setting the network owner to the player. This means that the choppiness issue will still happen on other clients however you can mitigate it where it most matters, whenever a player interacts with the enemy (eg gets damaged/attacks) you should set the network owner to said player.

So I tried setting it to the enemy’s target player, but now it is spazing outta control and no longer moves.

RunService.Heartbeat:Connect(function(deltaTime)
	
	if (#playersInRange > 0 and #Abilities ~= 0) then

		local target = GetTarget(); if (target == nil) then return; end
		-- ^^^ GetTarget returns a player object from game:GetService("Players").
		
		RigidBody.Collider:SetNetworkOwner(target);
		
		-- check if ability permissions are active
		if (canMove) then
			-- move
			
			Move(Vector2.new(1, 0), walkSpeed);

		end
		
	end

end)

– Edit: When I switched to the server’s perspective most is fine (spazing stopped), but the enemy still doesn’t move.

Roblox physics and replication is usually smoother than this, so I’d presume that there might be an issue where LinearVelocity is set to (0, 0, 0) instead of the desired result. Use the debugger’s watch feature to watch the variables direction, movement, forceVelocity, and linearVelocity.PlaneVelocity.

Also, I’d suggest moving the collider and linear velocity variables outside of the Move() function. assert() that they exist instead of relying on an if-then statement. :FindFirstChild() is slow (20% slower than accessing children via ., iirc), and if you know for a fact that those instances are there, there isn’t any need to find them again. Ditto for collider:GetAttribute("ForceVelocity"), if it isn’t updated frequently.

If you want to get really nerdy you can “create” the enemy on the server but render it on the clients only.
You’d have to learn how to sync it properly though

I feel like replicating velocity to clients like this is bad, if you dropped/a few packets it could get really unsynced, maybe just try sending positions of the enemies and lerp them on the client, the Source Engine has it documented here

2 Likes

That happens because you are setting the network owner every frame, it is going to spaz out and act really weirdly. Thats why i suggested setting it once the enemy aggroes onto a player

I was able to do this, but not too sure if this is an optimized solution since I’m updating every enemy on the map at once (in a theoretical that there are over 100 enemies on the map at once)
But I guess I could also only update the visuals of enemies in close proximity to the player to fight that.

Anyways, thanks for the info, the enemies are moving much smoother now!

– Edit: Here is the script - UpdateEnemyInstances.LocalScript

local RunService = game:GetService("RunService");
local CollectionService = game:GetService("CollectionService");

RunService.RenderStepped:Connect(function(deltaTime)
	
	for _, enemyInstance in ipairs(CollectionService:GetTagged("EnemyInstance")) do
		
		if (enemyInstance:FindFirstChild("Model") and enemyInstance:FindFirstChild("PrimaryRootPart")) then
			
			if (enemyInstance.Model:FindFirstChild("ModelRootPart")) then
				
				enemyInstance.Model.ModelRootPart.CFrame = enemyInstance.Model.ModelRootPart.CFrame:Lerp(enemyInstance.PrimaryRootPart.CFrame, 10 * deltaTime);
				
			end
			
		end
		
	end
	
end)

I tried using the watch method, but couldn’t get it to work (will probably rely on the console unless I figure it out one day.

I’ve now set them outside of the move method, and will try to avoid using FindFirstChild() (I used it a lot when I was coding in unity for ease of prototyping)

And as for ForceVelocity that updates per frame because it needs to decrease by a specific trend to be smooth, so not much I can do for that.

Thanks for the help!

Think I’ve now accomplished that thanks to @hazame4’s suggestion. Might take another crack at it later tho if it comes out as unoptimized in the long run.

Will try to make my code less perpetual going forward, thanks for the explanation!

1 Like

“Creating” the enemy on the server (not rendering on the server), and only rendering on the clients would be the most performant (at least from my past experiences). You’d just have to deal with sync and making sure the enemy attacks the same player(s) on each client, which could be tricky.

I have a game where all the NPC’s are created on the server, then the server tells the client some information such as, NPC Name, CFrame, Weapon, etc. Then the client will render the actual model with so weapon and the NPC model (NPC Name is the name of the NPC model which the client will find based off of).

I suggest you to use a buffer library if you do go this route as it will save on some networking.

A small example:


Server:

local ReplicatedStorage = game:GetService('ReplicatedStorage')
local HttpService = game:GetService('HttpService')

local SpawnedNPCs = {}
local SpawnNPC = ReplicatedStorage.SpawnNPC

local function spawnNPC(NPC : string, data : {health : number, speed : number}, cframe : CFrame)
    local ID = HttpService:GenerateGUID(false)
    
    SpawnedNPCs[ID] = {
        NPC = NPC,
        Health = data.health,
        Speed = data.speed
    }

   SpawnNPC:FireAllClients(ID, cframe,  NPC, data)
end

spawnNPC('Gojo', {health = 100, speed = 24}, CFrame.new(10, 0, 10))

Client:

local ReplicatedStorage = game:GetService('ReplicatedStorage')

local NPCFolder = ReplicatedStorage.NPCs
local SpawnNPC = ReplicatedStorage.SpawnNPC

SpawnNPC.OnClientEvent:Connect(function(ID : string, cframe : CFrame, NPCName : string, data : {health : number, speed : number})
    local NPCModel = NPCFolder:FindFirstChild(NPCName):Clone()
    NPCModel.Name = ID
    
    local Humanoid = NPCModel.Humanoid
    
    Humanoid.MaxHealth = data.health
    Humanoid.Health = data.health
    Humanoid.WalkSpeed = data.speed

    NPCModel:PivotTo(cframe)
    NPCModel.Parent = workspace.CurrentCamera
end)
1 Like

I see what you mean. I mean I don’t know if this is applicable to my current game but I will fs come back to this once I’m ready to organize it all more. Also didn’t know you can setup the parameters like that so I’ll start doing that in my new code, thanks for the heads up!

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