Your first real tutorial
We are going to build a four-step onboarding flow, one step at a time, and explain every decision. By the end you will have used all three guidance tools and both ways of ending a step.
The flow we want:
- Fly the camera over the spawn area so the player sees where they are.
- Point them at the quest giver across the map.
- Highlight the shop button once they get back.
- Say goodbye.
Step 0 — the skeleton
Every tutorial has the same three lines around it.
const TutorialKit = require(ReplicatedStorage.TutorialKit)
const Tutorial, Step = TutorialKit.Tutorial, TutorialKit.Step
const tutorial = Tutorial.build({
-- steps go here
})
tutorial:Start()
Tutorial.build needs at least one step, so this does not run yet. Let us fill it in.
Step 1 — a camera flight
const Cinematic = TutorialKit.Cinematic
Step.new(1, {
Cinematic.focus(workspace.Landmarks.SpawnStatue, {
Hold = 3,
Distance = 45,
Height = 15,
Advance = true,
}),
})
Reading it in order:
Step.new(1, ...)— the1is the order, not the position in the list. You could write this step last in the file and it would still run first.Cinematic.focustakes the part to look at.Hold = 3lingers there for three seconds.DistanceandHeightframe the shot: 45 studs back, 15 studs up.Advance = truemoves to step 2 when the flight ends.
That last one matters more than it looks. Camera state is captured when the flight starts and restored when it ends whether it finished or was interrupted, and Advance fires in both cases. A player who somehow leaves mid-flight still lands on step 2 rather than getting stuck.
Step 2 — pointing across the map
const PointWorld = TutorialKit.PointWorld
const Custom = TutorialKit.Custom
Step.new(2, {
PointWorld.to(workspace.NPCs.QuestGiver.HumanoidRootPart, {
Outline = workspace.NPCs.QuestGiver,
}),
Custom.new(function(context)
const connection = QuestService.Started:Connect(function()
context:Advance()
end)
context.Janitor:add(connection, "Disconnect")
end),
})
Two actions in one step. They start at the same moment and neither blocks the other: the beam appears immediately, and the listener is armed immediately.
Notice the split of responsibilities. PointWorld shows and never ends the step. Something has to decide when the step is over, and here that is the Custom action listening for the quest to start.
The line that people forget is this one:
context.Janitor:add(connection, "Disconnect")
Without it the listener outlives the step. The player finishes the tutorial, starts a second quest an hour later, and the handler fires again on a tutorial that no longer exists. Registering it on the step's Janitor means leaving the step disconnects it, and you never write cleanup code yourself.
Anything you create inside a Custom action, connections, instances, threads, belongs on context.Janitor. That is the whole contract.
Step 3 — highlighting a button, with a condition
const FocusUI = TutorialKit.FocusUI
Step.new(3, {
FocusUI.to(shopButton, {
Padding = Vector2.new(8, 8),
Clicked = function(context)
if PlayerData:Get("Coins") >= 100 then
context:Advance()
else
NotificationModule:ShowError("Come back with 100 coins")
end
end,
}),
})
This is the long form of Advance = true. Use it when ending the step depends on something the kit cannot know.
Clicked receives the same context every action gets, so it can advance, jump elsewhere with context:GoTo(7), or end the whole tutorial with context:Complete().
Padding gives the highlight 8 pixels of breathing room on each axis. Without it the hole hugs the button exactly, which looks tight on buttons that already have their own stroke.
Step 4 — the goodbye
Step.new(4, {
Dialog.new({
Speaker = "Guide",
Lines = { "That's everything.", "Good luck out there!" },
Choices = { { Text = "Thanks!", Advance = true } },
}),
})
Dialog is the one action you cannot use straight out of the box, because the kit does not draw dialogs. See Dialogs for the ten lines that wire it up.
Advancing past the last step completes the tutorial. That fires Completed and clears everything.
Putting it together
const tutorial = Tutorial.build({
Step.new(1, {
Cinematic.focus(workspace.Landmarks.SpawnStatue, {
Hold = 3, Distance = 45, Height = 15, Advance = true,
}),
}),
Step.new(2, {
PointWorld.to(workspace.NPCs.QuestGiver.HumanoidRootPart, {
Outline = workspace.NPCs.QuestGiver,
}),
Custom.new(function(context)
context.Janitor:add(QuestService.Started:Connect(function()
context:Advance()
end), "Disconnect")
end),
}),
Step.new(3, {
FocusUI.to(shopButton, { Padding = Vector2.new(8, 8), Advance = true }),
}),
Step.new(4, {
Dialog.new({
Speaker = "Guide",
Lines = { "That's everything.", "Good luck out there!" },
Choices = { { Text = "Thanks!", Advance = true } },
}),
}),
})
tutorial.Completed:Connect(function()
print("onboarding done")
end)
tutorial:Start()
Two habits worth picking up now
Number your steps in tens
Step.new(10, ...)
Step.new(20, ...)
Step.new(30, ...)
Inserting a step between the first two later costs nothing. With 1, 2, 3 you renumber everything below it, and any GoTo pointing into that range silently means something else.
Style once, not per step
Tutorial.build(steps, {
Spotlight = {
DimTransparency = 0.55,
StrokeColor = Color3.fromRGB(255, 214, 102),
},
Camera = { FocusDuration = 2 },
})
The spotlight, pointer and camera are shared by every step, so their look is configured once on the tutorial rather than repeated on each action.
Next
- Dialogs — the wiring step 4 needs
- Guidance tools — everything the three tools can do
- Saving progress — resume a returning player