Tutorial

How to make a pet follow the player in Roblox (Luau script)

Make a pet follow the player in Roblox: model setup, a client-side Luau follow script, AlignPosition alternative, circle formation, flying bob and jitter fixes.

Updated 9 min readBy the Proppo team

On this page

Quick answer

Store each player’s equipped pets on the server, then let a LocalScript on every client clone those pets, anchor them with CanCollide off and move them each frame with PivotTo toward a slot next to the HumanoidRootPart, using a frame-rate independent lerp. AlignPosition and AlignOrientation with the player as network owner are the physics-based alternative.

Key takeaways

  • Most large pet games draw pets on each player's device: the server only stores which pets are equipped, and every client moves them.
  • Client pets are anchored with CanCollide off and moved with PivotTo once per frame, so physics and network ownership never cause jitter.
  • A frame-rate independent lerp (alpha = 1 - e^(-speed × dt)) keeps the follow smooth at 30 fps and at 240 fps.
  • AlignPosition and AlignOrientation with the pet's network owner set to the player are the physics-based alternative with no per-frame loop.
  • Keep each pet near 4,000 triangles (2,000 for mobile-first games) because a full server shows dozens at once.

A pet that trails behind the player, hovers when it can fly and spreads out when there are three of them is the most visible system in any pet simulator. This tutorial builds that system in Luau: the model setup, a client-side follow script you can paste in, an AlignPosition alternative, formations, a flying bob, the usual jitter fixes and what to watch with many players. The scripts are free to use in your game. API details are checked against the Roblox Creator Hub as of October 2026.

How do you make a pet follow the player in Roblox?

You move a pet model every frame toward a spot next to the player's HumanoidRootPart, either by setting its pivot with PivotTo on each client or by letting physics constraints such as AlignPosition pull it there.

The first approach treats pets as pure visuals: each player's device clones the pets of everyone nearby and moves them itself. The second keeps one real, unanchored pet on the server and lets the physics engine do the moving. The table compares the two.

  • Who moves the pet

    Client-side CFrame follow
    Every client, for every visible player
    Server pet with AlignPosition
    Physics, simulated by the owning player's device
  • Parts

    Client-side CFrame follow
    Anchored, CanCollide off
    Server pet with AlignPosition
    Unanchored, CanCollide off, extra parts Massless
  • Smoothness

    Client-side CFrame follow
    Very smooth: no physics, no replication delay
    Server pet with AlignPosition
    Smooth for the owner, slightly delayed for others
  • Server cost

    Client-side CFrame follow
    Almost none: one attribute per player
    Server pet with AlignPosition
    One physics assembly per pet
  • Best for

    Client-side CFrame follow
    Pet simulators with many equipped pets
    Server pet with AlignPosition
    Few pets that must touch the world or other parts

Step 1: Prepare the pet model

Create a folder PetModels in ReplicatedStorage and put each pet in it as a Model named after the pet. Every model needs:

  • A PrimaryPart (for a single-mesh pet, the MeshPart itself) and a pivot at the bottom center, so PivotTo puts the pet's feet on the ground.
  • Its front facing -Z, the direction of a part's LookVector. Otherwise the pet walks sideways or backwards.
  • CanCollide, CanTouch and CanQuery off, so pets never push the player, fire Touched events or block raycasts. The script also sets these, but setting them on the template is cleaner.
  • An optional boolean attribute Flying for pets that should hover.

Multi-part pets must be welded together (WeldConstraint to the PrimaryPart) if you use the physics approach. For the client approach every part is anchored and PivotTo moves the whole model at once.

Step 2: Store equipped pets on the server

The server is the source of truth for what a player owns and has equipped. Write the list to an attribute on the Player object. Player attributes set by the server replicate to every client, so each device knows which pets to draw for every player.

Luau
-- Script in ServerScriptService (sketch): the server owns what is equipped
local Players = game:GetService("Players")

local function setEquipped(player: Player, petNames: { string })
	-- validate against the player's saved inventory before this line
	player:SetAttribute("EquippedPets", table.concat(petNames, ","))
end

Players.PlayerAdded:Connect(function(player)
	setEquipped(player, { "BatRay", "GhostJelly" }) -- load these from your save data
end)

