Pets following after a delay

Hey DevForum!

Recently I’ve been working on a simple pet follow module, where I can assign pets to a player with specific functions. However, I’ve run into an issue illustrated below:

The pets follow the player with a delay.

The pets use an AlignOrientation to rotate the pet (which has no issues) and an AlignPosition to make the pet follow the player (which takes its own sweet time to do so).

I’ve tried setting the network ownership of the pet to the player, no luck. I’ve looked up on the DevForum for any solutions but I couldn’t find any that could solve mine. I thought it’s because I change the Position of AlignPosition on the server using RunService.Heartbeat, so I ran an local server and tested, but the delay occurs on the server too.

Here’s my code so far:

follow() function of ModuleScript in ReplicatedStorage.Modules
function pet:follow(distance, radius, hoverHeight)
	distance = tonumber(distance) or 6;
	radius = math.rad(tonumber(radius) or 0);
	hoverHeight = tonumber(hoverHeight) or 4;
	
	self._connections.follow = runService.Heartbeat:Connect(function()
		local obj = self.object;
		if not obj then return; end
		
		if not self:updateState() then return; end
		
		local character = self.owner.character;
		local hrp = character.PrimaryPart;
		if not hrp then return; end
		
		--local posXZ = (hrp.CFrame * CFrame.fromEulerAnglesXYZ(0, radius, 0) * CFrame.new(0, 0, distance + (obj.Size.Z / 2))).Position;
		local posXZ = hrp.Position + Vector3.new(math.cos(radius) * (distance + obj.Size.X / 2), 0, math.sin(radius) * (distance + obj.Size.Z / 2));
		
		raycastParams.FilterDescendantsInstances = { character, folder };
		local res = workspace:Raycast(posXZ + Vector3.new(0, 7.5, 0), Vector3.new(0, -20, 0), raycastParams);
		local finalPosition = (res and res.Position) or posXZ - Vector3.new(0, 2.5, 0);
		finalPosition += Vector3.new(0, hoverHeight + (obj.Size.Y / 2), 0);
	
		if self.animate and self.state == "Idle" then
			finalPosition += Vector3.new(0, math.sin(os.clock() * self.animMultiplier) * self.animOffset, 0);
		end
		
		obj.Gyro.Attachment0 = obj.Center;
		obj.Move.Attachment0 = obj.Center;
		--obj.Gyro.CFrame = CFrame.lookAt(obj.Position, character.Head.Position);
		obj.Gyro.CFrame = hrp.CFrame;
		obj.Move.Position = finalPosition;
	end);
end
Testing Script in ServerScriptService
local playersService = game:GetService("Players");

local petModule = require(game:GetService("ReplicatedStorage").Modules.Pet);

playersService.PlayerAdded:Connect(function(player)
	player.CharacterAppearanceLoaded:Wait();
	
	for i = 0, 330, 30 do
		local p = petModule.new("Pet", player);
		p:follow(6, i, 4);
	end
end);

Any help would be appreciated, thank you in advance!

1 Like

Update:

I tried using the follow() function on the client like so:

Testing Script in ServerScriptService
local playersService = game:GetService("Players");
local replicatedStorage = game:GetService("ReplicatedStorage");

local petModule = require(replicatedStorage.Modules.Pet);
local petFollow = replicatedStorage.Events.PetFollow;

playersService.PlayerAdded:Connect(function(player)
	player.CharacterAppearanceLoaded:Wait();
	
	for i = 0, 330, 30 do
		local p = petModule.new("Pet", player);
		petFollow:FireClient(player, p, 6, i, 4);
	end
end);
Following LocalScript in StarterPlayerScripts
local replicatedStorage = game:GetService("ReplicatedStorage");

local petFollow = replicatedStorage.Events.PetFollow;
local petModule = require(replicatedStorage.Modules.Pet);

petFollow.OnClientEvent:Connect(function(pet, ...)
	setmetatable(pet, petModule):follow(...);
end);

FInally I upped the value of Responsiveness of the AlignPosition from 10 to 25.

It kind of work as shown below:

However, I don’t like how the pets behave after changing the Responsiveness. Any solution to this?

