Clone controlling system

Hey so hopefully this is the correct category this time!

Im working on an clone controlling system, where every player can have up to 15 clones spawned that fight other players clones.

Ive been scripting it and im trying to make it as performance friendly as possible while still maintaining good quality. What I have now works, but its not smooth, the clones pause and im not sure its very efficient. Ill share the code below:

Client code:


local Players = game:GetService("Players")
local player = game.Players.LocalPlayer
local clonesFolder = workspace:WaitForChild("PlayerClones")
local PlayerClonesFol = clonesFolder:FindFirstChild(player.Name)
while not PlayerClonesFol do
	wait()
	PlayerClonesFol = clonesFolder:FindFirstChild(player.Name)
end
local battlefield = workspace:WaitForChild("BattleGround")

local repStorage = game:GetService("ReplicatedStorage")
local RemotesFolder = repStorage:WaitForChild("Remotes")
local CloneRemote = RemotesFolder:WaitForChild("CloneRemote")

-- Define battlefield boundaries
local minX = battlefield.Position.X - (battlefield.Size.X / 2)
local maxX = battlefield.Position.X + (battlefield.Size.X / 2)
local minZ = battlefield.Position.Z - (battlefield.Size.Z / 2)
local maxZ = battlefield.Position.Z + (battlefield.Size.Z / 2)
local yPosition = battlefield.Position.Y + 5

local ClonesToUpdate = {}
local running = false

-- Generate a random position within the battlefield
local function getRandomPosition()
	return Vector3.new(
		math.random(minX, maxX),
		yPosition,
		math.random(minZ, maxZ)
	)
end

-- Update the clone UI
local function UpdateCloneUI(Clone)
	local head = Clone:FindFirstChild("Head")
	if not head then return end

	local cloneUI = head:FindFirstChild("CloneUI")
	if not cloneUI or not cloneUI:FindFirstChild("Frame") then return end

	local hum = Clone:FindFirstChild("Humanoid")
	if not hum then return end

	local healthBar = cloneUI.Frame.HealthBarBg
	local healthLabel = healthBar.Health
	local healthBarChange = healthBar.Bar
	local levelLabel = cloneUI.Frame.Lvl
	local ownerLabel = cloneUI.Frame.OwnerName

	healthLabel.Text = hum.MaxHealth .. " / " .. hum.Health
	healthBarChange.Size = UDim2.new(hum.Health / hum.MaxHealth, 0, 1, 0)
	levelLabel.Text = "Lvl: " .. player.leaderstats.Level.Value
	ownerLabel.Text = player.Name
end

-- Find the closest enemy clone
local function returnClosestCloneEnemy(Clone)
	local closestClone, minDistance = nil, 100 -- Max search range is 100
	local ourRoot = Clone:FindFirstChild("HumanoidRootPart")
	if not ourRoot then return nil end

	for _, folder in pairs(clonesFolder:GetChildren()) do
		if folder.Name ~= player.Name then
			for _, enemyClone in pairs(folder:GetChildren()) do
				local enemyRoot = enemyClone:FindFirstChild("HumanoidRootPart")
				if enemyRoot then
					local distance = (ourRoot.Position - enemyRoot.Position).Magnitude
					if distance < minDistance then
						minDistance = distance
						closestClone = enemyClone
					end
				end
			end
		end
	end

	return closestClone
end

-- Check for clone death and update state
local function DieCheck(Clone)
	local hum = Clone:FindFirstChild("Humanoid")
	if not hum then return end

	hum.Died:Connect(function()
		CloneRemote:FireServer("Died", {Clone})
		for i, cloneData in pairs(ClonesToUpdate) do
			if cloneData[1] == Clone then
				cloneData[3] = "Idle"
				cloneData[4] = nil
			end
		end
	end)
end

-- Main clone behavior loop
local function cloneBehavior()
	while #ClonesToUpdate > 0 do
		for _, cloneData in pairs(ClonesToUpdate) do
			local Clone, cooldown, action, target = unpack(cloneData)

			if cooldown <= 0 then
				if action == "Idle" then
					local enemy = returnClosestCloneEnemy(Clone)
					if enemy then
						cloneData[3] = "Attack"
						cloneData[4] = enemy
					else
						local randomPos = getRandomPosition()
						CloneRemote:FireServer("MoveTo", {Clone, randomPos})
						cloneData[2] = math.random(20, 40)
					end
				elseif action == "Attack" then
					if not target or not target:FindFirstChild("Humanoid") then
						cloneData[3] = "Idle"
						cloneData[4] = nil
					else
						local distance = (Clone.HumanoidRootPart.Position - target.HumanoidRootPart.Position).Magnitude
						if distance < 5 then
							CloneRemote:FireServer("attack", {Clone, target, 20})
						else
							CloneRemote:FireServer("MoveTo", {Clone, target.HumanoidRootPart.Position})
						end
					end
				end
			else
				cloneData[2] = cloneData[2] - 1
			end

			UpdateCloneUI(Clone)
			DieCheck(Clone)
		end
		task.wait(0.2)
	end

	running = false
