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
PivotToputs 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
Flyingfor 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.
-- 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.
-- 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.HipHeightis 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:
-- 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)
endFlying 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.
-- 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.
MaxForceandMaxTorqueset tomath.hugestop 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_DISTANCEstops 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?
How do I stop my pet from pushing the player?
Is BodyPosition still okay for pets?
How do I make a pet fly and bob up and down?
How do I make several pets follow in a circle?
Why does my pet face the wrong way?
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.
Related guides
All guides
TutorialTutorial
How to animate a pet in Roblox (idle, walk, fly)
Bones and weights, looping clips, the import and publish steps, and a script that switches idle to walk.
7 min readUpdated October 31, 2026Read
TutorialTutorial
How to import a 3D model into Roblox Studio (FBX, textures, rigs)
3D Importer settings that matter, rigs and animations, textures and the usual import errors.
7 min readUpdated October 31, 2026Read
ReferenceReference
Roblox triangle and texture limits for mobile-friendly pets
The hard limits, the budgets pet games actually use, and how to hit them without losing the silhouette.
5 min readUpdated October 31, 2026Read
