Treat a Player as the durable identity and each Character as a replaceable session with its own
references, connections, animation tracks, and cleanup. Targets Roblox's rolling platform APIs.
When not to use: general physics queries and constraints belong to roblox-physics; remote
trust belongs to roblox-networking; camera logic belongs to camera-systems.
StarterCharacter,
StarterCharacterScripts, CharacterAutoLoads, R6/R15 support, existing Animate/controller
scripts, tools, tags, collision groups, and server/client ownership.CharacterAdded, then bind player.Character
if present. Do not assume event subscription alone sees a character that already spawned.Humanoid, root, Animator, and rig assumptions. Abort if that character is no longer
current before applying delayed work.AssemblyLinearVelocity, BasePart:ApplyImpulse(), or a LinearVelocity/AlignPosition
constraint only for mechanics that need physical control. Keep gameplay
authority and network ownership implications explicit.Animator; store tracks/connections; use named
markers for gameplay timing only with server validation; stop/disconnect on character cleanup.local Players = game:GetService("Players")
local player = Players.LocalPlayer
local generation = 0
local connections: {RBXScriptConnection} = {}
local currentCharacter: Model? = nil
local function clearCharacter()
generation += 1
for _, connection in connections do connection:Disconnect() end
table.clear(connections)
currentCharacter = nil
end
local function bindCharacter(character: Model)
-- Guard BEFORE teardown. A stale invocation (see the CharacterAdded/defer race below) must not
-- clear a binding that is already current, or nothing ends up bound at all.
if player.Character ~= character then return end
clearCharacter()
currentCharacter = character
local thisGeneration = generation
local humanoid = character:WaitForChild("Humanoid", 10)
local root = character:WaitForChild("HumanoidRootPart", 10)
-- Re-check after the yields: a respawn during WaitForChild bumps generation and makes this call stale.
if not humanoid or not root or generation ~= thisGeneration then return end
table.insert(connections, humanoid.Died:Connect(function()
if generation ~= thisGeneration then return end
setCharacterUiEnabled(false)
end))
attachCurrentCharacterSystems(character, humanoid, root)
end
player.CharacterRemoving:Connect(function(character)
if currentCharacter == character then clearCharacter() end
end)
player.CharacterAdded:Connect(bindCharacter)
if player.Character then task.defer(bindCharacter, player.Character) end
Use the project's cleanup utility when one exists; do not introduce a new framework for three
connections. Server systems repeat this binding per Player and clear player-scope tables on
PlayerRemoving.
local animation = Instance.new("Animation")
animation.AnimationId = "rbxassetid://1234567890"
local track = animator:LoadAnimation(animation)
local markerConnection = track:GetMarkerReachedSignal("Commit"):Connect(function(parameter)
playLocalSwingEffect(parameter) -- presentation; server still validates any hit
end)
track:Play(0.1)
-- On character teardown:
markerConnection:Disconnect()
track:Stop(0.1)
animation:Destroy()
For rigs without a Humanoid, use an AnimationController with an Animator. Do not use the
deprecated convenience path as a substitute for owning the actual Animator and track lifecycle.
Humanoid.RigType only where topology materially differs.HumanoidRootPart is the usual character assembly root, not a universal guarantee for every
custom model. Define the custom rig contract and validate it at spawn.Humanoid:Move()/standard controls for ordinary avatar locomotion. Directly changing
AssemblyLinearVelocity is an instantaneous physical action; use forces/constraints or impulses
when continuous or instantaneous physics is the real intent.HumanoidDescription when editing avatar appearance.| Symptom | Likely cause | Remedy |
|---|---|---|
| works once, breaks after reset | cached character/Humanoid/root | rebuild per CharacterAdded; clear on removal |
| callbacks fire twice after deaths | old character connections survived | character-scoped cleanup and generation/current checks |
| delayed load edits wrong rig | async work outlived spawn | compare current Character/generation after every yield |
| animation visible only locally or not at all | wrong Animator/context/asset ownership | inspect rig Animator, execution side, permissions, and replication |
| hit marker awards impossible hit | animation marker trusted as authority | use marker for timing/presentation; server validates combat state |
| R6/custom rig errors | R15 names assumed | define rig contract; attachments/adapter; test each supported rig |
| tool disappears from system | fixed Backpack/Character parent assumed | handle equip/unequip ancestry and character replacement |
| custom force fights Humanoid | two controllers own motion | choose one movement authority per state and restore cleanly |
references/lifecycle-and-animation.md for server binding, death vs removal, rig and
animation verification, custom characters, and the lifecycle stress matrix.roblox-physics — forces, constraints, assemblies, collision, and network ownership.roblox-networking — server validation of character actions and stale requests.input-systems — action mapping and responsive movement intent.camera-systems — camera behavior following replaceable characters.https://create.roblox.com/docs/characters
https://create.roblox.com/docs/animation/using
https://create.roblox.com/docs/characters/appearance