How do I make a pet that follows the player in Roblox?
Move the pet on the client, every frame, with CFrame maths — not with a BodyMover and not on the server. The pet is one anchored model that lerps toward an offset behind the character. The server only decides which pet is equipped; the following itself is local and costs nothing.
In short
- Anchored parts and RenderStepped, never physics: physics pets fight the character.
- One offset per pet slot, so two pets never sit inside each other.
- The server owns which pet is equipped; the client owns where it is.
The pets that feel good are not simulated, they are drawn. A pet moved by physics on the server shoves the player on a slope, lags behind on a 200ms ping and costs a network update every frame. The same pet interpolated on the client sits exactly where it should, for free.
Steps
1. Prepare the model
One Model with a PrimaryPart, every part anchored and non-collidable. A pet that collides is a pet that pushes the player off a bridge.
local function prepare(model: Model): Model
for _, part in model:GetDescendants() do
if part:IsA("BasePart") then
part.Anchored = true
part.CanCollide = false
part.CanQuery = false
part.CanTouch = false
part.Massless = true
end
end
if not model.PrimaryPart then
model.PrimaryPart = model:FindFirstChildWhichIsA("BasePart")
end
return model
end
2. Give each slot its own offset
Three pets in the same place look like one pet. The offsets are a small table, in studs, relative to the character.
local SLOTS = {
Vector3.new(-2.5, 1.5, 3),
Vector3.new(2.5, 1.5, 3),
Vector3.new(0, 1.5, 4.5),
}
3. Follow on RenderStepped
Lerp is the whole trick: move a fraction of the remaining distance each frame and the pet trails naturally without any easing code.
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local player = Players.LocalPlayer
local function follow(model: Model, slot: number)
local offset = SLOTS[slot] or SLOTS[1]
local bob = 0
RunService.RenderStepped:Connect(function(dt: number)
local char = player.Character
local root = char and char:FindFirstChild("HumanoidRootPart")
if not root or not model.PrimaryPart then return end
bob += dt
local goal = root.CFrame * CFrame.new(offset + Vector3.new(0, math.sin(bob * 3) * 0.25, 0))
-- Face the way the player faces, and keep the pet upright.
goal = CFrame.new(goal.Position) * CFrame.Angles(0, select(2, root.CFrame:ToEulerAnglesYXZ()), 0)
-- 0.15 per frame at 60fps is a soft trail; 1 would glue it to the offset.
model:PivotTo(model.PrimaryPart.CFrame:Lerp(goal, math.min(1, dt * 9)))
end)
end
4. Equip from the server
The server says which pets are out; the client builds them. The list travels, not the models.
-- Server
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local equipped = Instance.new("RemoteEvent")
equipped.Name = "PetsEquipped"
equipped.Parent = ReplicatedStorage
local MAX_OUT = 3
local function setEquipped(player: Player, petIds: { string })
local out = {}
for i = 1, math.min(#petIds, MAX_OUT) do
out[i] = petIds[i]
end
-- Everyone sees everyone's pets, so this goes to all clients.
equipped:FireAllClients(player, out)
end
-- Client
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local models = ReplicatedStorage:WaitForChild("PetModels")
local equipped = ReplicatedStorage:WaitForChild("PetsEquipped")
local live: { [Player]: { Model } } = {}
equipped.OnClientEvent:Connect(function(owner: Player, petIds: { string })
for _, m in live[owner] or {} do
m:Destroy()
end
local spawned = {}
for i, id in petIds do
local source = models:FindFirstChild(id)
if source then
local m = prepare(source:Clone())
m.Parent = workspace
follow(m, i)
spawned[#spawned + 1] = m
end
end
live[owner] = spawned
end)
How to move the pet
| Method | Feels right | Costs network | Pushes the player | Verdict |
|---|---|---|---|---|
Client RenderStepped + Lerp | yes | nothing | no | What to build |
AlignPosition on the server | sometimes | every frame | yes, on slopes | Avoid for pets |
BodyPosition / BodyGyro | no | every frame | yes | Deprecated; do not |
| Welding to the character | no — rigid | nothing | no | Only for a backpack, not a pet |
FAQ
Should the pet be in the character model?
No. Parent it to workspace (or to a folder in it) and move it with PivotTo. Inside the character it inherits the character's physics and the Humanoid starts treating it as part of the body.
Why does my pet lag behind when the player runs?
Because the lerp factor is too small, or because you are moving it on the server. Raise dt * 9 toward dt * 16 for a tighter follow. If it is on the server, no factor will fix it: the position is arriving over the network.
Do other players see the pet?
Only if the equip event goes to every client, as above, and every client builds the model locally. A pet created on the server is replicated to everyone and costs you an update per frame — which is the thing this design exists to avoid.
How many pets can a player have out?
Three is a sensible cap, and the cap belongs on the server (MAX_OUT). The client could draw thirty; the thing that stops it is that the server never names more than three.
Updated: Tested in Studio: Roblox Studio, October 2026Author: GustavoLicense: Code in this guide may be used in any Roblox experience, including a paid one
Related pack