In a full game this list comes from your save data and your equip button. The egg hatching tutorial covers saving pets to a DataStore, and an equip limit (a few slots at the start, raised by upgrades or game passes) caps the list.

Step 3: Render and move pets on each client

This LocalScript is the whole follow system. It rebuilds a player's pets when their EquippedPets attribute or character changes, and moves every pet once per frame in RunService.PreRender, which fires on the client before each frame is drawn.

Luau
-- LocalScript in StarterPlayer > StarterPlayerScripts
-- Draws the equipped pets of every player on this device and moves them each frame.
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")

local PetModels = ReplicatedStorage:WaitForChild("PetModels")

local RADIUS = 4 -- studs from the player to each pet
local FOLLOW_SPEED = 10 -- higher = snappier
local FLY_HEIGHT = 2.5 -- extra height for pets with a Flying attribute
local BOB_HEIGHT = 0.35 -- studs up and down
local BOB_RATE = 0.6 -- bobs per second
local DRAW_DISTANCE = 150 -- skip pets this far from the camera

local active = {} -- [Player] = { { model, base, flying } }

local function prepare(model: Model)
	for _, part in model:GetDescendants() do
		if part:IsA("BasePart") then
			part.Anchored = true -- moved by this script, not by physics
			part.CanCollide = false
			part.CanTouch = false
			part.CanQuery = false
		end
	end
end

local function clear(player: Player)
	for _, pet in active[player] or {} do
		pet.model:Destroy()
	end
	active[player] = nil
end

local function build(player: Player)
	clear(player)
	local equipped = player:GetAttribute("EquippedPets")
	if typeof(equipped) ~= "string" or equipped == "" then
		return
	end
	local character = player.Character
	local root = character and character:FindFirstChild("HumanoidRootPart")
	local start = if root then root.CFrame else CFrame.identity
	local list = {}
	for _, name in string.split(equipped, ",") do
		local template = PetModels:FindFirstChild(name)
		if template then
			local model = template:Clone()
			prepare(model)
			model:PivotTo(start)
			model.Parent = workspace
			table.insert(list, { model = model, base = start, flying = template:GetAttribute("Flying") == true })
		end
	end
	active[player] = list
end

local function watch(player: Player)
	player:GetAttributeChangedSignal("EquippedPets"):Connect(function()
		build(player)
	end)
	player.CharacterAdded:Connect(function()
		build(player)
	end)
	build(player)
end

for _, player in Players:GetPlayers() do
	watch(player)
end
Players.PlayerAdded:Connect(watch)
Players.PlayerRemoving:Connect(clear)

RunService.PreRender:Connect(function(dt)
	local camera = workspace.CurrentCamera
	local alpha = 1 - math.exp(-FOLLOW_SPEED * dt) -- same feel at 30 or 240 fps
	local now = os.clock()
	for player, list in active do
		local character = player.Character
		local root = character and character:FindFirstChild("HumanoidRootPart")
		local humanoid = character and character:FindFirstChildOfClass("Humanoid")
		if not root or not humanoid then
			continue
		end
		if (root.Position - camera.CFrame.Position).Magnitude > DRAW_DISTANCE then
			continue
		end
		-- R15: the root part's bottom is HipHeight above the ground
		local ground = -(humanoid.HipHeight + root.Size.Y / 2)
		local count = #list
		for i, pet in list do
			local angle = (i - 1) / count * math.pi * 2 -- even circle; pet 1 sits right behind
			local offset = Vector3.new(math.sin(angle) * RADIUS, ground, math.cos(angle) * RADIUS)
			local bob = 0
			if pet.flying then
				offset += Vector3.new(0, FLY_HEIGHT, 0)
				bob = math.sin((now * BOB_RATE + i / count) * math.pi * 2) * BOB_HEIGHT
			end
			local target = root.CFrame * CFrame.new(offset)
			pet.base = pet.base:Lerp(target, alpha)
			pet.model:PivotTo(pet.base + Vector3.new(0, bob, 0))
		end
	end
end)