end

-- Handle new clones
PlayerClonesFol.ChildAdded:Connect(function(Clone)
	table.insert(ClonesToUpdate, {Clone, 0, "Idle", nil})
	if not running then
		running = true
		task.spawn(cloneBehavior)
	end
end)

-- Handle clone removal
PlayerClonesFol.ChildRemoved:Connect(function(Clone)
	for i, cloneData in ipairs(ClonesToUpdate) do
		if cloneData[1] == Clone then
			table.remove(ClonesToUpdate, i)
			break
		end
	end
end)

SERVER CODE:


local rs = game:GetService("ReplicatedStorage")
local remotes = rs:WaitForChild("Remotes")
local clonesFol = workspace:WaitForChild("PlayerClones")
local bodyClones = workspace:WaitForChild("Bodies")
local PlayerModule = require(game:GetService("ServerScriptService").Modules.PlayerData)

local deathAnimations = {"rbxassetid://93369397898594"} -- Add more animations here
local cloneLastHit = {}

local function PlayDeathAnimation(rigClone)
	local hum = rigClone:FindFirstChild("Humanoid")
	if not hum then return end

	local animator = hum:FindFirstChildOfClass("Animator") or Instance.new("Animator", hum)
	local deathAnim = Instance.new("Animation")
	deathAnim.AnimationId = deathAnimations[math.random(1, #deathAnimations)]
	local track = animator:LoadAnimation(deathAnim)

	track.Looped = false
	track:Play()

	task.delay(5, function()
		if rigClone then
			rigClone:Destroy()
		end
	end)
end

remotes.CloneRemote.OnServerEvent:Connect(function(Player, action, data)
	if action == "MoveTo" then
		local Clone, Position = unpack(data)
		local hum = Clone:FindFirstChild("Humanoid")
		if hum then
			hum:MoveTo(Position)
		end
	elseif action == "attack" then
		local attacker, target, damage = unpack(data)
		local targetHum = target:FindFirstChild("Humanoid")
		if targetHum then
			cloneLastHit[target] = Player
			targetHum:TakeDamage(damage)
		end
	elseif action == "Died" then
		local Clone = data[1]
		local hum = Clone:FindFirstChild("Humanoid")
		if hum and hum.Health <= 0 then
			local PlayerWhoKilled = cloneLastHit[Clone]
			if PlayerWhoKilled then
				PlayerModule.AddWon(PlayerWhoKilled, 10)
			end
			Clone.Parent = bodyClones
			PlayDeathAnimation(Clone)
		end
	end
end)

Any help on how I can go at this, make it perform better and improve the looks of it in game would be greatly appreciated!! Thanks y’all

Could you show a video of the clones pausing or describe between what steps they pause? I would guess most of the pausing would be from either the Humanoid movement functions being lazy/annoying or from lag when sending your RemoteEvents from the client.

1 Like

I tried uploading a gif but it wouldn’t let me,

when they kill their target, they pause for like 30s to a minute before they start “idling” (roaming) again.

alongside that, is there a way I can stop the clones from running directly into eachothers faces when they are fighting?

1 Like

This is really impressive scripting. Maybe try the actual move within the client.

I would add print statements to your code to see where the pausing happens (or in your case what state it’s in when it stops sending the server actions). It’s probably in the client code.

Random guess, does the cooldown get updated anywhere? I would personally use string indexing (e.g. cloneData.Cooldown = 5) instead of storing the cloneData in an array to make the code more readable.

This line seems to be updating the cooldown to about 30/5 seconds: cloneData[2] = math.random(20, 40), so that might be the problem. Perhaps this line is causing the waiting or getting triggered too much?

1 Like

I was thinking of that, but then with a lot of clones, like 50 odd I feel like it’d start to have a performance impact on worser clients, mobile users specifically; and it wouldn’t be synced well across the other clients.

I’ve never made that many clone npcs so I’m not really sure of that but, If I did I would test that.