Scripting Game Objects and Services
Prerequisites
You should have Roblox Studio installed and know how to create a new project and insert scripts. No prior Roblox-specific knowledge is required — this tutorial starts from the fundamentals. The first few examples can be run in any script, so you can follow along even if you’ve never opened the Explorer panel before.
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)
This pattern chains Wait on PlayerAdded and CharacterAdded events with WaitForChild on the Humanoid object. Each step blocks until the next piece of the hierarchy is ready, so the script never tries to access something that hasn’t loaded yet. This is essential during the first few frames after a player joins, when Roblox is still assembling the character model and its components.
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
With the timeout parameter, your script gets a chance to handle the missing object gracefully instead of hanging forever. This matters during development when you might misname a child, and in production when Roblox takes longer than expected to replicate objects from the server to the client. Checking for nil after WaitForChild with a timeout is a defensive pattern worth adopting early.
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
FindFirstChild is non-blocking, so it’s safe to call inside game loops or rapid-fire event handlers where you can’t afford to yield. The trade-off is that you must always check for nil before using the result — the function won’t wait, so the child might genuinely not exist yet.
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
The tween:Play() call starts the animation immediately; it doesn’t block your script. The part moves while the rest of your code keeps running. This non-blocking behaviour is what makes TweenService useful for UI and gameplay animations — you can fire off dozens of tweens at once without freezing anything. After Play() returns, you can optionally connect to tween.Completed to run code when the animation finishes.
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
ComputeAsync is asynchronous — it pauses your script while Roblox’s pathfinding engine calculates the route, then resumes with the result. Calling MoveTo on each waypoint sequentially and waiting for MoveToFinished ensures the NPC completes one leg before starting the next. Without that wait, the NPC would lurch toward waypoints out of order and miss most of them.
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
Properties are the simplest way to interact with an instance — just read or assign them like a Lua table field. Every Roblox class defines its own set of properties; a Part has Size and BrickColor, while a Sound has Volume and PlaybackSpeed. The Object Browser in Roblox Studio lists every property available on each class.
Methods are functions you call on the instance:
part:SetAttribute("damage", 25)
part:Destroy()
part:Clone()
Methods use Lua’s colon syntax (part:Destroy()) rather than dot syntax. The colon passes part as the implicit self parameter, which is how Roblox knows which instance to act on. You can chain method calls when they return the instance — for example, cloning a part and immediately parenting it in one expression.
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. The connection variable holds an RBXScriptConnection object; calling Disconnect on it stops the callback from receiving any more events.
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. They survive Clone() operations, they replicate automatically from server to client, and Studio’s Properties window displays them for easy debugging. For complex structured data, however, attributes are limited to a handful of primitive types — strings, numbers, booleans, and a few others. If you need to store an entire configuration table on a part, a ModuleScript is still the better fit.
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.
Next steps
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:
- Building User Interfaces in Roblox: create interactive ScreenGuis, buttons, and health bars
- RemoteEvents and Client-Server Communication: bridge the gap between server and client scripts
See Also
- Getting Started with Roblox Lua: set up Roblox Studio and write your first Luau scripts
- Roblox UI Scripting: build on-screen interfaces with ScreenGui and TweenService
- Lua Metatables: the metatable system that underlies Roblox’s Instance model