Three details carry most of the quality:

  • The lerp alpha. 1 - math.exp(-FOLLOW_SPEED * dt) covers the same share of the remaining distance per second at any frame rate. A fixed alpha like 0.1 would make pets lag on slow phones and snap on 144 Hz screens.
  • Ground height. For R15 characters, Humanoid.HipHeight is the distance from the ground to the bottom of the HumanoidRootPart, so the offset -(HipHeight + root.Size.Y / 2) lands a bottom-pivoted pet on the floor. On R6, use a fixed offset of about -3 studs.
  • Facing. root.CFrame * CFrame.new(offset) keeps the player's rotation, so pets look where the player looks.

Step 4: Arrange several pets

The script spaces pets evenly on a circle: one pet sits behind the player, two sit behind and in front, three form a triangle. If you would rather keep pets out of the player's way, use an arc behind them, or rows for games with large equip limits:

Luau
-- Swap the offset line in the loop above for one of these.
-- Arc behind the player: pets fan out instead of surrounding them.
local SPREAD = math.rad(35) -- angle between neighbours
local function arcSlot(i: number, count: number): Vector3
	local angle = (i - (count + 1) / 2) * SPREAD
	return Vector3.new(math.sin(angle) * RADIUS, 0, math.cos(angle) * RADIUS)
end

-- Rows of three behind the player, for big equip limits.
local function gridSlot(i: number): Vector3
	local column = (i - 1) % 3 - 1 -- -1, 0, 1
	local row = math.floor((i - 1) / 3)
	return Vector3.new(column * 3, 0, RADIUS + row * 3)
end

Flying pets get FLY_HEIGHT plus a sine-wave bob. The i / count phase shift makes neighbours bob out of step, which looks less mechanical. The bob is added after the lerp, so smoothing never flattens it.

How do you make a pet follow with AlignPosition instead?

Create the pet on the server, unanchor it, and let an AlignPosition and an AlignOrientation in two-attachment mode pull it toward an Attachment parented to the player's HumanoidRootPart. No loop is needed, because the attachment moves with the player.

Luau
-- Script in ServerScriptService: physics follow with AlignPosition + AlignOrientation.
-- No per-frame loop: the constraints pull the pet toward an attachment on the player.
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local PetModels = ReplicatedStorage:WaitForChild("PetModels")
local petFolder = workspace:FindFirstChild("Pets") or Instance.new("Folder")
petFolder.Name = "Pets"
petFolder.Parent = workspace

local function spawnPet(player: Player, petName: string, offset: Vector3): Model?
	local character = player.Character
	local root = character and character:FindFirstChild("HumanoidRootPart")
	local template = PetModels:FindFirstChild(petName)
	if not root or not template then
		return nil
	end

	local pet = template:Clone()
	local primary = pet.PrimaryPart
	for _, part in pet:GetDescendants() do
		if part:IsA("BasePart") then
			part.Anchored = false
			part.CanCollide = false
			part.CanTouch = false
			part.CanQuery = false
			part.Massless = part ~= primary -- extra parts (welded to primary) add no mass
		end
	end

	local slot = Instance.new("Attachment") -- the spot the pet chases
	slot.Name = "PetSlot"
	slot.Position = offset
	slot.Parent = root

	local hold = Instance.new("Attachment")
	hold.Parent = primary

	local align = Instance.new("AlignPosition")
	align.Mode = Enum.PositionAlignmentMode.TwoAttachment
	align.Attachment0 = hold
	align.Attachment1 = slot
	align.Responsiveness = 25
	align.MaxForce = math.huge
	align.Parent = primary

	local turn = Instance.new("AlignOrientation")
	turn.Mode = Enum.OrientationAlignmentMode.TwoAttachment
	turn.Attachment0 = hold
	turn.Attachment1 = slot
	turn.Responsiveness = 20
	turn.MaxTorque = math.huge
	turn.Parent = primary

	pet:PivotTo(root.CFrame * CFrame.new(offset))
	pet.Parent = petFolder
	primary:SetNetworkOwner(player) -- the owner simulates it next to their character: smooth for them
	return pet
end

