Appearance
Services and Events
Written: 2026-10-14
Learning objectives
- You can get each service once with
game:GetService. - You can name the key services and their jobs.
- You can connect, fire once, and wait on events.
- You can explain deferred events.
- You can wire a small service-driven system.
Prerequisites
Get each service once
Services give you engine features. You reach them through game. The services guide sets the convention: retrieve a service once per script and name the variable after the service.
Script in ServerScriptService
luau
--!strict
local Players = game:GetService("Players")
local function greet(player: Player): ()
print(player.Name)
end
for _, player in Players:GetPlayers() do
greet(player)
end
Players.PlayerAdded:Connect(greet)Get Players once. Add a local function. Handle current players. Connect future players. Expected behavior: Output prints every current player name, then prints each newcomer name. This also covers players who joined before the script ran; you revisit that loop in Players and Characters.
Key services tour
Learn these nine first. Each one answers a common need.
Workspace holds the 3D world. You parent visible parts and models there. Players tracks everyone in the game. It fires PlayerAdded and PlayerRemoving.
ReplicatedStorage is visible to server and clients. You store shared modules and remotes there. ServerScriptService holds server-only scripts and modules. Clients never see inside it.
TweenService animates numeric properties with easing and timing controls. RunService drives per-frame work and time checks. UserInputService reports device input on the client.
Lighting holds global light settings such as atmosphere and sky objects. SoundService controls global audio behavior and sound groups.
Script in ServerScriptService
luau
--!strict
local TweenService = game:GetService("TweenService")
local Workspace = game:GetService("Workspace")
local target = Instance.new("Part")
target.Name = "Mover"
target.Size = Vector3.new(2, 2, 2)
target.Anchored = true
target.Position = Vector3.new(0, 5, 0)
local info = TweenInfo.new(2, Enum.EasingStyle.Sine, Enum.EasingDirection.Out)
local tween = TweenService:Create(target, info, { Position = Vector3.new(0, 10, 0) })
target.Parent = Workspace
tween:Play()Build the part fully before parenting it. Create tween timing with TweenInfo. Ask TweenService to interpolate Position. Parent last. Play the tween. Expected behavior: a part appears and glides upward over two seconds.
LocalScript in StarterPlayerScripts
luau
--!strict
local UserInputService = game:GetService("UserInputService")
local function onJumpRequest(input: InputObject, processed: boolean): ()
if processed then
return
end
if input.KeyCode == Enum.KeyCode.Space then
print("Jump pressed")
end
end
UserInputService.InputBegan:Connect(onJumpRequest)This runs on the client. Connect one input event. Ignore input the game already handled. React to the space key. Expected behavior: Output prints Jump pressed when you press space outside chat and menus.
Connect, Once, and Wait
An event fires when something happens. You respond with Connect, Once, or Wait.
Connect runs your function on every fire. It returns a connection you can disconnect later. Once runs your function on the first fire only, which the events guide recommends over manual connect-then-disconnect. Wait yields your thread until the next fire and returns the event arguments.
Script in ServerScriptService
luau
--!strict
local Workspace = game:GetService("Workspace")
local plate = Instance.new("Part")
plate.Name = "WelcomeMat"
plate.Size = Vector3.new(8, 1, 8)
plate.Anchored = true
plate.Position = Vector3.new(0, 1, 0)
plate.Parent = Workspace
plate.Touched:Connect(function(hit: BasePart): ()
print(hit.Name)
end)Create a floor pad. Set size and position before parenting. Connect Touched. The handler receives the part that touched. Expected behavior: Output prints toucher names whenever something touches the pad.
Script in ServerScriptService
luau
--!strict
local Workspace = game:GetService("Workspace")
local door = Instance.new("Part")
door.Name = "FirstTouchDoor"
door.Size = Vector3.new(4, 6, 1)
door.Anchored = true
door.Position = Vector3.new(10, 4, 0)
door.Parent = Workspace
local first = door.Touched:Wait()
print(first.Name)
door.Touched:Once(function(hit: BasePart): ()
print(hit.Name)
end)Both one-shot styles run in order. Wait yields for the first touch and prints it. Once then arms for the next touch and prints that one. Expected behavior: Output prints the first touch once from Wait, then prints the next touch once from Once. Connect both before any fire and they would both catch the same first touch, so sequence them when you want separate touches.
Deferred events change timing
Deferred events do not run handlers immediately. The engine queues them until the next resumption point. Order stays predictable, but nested fires run later than beginners expect.
Do not read a flag right after you trigger its event in the same thread. The handler may not have run yet. Yield with task.wait or restructure around the handler instead. The events guide points you to this deferred behavior for performance and consistency.
Script in ServerScriptService
luau
--!strict
local Workspace = game:GetService("Workspace")
local flag = Instance.new("BoolValue")
flag.Name = "Ready"
flag.Value = false
flag.Parent = Workspace
flag.Changed:Connect(function(): ()
print(flag.Value)
end)
flag.Value = true
task.wait()
print(flag.Value)Connect Changed first. Flip the value. Yield one scheduler step. Read the value after the queue has run. Expected behavior: Output prints true from the handler and again from the final print.
Common mistakes
Mistake 1: calling GetService for the same service twice.
Wrong example below. It repeats the lookup.
Script in ServerScriptService
luau
--!strict
local Players = game:GetService("Players")
local Again = game:GetService("Players")
print(#Players:GetPlayers() + #Again:GetPlayers())It works but it breaks the services convention. Duplicate lookups clutter larger scripts. Fix it by binding once and reusing the variable.
Script in ServerScriptService
luau
--!strict
local Players = game:GetService("Players")
print(#Players:GetPlayers())Mistake 2: assuming the handler ran immediately.
Wrong example below. It reads the flag too soon.
Script in ServerScriptService
luau
--!strict
local Workspace = game:GetService("Workspace")
local done = false
local plate = Workspace:FindFirstChild("WelcomeMat")
if plate and plate:IsA("BasePart") then
plate.Touched:Connect(function(): ()
done = true
end)
end
print(done)It prints false because deferred handlers run later. The deferred page describes this queue. Fix it by doing follow-up work inside the handler or after an explicit yield.
Mistake 3: never disconnecting a temporary connection.
Wrong shape below. It stays connected after its job ends.
LocalScript in StarterPlayerScripts
luau
--!strict
local UserInputService = game:GetService("UserInputService")
UserInputService.InputBegan:Connect(function(input: InputObject): ()
if input.KeyCode == Enum.KeyCode.E then
print("Used")
end
end)It keeps printing for the whole session. Fix it by saving the connection and disconnecting, or by using Once for one-shot behavior described in events.
Exercises
- Connect a Touched handler that prints toucher names.
- Use Once to reward only the first touch.
- Combine a service lookup with a deferred-safe read.
Solution 1
Script in ServerScriptService
luau
--!strict
local Workspace = game:GetService("Workspace")
local pad = Instance.new("Part")
pad.Name = "ExercisePad"
pad.Size = Vector3.new(6, 1, 6)
pad.Anchored = true
pad.Position = Vector3.new(0, 1, 12)
pad.Parent = Workspace
pad.Touched:Connect(function(hit: BasePart): ()
print(hit.Name)
end)Expected behavior: touching the pad prints the touching part name.
Solution 2
Script in ServerScriptService
luau
--!strict
local Workspace = game:GetService("Workspace")
local chest = Instance.new("Part")
chest.Name = "FirstReward"
chest.Size = Vector3.new(3, 3, 3)
chest.Anchored = true
chest.Position = Vector3.new(6, 3, 0)
chest.Parent = Workspace
chest.Touched:Once(function(hit: BasePart): ()
print(hit.Name)
end)Expected behavior: only the first touch prints.
Solution 3
Script in ServerScriptService
luau
--!strict
local Workspace = game:GetService("Workspace")
local marker = Instance.new("BoolValue")
marker.Name = "ExerciseReady"
marker.Value = false
marker.Parent = Workspace
marker.Changed:Connect(function(): ()
print(marker.Value)
end)
marker.Value = true
task.wait()
print(marker.Value)Expected behavior: Output prints true twice, once from the handler and once after the yield.
Summary
Get services once. Pick the right tool for the job. Connect with care. Respect deferred timing. Next, schedule work cleanly in Task and Async.