I need help with my pet following system. Can't achieve smooth movement

I’m trying to make just a simple pet following system and I almost made it but I can’t fix one thing. The animations are really glitchy and not close to smooth even when I’m trying to create them with tweens. Another problem is that I think when I try making the animation smoother it became really inefficient.

Here is a video: https://youtu.be/8t30OoPlAt4

I’m trying to solve the pet system from a long time but this part made everything much harder so I would really appreciate every help.

Here is the code: (server script)

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
local TweenService = game:GetService("TweenService")

local EquipPetEvent = ReplicatedStorage:WaitForChild("Events"):WaitForChild("EquipPetEvent")
local UnequipPetEvent = ReplicatedStorage:WaitForChild("Events"):WaitForChild("UnequipPetEvent")

local D = 5         -- dis behind player for first row
local R = 3         -- dis between rows
local S = 3        -- horizontal spacing between pets
local maxPerRow = 5
-- tween settings
local tweenDuration = 0.3 
local bobAmplitude = 0.5  

local equippedPets = {}    
local petCoroutines = {}  

local function calculateTargetCFrame(hrp, formationIndex, totalPets, phaseOffset)
	local row = math.ceil(formationIndex / maxPerRow)
	local posInRow = (formationIndex - 1) % maxPerRow
	local petsInRow = math.min(maxPerRow, totalPets - (row - 1) * maxPerRow)
	local behindOffset = -(D + (row - 1) * R) * hrp.CFrame.LookVector
	local lateralOffset = (-(petsInRow - 1) / 2 + posInRow) * S * hrp.CFrame.RightVector

	local basePos = hrp.Position + behindOffset + lateralOffset + Vector3.new(0, 2, 0)

	local yOffset = bobAmplitude * math.sin(2 * math.pi * os.clock() + phaseOffset)

	return CFrame.new(basePos + Vector3.new(0, yOffset, 0)) * hrp.CFrame.Rotation * CFrame.Angles(0, math.rad(180), 0)
end

local function startPetTweenLoop(player, pet)
	local thread = coroutine.create(function()
		local phaseOffset = pet:GetAttribute("PhaseOffset") or 0
		local hrp = player.Character:FindFirstChild("HumanoidRootPart")
		pet:SetPrimaryPartCFrame(hrp.CFrame)
		while pet.Parent and player.Character and player.Character:FindFirstChild("HumanoidRootPart") do
			local pets = equippedPets[player]
			local formationIndex
			for i, p in ipairs(pets) do
				if p == pet then
					formationIndex = i
					break
				end
			end
			if not formationIndex then
				break 
			end


			local totalPets = #pets
			local targetCFrame = calculateTargetCFrame(hrp, formationIndex, totalPets, phaseOffset)

			local tween = TweenService:Create(pet.PrimaryPart, TweenInfo.new(tweenDuration, Enum.EasingStyle.Quad, Enum.EasingDirection.Out), {CFrame = targetCFrame})
			tween:Play()
			task.wait(0.1)
		end
	end)

	petCoroutines[player] = petCoroutines[player] or {}
	petCoroutines[player][pet] = thread
	coroutine.resume(thread)
end

EquipPetEvent.OnServerEvent:Connect(function(player, petId)
	equippedPets[player] = equippedPets[player] or {}

	local petModel = ReplicatedStorage.Pets:FindFirstChild(petId, true)
	if petModel then
		local clonedPet = petModel:Clone()

		clonedPet:SetAttribute("PhaseOffset", math.random() * 2 * math.pi)


		if not clonedPet.PrimaryPart then
			clonedPet.PrimaryPart = clonedPet:FindFirstChildWhichIsA("BasePart")
		end


		for _, part in ipairs(clonedPet:GetDescendants()) do
			if part:IsA("BasePart") then
				part.CanCollide = false
			end
		end

		clonedPet.Parent = workspace
		table.insert(equippedPets[player], clonedPet)


		startPetTweenLoop(player, clonedPet)
	else
		warn("Pet model not found for petId:", petId)
	end
end)


UnequipPetEvent.OnServerEvent:Connect(function(player, petId)
	if equippedPets[player] then
		for i, pet in ipairs(equippedPets[player]) do
			if pet.Name == petId then
				if petCoroutines[player] and petCoroutines[player][pet] then
					petCoroutines[player][pet] = nil
				end
				pet:Destroy()
				table.remove(equippedPets[player], i)
				break
			end
		end
	end
end)

Players.PlayerRemoving:Connect(function(player)
	if equippedPets[player] then
		for _, pet in ipairs(equippedPets[player]) do
			if pet and pet.Parent then
				pet:Destroy()
			end
		end
		equippedPets[player] = nil
		petCoroutines[player] = nil
	end
end)


Again thanks a lot

1 Like

Don’t use tweens in a while loop as they usually come out very choppy and the tweens usually don’t finish when the next iteration starts, use something like lerping instead.

I would also probably do visual things on the client as they don’t impact the game very much, and just causes latency, but you do you ig.

1 Like

How I could do it without tweens but to be smooth?

Like I said, use lerping, it gives you more control over your animation and doesn’t cause jittering(usually) because of the control.

research into lerping yourself, you’ll learn from it, here is a basic function:

function Lerp(startPoint, endPoint, factor)
	return startPoint + (endPoint - startPoint) * factor
end