Since I didn’t find any solutions upon two hours of research, I’ll call it an end.
My result so far:

I optimised the follow() function a little so that it doesn’t keep raycasting every frame when it’s idle.

follow() function
function pet:follow(distance, radius, hoverHeight)
	distance = tonumber(distance) or 6;
	radius = math.rad(tonumber(radius) or 0);
	hoverHeight = tonumber(hoverHeight) or 4;
	
	self._connections.follow = runService.Stepped:Connect(function()
		local obj = self.object;
		if not obj then return; end
		
		if not self:updateState() then return; end
		
		local character = self.owner.character;
		local hrp = character.PrimaryPart;
		if not hrp then return; end
		
		local posXZ = hrp.Position + Vector3.new(math.cos(radius) * (distance + obj.Size.X / 2), 0, math.sin(radius) * (distance + obj.Size.Z / 2));
		local finalPosition = posXZ - Vector3.new(0, 2.5, 0);
		
		if self.state == "Idle" then
			finalPosition += Vector3.new(0, (self.animate and math.sin(os.clock() * self.animMultiplier) * self.animOffset) or 0, 0);			
		else
			raycastParams.FilterDescendantsInstances = { character, folder };
			local res = workspace:Raycast(posXZ + Vector3.new(0, 7.5, 0), Vector3.new(0, -20, 0), raycastParams);
			finalPosition = (res and res.Position) or finalPosition;
		end
		finalPosition += Vector3.new(0, hoverHeight + (obj.Size.Y / 2), 0);
		
		obj.Gyro.Attachment0 = obj.Center;
		obj.Move.Attachment0 = obj.Center;
		
		obj.Gyro.CFrame = hrp.CFrame;
		obj.Move.Position = finalPosition;
	end);
end

Finally, I changed the value of Responsiveness from 25 to 30.

Marking this as a solution in case someone else stumbles upon this topic further down the road.

1 Like

why are you even doing following on the server side?

Please read the update replies. I’ve changed it to client side and also, this topic is solved. There’s no point in bumping this thread.

1 Like

my bad, I am actually making a pet following system as well and I am having some troubles, I do really like your idea and it is good performance-wise as well,


The code shown in this photo is not a full code I assume, I wanted to ask you if this pet following system is open source and if it is, then can I have it?

Sure, you can have it. It’s not for a project or anything, I was just fiddling around.

local pet = {};
pet.__index = pet;

local workspace = game:GetService("Workspace");
local runService = game:GetService("RunService");

local pets = game:GetService("ReplicatedStorage").Pets;

local folder = workspace:FindFirstChild("Pets") or Instance.new("Folder");
folder.Name = "Pets";
folder.Parent = workspace;

local raycastParams = RaycastParams.new();
raycastParams.FilterType = Enum.RaycastFilterType.Blacklist;
raycastParams.IgnoreWater = true;

local function getTouchingParts(p)
	local con = p.Touched:Connect(function() end);
	local parts = p:GetTouchingParts();
	con:Disconnect();
	return parts;
end

function pet.new(name, owner)
	local petObj = pets:FindFirstChild(name);
	assert(petObj, "No pet exists with the name \"" .. name .. "\"");
	
	petObj = petObj:Clone();
	
	local collider = Instance.new("Part");
	collider.Name = "Collider";
	collider.CanCollide = false;
	collider.Transparency = .5;
	collider.Size = Vector3.new(petObj.Size.X, 1, petObj.Size.Z);
	
	local weld = Instance.new("WeldConstraint");
	weld.Part0 = collider;
	weld.Part1 = petObj;
	
	weld.Parent = collider;
	collider.Parent = petObj;
	petObj.Parent = folder;
	
	petObj:SetNetworkOwner(owner);
	
	local self = setmetatable({
		owner = owner,
		name = name,
		state = "Idle",
		object = petObj,
		animate = true,
		animMultiplier = 4;
		animOffset = 0.25,

		_connections = {},
	}, pet);
	
	return self;
end