return spawnPet
  • Network ownership. The server owns unanchored parts by default and can hand them to nearby clients on its own. Calling SetNetworkOwner(player) after the pet is parented to the workspace makes the owner simulate it together with their character, which removes most stutter for them (see Roblox's network ownership docs and the glossary entry).
  • Massless only counts for parts welded to a part with mass that are not the assembly's root, per Roblox's assemblies docs. That is why the script keeps the PrimaryPart massive and marks only the extra parts Massless.
  • Tuning. Responsiveness sets how fast the pet closes the gap. MaxForce and MaxTorque set to math.huge stop heavy or top-heavy pets from sagging or tipping over. Property names follow the AlignPosition and AlignOrientation reference.

Why does my pet jitter or lag behind?

Jitter almost always comes from two systems moving the same pet: physics and a script, or the server and a client. Pick one owner for the motion and the stutter goes away.

The pet stutters when the server moves it

Setting a pet's CFrame from a server Script every Heartbeat replicates each step to clients with network delay. Move pets on the client (step 3) or use constraints with the player as network owner.

The pet shakes while standing still

The pet is unanchored and colliding with the character or the floor. Anchor client pets, and turn CanCollide off for both approaches.

The pet is smooth for me but laggy for others

Expected with the constraint approach: other players see the owner's simulation through replication. Client-side rendering avoids this because every device runs its own copy.

The pet flips or spins

AlignOrientation can't hold a top-heavy pet with a low MaxTorque. Raise MaxTorque, or switch to the anchored client approach.

The pet floats or sinks into the floor

The pivot isn't at the bottom of the model. Fix it in your 3D tool or with Studio's pivot tool; our import guide shows how.

How many pets can a server handle?

The script cost is small. The limit is usually rendering: every client draws every visible pet, so triangle count, part count and texture size per pet matter more than the follow code.

A worked example: 12 players with 3 pets each puts 36 pets in the scene. At 4,000 triangles each that is 144,000 triangles of pets alone, before the map. Rules of thumb (heuristics, not Roblox limits):

  • One MeshPart per pet with one texture. Many small parts cost more than one mesh with the same triangles.
  • A triangle budget of about 4,000 per pet, or 2,000 if most players are on phones, with 512px textures. The triangle and texture guide explains the numbers.
  • Skip distant pets. DRAW_DISTANCE stops moving pets far from the camera. For big servers, also parent those models to nil and back when the player comes closer.
  • One loop, not one script per pet. A single PreRender connection for all pets is cheaper than dozens of scripts.
  • Test on a phone or with Studio's device emulator, using a local server with several test players.

Step 5: Test with a full server

In Studio, open the Test tab, choose a local server with two or more players and start it. Check that every window shows everyone's pets, that pets rebuild after a player resets, and that equipping or unequipping updates all clients. Then walk, jump and stop to tune FOLLOW_SPEED and RADIUS. Next steps: animate the pets with an idle and a walk loop (animation guide) and fill the eggs that give them out (pet simulator overview).

Frequently asked questions

Should pets follow the player on the client or the server?
For most pet simulators, on the client. The server stores which pets each player has equipped, and every client draws and moves those pets locally. That is smoother and costs the server almost nothing. Keep gameplay checks on the server and never trust a client pet's position.
How do I stop my pet from pushing the player?
Turn CanCollide off on every part of the pet, plus CanTouch and CanQuery so it fires no Touched events and doesn't block raycasts. Client-side pets should also be anchored, because the script moves them, not physics.
Is BodyPosition still okay for pets?
No. BodyPosition and BodyGyro are deprecated body movers. Use AlignPosition and AlignOrientation, or move anchored pets with PivotTo on the client.
How do I make a pet fly and bob up and down?
Add a height offset to the pet's follow position, then add math.sin(time × rate × 2π) × amplitude to the Y position after the smoothing step. Give each pet a different phase so several flying pets don't bob in sync.
How do I make several pets follow in a circle?
Give each pet a slot angle of (index - 1) / count × 2π and place it at sin(angle) × radius, cos(angle) × radius relative to the HumanoidRootPart. For pets that stay behind the player, spread the angles over an arc instead of the full circle.
Why does my pet face the wrong way?
Roblox treats -Z (a part's LookVector) as forward. If the model's front points another way, rotate it in your 3D tool before export or adjust World Forward in the 3D Importer, then re-import.

Fill PetModels with your own pets

Describe a pet or upload a drawing. Proppo makes the 3D model at Roblox scale with a bottom-center pivot and a pet triangle budget, ready for the follow script.

60 free credits when you confirm your email. No card needed.

All guides