local Half = Lerp(0, 10, 0.5)

print(Half) -- Prints 5

theres the start point, where the beginning is.
theres the end point, where the end is.
and the interpolation factor, which describes the place in the animation.

the interpolation factor is usually always between 0 and 1, you will need to insert a decimal/float.

In this code, the start point is 0, and the end point is 10.

Decimals are basically fractions in a different form, so 0.5 is basically 1/2.

So if half of 10 is 5, the answer is 5.

Now you can combine os.clock() or tick() with a RunService loop which will be the smoothest, or a while loop, which might not look as good.

CFrame already has a :Lerp() function, so you can utilise it to your advantage.

Im not going to solve it, since this is an opportunity to learn, and cuz im quite lazy tbh, if you are REALLY stuck, then refer back here.

2 Likes

I tried that but it’s still the same…

Here is the new code with lots of changes:

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")


local EquipPetEvent = ReplicatedStorage:WaitForChild("Events"):WaitForChild("EquipPetEvent")
local UnequipPetEvent = ReplicatedStorage:WaitForChild("Events"):WaitForChild("UnequipPetEvent")


local DISTANCE_BEHIND = 5  
local SPACING = 3        
local MAX_PER_ROW = 5
local ROW_DISTANCE = 3 
local HEIGHT_OFFSET = 2 
local LERP_ALPHA = 0.1    
local BOB_AMPLITUDE = 0.6

local equippedPets = {}        
local petUpdateConnections = {} 

local function calculateTargetCFrame(humanoidRootPart, petIndex, totalPets, phaseOffset)
	local row = math.ceil(petIndex / MAX_PER_ROW)
	local positionInRow = (petIndex - 1) % MAX_PER_ROW
	local petsInRow = math.min(MAX_PER_ROW, totalPets - (row - 1) * MAX_PER_ROW)

	local behindOffset = -(DISTANCE_BEHIND + (row - 1) * ROW_DISTANCE) * humanoidRootPart.CFrame.LookVector
	local lateralOffset = (-(petsInRow - 1) / 2 + positionInRow) * SPACING * humanoidRootPart.CFrame.RightVector
	local basePosition = humanoidRootPart.Position + behindOffset + lateralOffset + Vector3.new(0, HEIGHT_OFFSET, 0)

	local yOffset = BOB_AMPLITUDE * math.sin(2 * math.pi * os.clock() + phaseOffset)
	local targetPosition = basePosition + Vector3.new(0, yOffset, 0)

	return CFrame.new(targetPosition) * humanoidRootPart.CFrame.Rotation * CFrame.Angles(0, math.rad(180), 0)
end

EquipPetEvent.OnServerEvent:Connect(function(player, petId)
	equippedPets[player] = equippedPets[player] or {}

	local petModel = ReplicatedStorage.Pets:FindFirstChild(petId, true)
	if not petModel then
		warn("Pet model not found for petId:", petId)
		return
	end

	local clonedPet = petModel:Clone()
	clonedPet:SetAttribute("PhaseOffset", math.random() * 2 * math.pi)
	if not clonedPet.PrimaryPart then
		clonedPet.PrimaryPart = clonedPet:FindFirstChildWhichIsA("BasePart")
	end

	for _, part in ipairs(clonedPet:GetDescendants()) do
		if part:IsA("BasePart") then
			part.CanCollide = false
		end
	end

	clonedPet.Parent = workspace
	table.insert(equippedPets[player], clonedPet)

	if #equippedPets[player] == 1 then
		petUpdateConnections[player] = RunService.Heartbeat:Connect(function()
			local hrp = player.Character and player.Character:FindFirstChild("HumanoidRootPart")
			if hrp then
				for index, pet in ipairs(equippedPets[player]) do
					if pet and pet.PrimaryPart then
						local phaseOffset = pet:GetAttribute("PhaseOffset") or 0
						local targetCFrame = calculateTargetCFrame(hrp, index, #equippedPets[player], phaseOffset)
						pet.PrimaryPart.CFrame = pet.PrimaryPart.CFrame:Lerp(targetCFrame, LERP_ALPHA)
					end
				end
			end
		end)
	end
end)

UnequipPetEvent.OnServerEvent:Connect(function(player, petId)
	if not equippedPets[player] then return end

	for index, pet in ipairs(equippedPets[player]) do
		if pet.Name == petId then
			pet:Destroy()
			table.remove(equippedPets[player], index)
			if #equippedPets[player] == 0 and petUpdateConnections[player] then
				petUpdateConnections[player]:Disconnect()
				petUpdateConnections[player] = nil
			end
			break
		end
	end
end)

Players.PlayerRemoving:Connect(function(player)
	if equippedPets[player] then
		for _, pet in ipairs(equippedPets[player]) do
			if pet and pet.Parent then
				pet:Destroy()
			end
		end
		equippedPets[player] = nil
	end
	if petUpdateConnections[player] then
		petUpdateConnections[player]:Disconnect()
		petUpdateConnections[player] = nil
	end
end)

Well then im guessing its just server latency between the client and server, maybe try making the animations on the client. As it just causes latency, it’s also better to use RenderStepped for this rather than Heartbeat. And use a combination of delta time in the RunService loop and tick() or os.clock() to manage speed and have more control over LERP_ALPHA.

Also this kind of seems like AI generated code…

1 Like