function pet:follow(distance, radius, hoverHeight)
	distance = tonumber(distance) or 6;
	radius = math.rad(tonumber(radius) or 0);
	hoverHeight = tonumber(hoverHeight) or 4;
	
	self.object.Collider.Position = self.object.Position - Vector3.new(0, (self.object.Size.Y / 2) + hoverHeight + (self.object.Collider.Size.Y / 2), 0);
	self._connections.follow = runService.Stepped:Connect(function()
		local obj = self.object;
		if not obj then return; end
		
		if not self:updateState() then return; end
		
		local character = self.owner.character;
		local hrp = character.PrimaryPart;
		if not hrp then return; end
		
		--local posXZ = (hrp.CFrame * CFrame.fromEulerAnglesXYZ(0, radius, 0) * CFrame.new(0, 0, distance + (obj.Size.Z / 2))).Position;
		local posXZ = hrp.Position + Vector3.new(math.cos(radius) * (distance + obj.Size.X / 2), 0, math.sin(radius) * (distance + obj.Size.Z / 2));
		
		raycastParams.FilterDescendantsInstances = folder:GetDescendants();
		local res = workspace:Raycast(posXZ + Vector3.new(0, 7.5, 0), Vector3.new(0, -20, 0), raycastParams);
		--print(res.Instance);
		local finalPosition = (res and res.Position) or posXZ - Vector3.new(0, 2.5, 0);
		
		--obj.Collider.Position = finalPosition + Vector3.new(0, obj.Collider.Size.Y, 0);
		for _,p in ipairs(getTouchingParts(obj.Collider) or {}) do
			if table.find(folder:GetDescendants(), p) then continue; end
			
			local posY = p.Position.Y + p.Size.Y / 2;
			if posY > obj.Position.Y - obj.Size.Y / 2 then
				print("sussy")
				finalPosition += Vector3.new(0, posY - finalPosition.Y, 0);
				--warn(finalPosition.Y, obj.Size.Y / 2, hoverHeight);
			end
		end
		--obj.Collider.Position = finalPosition + Vector3.new(0, obj.Collider.Size.Y, 0);
		
		finalPosition += Vector3.new(0, hoverHeight + (obj.Size.Y / 2), 0);
		finalPosition += Vector3.new(0, (self.animate and self.state == "Idle" and math.sin(os.clock() * self.animMultiplier) * self.animOffset) or 0, 0);
		
		obj.Gyro.Attachment0 = obj.Center;
		obj.Move.Attachment0 = obj.Center;
		
		--obj.Gyro.CFrame = CFrame.lookAt(obj.Position, character.Head.Position);
		obj.Gyro.CFrame = hrp.CFrame;
		obj.Move.Position = finalPosition;
	end);
end

function pet:updateState()
	local character = self.owner.Character;
	if not character then return; end
	
	local humanoid = character:FindFirstChild("Humanoid");
	if not humanoid then return; end
	
	local state = humanoid:GetState();
	if humanoid.MoveDirection == Vector3.new(0, 0, 0) and state ~= Enum.HumanoidStateType.Jumping and state ~= Enum.HumanoidStateType.Freefall then
		self.state = "Idle";
	else
		self.state = state.Name;
	end
	
	return self.state;
end

function pet:unfollow()
	if self._connections.follow then	
		self._connections.follow:Disconnect();
		self._connections.follow = nil;
	end
end

function pet:destroy()
	for _,c in ipairs(self._connections) do
		c:Disconnect();
	end
	table.clear(self._connections);
	self.object:Destroy();
	table.clear(self);
	self = nil;
end

return pet;

New thing is there’s a collider part that I was making in order to detect terrain better, but I couldn’t get it to work (I’ll work on it when I get some free time), so please remove that part of the code, or take it as a challenge and fix it :smiley:

(Also I reverted the raycast only when moving thingy because it was acting all weird)

3 Likes

yo thanks but I am having some trouble testing this and applying it to my current pet system, could you send me a studio place if it’s possible?

I think you should make a new topic, this topic is solved and you are asking for help in a post that is already solved.

Hi, would you mind if I used your code, if I can could you tell me what Gyro, Move, Center is for the pet model like are they parts you have inside the model or something.