luaguides

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 Player instance per connected user, each with a Character child 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 PlayerGui when 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.