Scripting Game Objects and Services
Introduction
Every element in a Roblox game — parts, characters, UI elements, lights — is an Instance stored in a hierarchical tree that starts at the game service. To build anything in Roblox, you need to understand how that tree is organized, how to create objects, how to find objects that already exist, and which services handle the heavy lifting.
This tutorial walks through all of that using Luau, Roblox’s typed variant of Lua. You’ll write real code you can paste straight into a Roblox Studio LocalScript or Script.
The Roblox Object Hierarchy
The hierarchy starts at game, the root of everything in a Roblox session. Under game live several container services:
- Workspace — holds all gameplay objects: parts, models, terrain, cameras. The world your player sees lives here.
- Players — one
Playerinstance per connected user, each with aCharacterchild that represents their avatar. - ReplicatedStorage — objects shared between server and client. Use this for modules, prefabs, and anything both sides need.
- ServerScriptService — scripts that run on the server only. Game logic lives here.
- ReplicatedFirst — client-side content that loads before the rest of the game.
- StarterGui — UI elements that get copied into each player’s
PlayerGuiwhen they join.
You access services the same way you access any other child: by name. For Players, that’s game.Players. For Workspace, that’s game.Workspace.
For services with spaces in their names, you need GetService:
local RunService = game:GetService("RunService")
local Players = game:GetService("Players")
This is why GetService exists — Run Service has a space, so game.RunService would be a syntax error.
Creating Instances
You create objects with Instance.new, passing the class name as a string. The object is created in memory but not yet placed in the hierarchy. You set its properties, then assign it a parent to insert it.
local part = Instance.new("Part")
part.Name = "MyPart"
part.Size = Vector3.new(4, 1, 2)
part.Position = Vector3.new(0, 5, 0)
part.Anchored = true
part.BrickColor = BrickColor.new("Bright blue")
part.Parent = workspace -- Inserts the part into the Workspace
Setting Parent is what actually places the object into the game world. Until then, it exists only in RAM and will be garbage collected if you don’t hold a reference to it.
You can create any Roblox class this way: SpawnLocation, Model, Script, LocalScript, Fire, PointLight, Attachment, and hundreds more.
Destroying Instances
Call :Destroy() on any instance to remove it and all its descendants from the game:
part:Destroy()
-- The part is now removed from the workspace and locked from further use
After Destroy() is called, the instance is locked — you cannot set its Parent or any of its properties. If you need temporary objects that clean themselves up automatically, see the Debris section below.
Finding and Waiting for Objects
Objects in the hierarchy aren’t always present when your script runs. A player’s Character doesn’t exist until they join. A tool’s Handle doesn’t exist until it’s equipped. Roblox provides two functions for dealing with this.
WaitForChild
WaitForChild yields (pauses) your thread until the child exists, then returns it. This is the right choice when you need an object that may not be there yet:
local Players = game:GetService("Players")
local player = Players.PlayerAdded:Wait()
local character = player.CharacterAdded:Wait()
local humanoid = character:WaitForChild("Humanoid")
-- At this point, humanoid exists and is safe to use
humanoid.HealthChanged:Connect(function(newHealth)
print("Health:", newHealth)
end)
WaitForChild has an optional second argument: a timeout in seconds. If the child isn’t found within that time, it returns nil instead of waiting forever:
local tool = character:WaitForChild("Tool", 5) -- Wait up to 5 seconds
if tool then
print("Tool found")
else
warn("Tool not found within 5 seconds")
end
FindFirstChild
FindFirstChild returns the child immediately if it exists, or nil if it doesn’t. It does not yield, so it won’t pause your thread:
local existingPart = workspace:FindFirstChild("MyPart")
if existingPart then
print("Part already exists")
else
print("Part not found")
end
Use FindFirstChild when you want to check for existence without pausing. Avoid using WaitForChild inside tight loops — if the child never appears, your loop never continues.
Descendant Lookup
Both functions accept a recursive flag as the second argument:
local thing = workspace:FindFirstChild("PartName", true) -- recursive search
local thing = workspace:WaitForChild("PartName", true) -- recursive wait
Recursive lookups scan the entire subtree, which is slower. Only use them when you genuinely don’t know the full path.
Key Services
RunService
RunService fires events every frame and is the heartbeat of your game’s loop. The three main events are:
Heartbeat— fires once per physics frame (roughly 60 times per second). Use for game logic that needs regular updates.Stepped— fires once per physics step, before the physics engine updates. Good for physics-related math.RenderStepped— fires once per render frame, which aligns with the client’s display refresh rate. Use this for camera updates and anything that affects what the player sees.
local RunService = game:GetService("RunService")
local accumulator = 0
RunService.Heartbeat:Connect(function(deltaTime)
accumulator = accumulator + deltaTime
-- deltaTime is the time elapsed since the last frame, in seconds
print("Time since start:", accumulator)
end)
For server-side scripts, Heartbeat and Stepped are reliable. RenderStepped only fires on the client — avoid using it in server scripts.
Debris
Debris schedules automatic cleanup. Pass any instance and a time in seconds, and the service destroys the object after that duration:
local Debris = game:GetService("Debris")
local part = Instance.new("Part")
part.Size = Vector3.new(2, 2, 2)
part.Position = Vector3.new(0, 10, 0)
part.Anchored = true
part.BrickColor = BrickColor.new("Bright red")
part.Parent = workspace
-- Destroy the part after 3 seconds automatically
Debris:AddItem(part, 3)
This pattern is useful for projectiles, particles, visual effects, and temporary obstacles. The object is destroyed regardless of where it is in the hierarchy, so you don’t need to track or manually remove it.
TweenService
TweenService smoothly animates properties over time. You define what to animate with a TweenInfo, then create a tween and play it:
local TweenService = game:GetService("TweenService")
local part = Instance.new("Part")
part.Name = "MovingPart"
part.Size = Vector3.new(4, 1, 2)
part.Position = Vector3.new(0, 2, 0)
part.Anchored = true
part.BrickColor = BrickColor.new("Forest green")
part.Parent = workspace
-- Tween that moves the part over 2 seconds
local tweenInfo = TweenInfo.new(
2, -- duration in seconds
Enum.EasingStyle.Quad, -- easing style
Enum.EasingDirection.Out -- easing direction
)
local tween = TweenService:Create(
part,
tweenInfo,
{Position = Vector3.new(20, 2, 0)} -- target position
)
tween:Play()
-- The part glides smoothly from (0, 2, 0) to (20, 2, 0) over 2 seconds
TweenInfo.new accepts several arguments:
TweenInfo.new(
2, -- duration
Enum.EasingStyle.Quad, -- style: Linear, Quad, Cubic, Quart, Quint, Sine, Expo, Circ, Back, Bounce, Elastic
Enum.EasingDirection.Out, -- direction: In, Out, InOut
-1, -- repeat count (-1 = infinite)
true, -- reverses on repeat
0 -- delay before starting
)
TweenService is client-side only. If you create a tween on the server, it won’t animate for other players. Use RemoteEvents to synchronize animations across clients.
PathfindingService
PathfindingService computes navigation paths for NPCs. You create a path, compute it to a target, and optionally visualize or follow it:
local PathfindingService = game:GetService("PathfindingService")
local path = PathfindingService:CreatePath({
AgentRadius = 2, -- NPC body radius
AgentHeight = 5, -- NPC body height
AgentCanJump = true, -- allow jumping over gaps
})
local humanoid = script.Parent:WaitForChild("Humanoid")
local targetPos = Vector3.new(50, 0, 30)
path:ComputeAsync(humanoid.RootPart.Position, targetPos)
if path.Status == Enum.PathStatus.Success then
local waypoints = path:GetWaypoints()
for i, waypoint in ipairs(waypoints) do
humanoid:MoveTo(waypoint.Position)
humanoid.MoveToFinished:Wait()
print("Reached waypoint", i)
end
else
warn("Path could not be computed:", path.Status)
end
Properties, Methods, and Events
Every Instance has three kinds of members.
Properties are values you can read and write:
part.Transparency = 0.5
print(part.Name) -- "Part"
print(part.Position) -- Vector3 value
Methods are functions you call on the instance:
part:SetAttribute("damage", 25)
part:Destroy()
part:Clone()
Events are signals that fire when something happens. You connect a callback function using :Connect:
local connection
connection = part.Touched:Connect(function(otherPart)
print(part.Name, "touched by", otherPart.Name)
end)
-- Later, disconnect to prevent memory leaks
connection:Disconnect()
Connecting to events and forgetting to disconnect is a common source of memory leaks in Roblox scripts. Always track connections you care about and disconnect them when appropriate, or use the :Connect return value.
Attributes
Beyond built-in properties, you can attach custom data using attributes:
part:SetAttribute("Damage", 50)
part:SetAttribute("IsExplosive", true)
local damage = part:GetAttribute("Damage") -- 50
local isExplosive = part:GetAttribute("IsExplosive") -- true
Attributes are a clean alternative to storing data in object names or using module scripts for simple per-instance data.
Common Patterns
Waiting for the Local Player
Client scripts often need to wait for the local player’s character before doing anything:
local Players = game:GetService("Players")
local player = Players.LocalPlayer
local character = player.CharacterAdded:Wait()
local humanoid = character:WaitForChild("Humanoid")
humanoid.HealthChanged:Connect(function(newHealth)
print("Local player health:", newHealth)
end)
Note that Players.LocalPlayer only exists in LocalScripts and client-side ModuleScripts.
Server-Client Initialization Order
Objects in ReplicatedFirst load on the client before the rest of the game. StarterGui contents copy into PlayerGui when the character spawns. If you need UI or logic to run before the character appears, ReplicatedFirst is the right home.
On the server, GameAdded on Players fires when a new player joins. Scripts in ServerScriptService run at server start, before any players connect.
Object Pooling with Debris
For frequently spawned objects like bullets or damage numbers, create the object, use it, then hand it to Debris instead of destroying it immediately. This avoids the overhead of Instance.new on a hot path:
local Debris = game:GetService("Debris")
local function spawnNumber(position, value)
local billboard = Instance.new("BillboardGui")
local label = Instance.new("TextLabel")
label.Text = tostring(value)
label.Size = UDim2.new(1, 0, 1, 0)
label.Parent = billboard
billboard.Parent = workspace
billboard.Adornee = Instance.new("Part")
billboard.Adornee.Position = position
billboard.Adornee.Parent = workspace
Debris:AddItem(billboard, 2) -- Auto-cleanup after 2 seconds
end
Common Pitfalls
Using dot notation for services with spaces
game.RunService throws an error. Always use game:GetService("RunService").
WaitForChild inside a loop
If you write while true do local obj = character:WaitForChild("Tool") end, the loop waits indefinitely after the first iteration because WaitForChild returns immediately once the child exists. Use FindFirstChild inside loops, or WaitForChild only once at initialization.
Setting Parent after Destroy
Once you call :Destroy(), the instance is locked. Any subsequent attempt to set Parent or read most properties raises an error. Keep a reference before destroying if you need to compare the object later.
Assuming children exist
Never assume a child is present. A Tool may not be equipped. A Humanoid may not be loaded. Use WaitForChild or FindFirstChild with a check before accessing.
TweenService on the server
Tweens are client-side only. A tween created in a server Script plays locally for that server instance but does not replicate to players. Use RemoteEvents to sync animations.
What’s Next
You now know how to navigate the Roblox object tree, create and destroy instances, wait for objects that may not exist yet, and use the core services that power game logic. From here, the natural next steps are:
- RemoteEvents and RemoteFunction — communicate between server and client
- CollectionsService (Tags) — group objects without parenting
- DataStoreService — persist player data across sessions
Each of those builds directly on the patterns covered here.