Skip to content

Advanced Usage

Learn advanced patterns and techniques for building sophisticated tutorial systems with SpotlightUI.


Conditional Tutorial Flows

Create tutorials that branch based on player choices or game state.

local SpotlightUI = require(ReplicatedStorage.SpotlightUI)
local player = game.Players.LocalPlayer

local function startCombatTutorial()
    local combat = SpotlightUI.new()
    combat:SetSteps({
        { UI = gui.AttackButton, Text = "Click to attack enemies", Pulse = 10 },
        { UI = gui.DefendButton, Text = "Click to block incoming attacks" },
        { Part = workspace.TrainingDummy, Text = "Practice on this dummy" }
    })
    combat:Start()
end

local function startBuildingTutorial()
    local building = SpotlightUI.new()
    building:SetSteps({
        { UI = gui.BuildMenu, Text = "Open the build menu", Pulse = 8 },
        { UI = gui.PlaceButton, Text = "Select and place structures" }
    })
    building:Start()
end

gui.CombatButton.Activated:Connect(startCombatTutorial)
gui.BuildingButton.Activated:Connect(startBuildingTutorial)

Multi-Phase Tutorials

Break complex tutorials into phases that can be triggered independently.

local TutorialManager = {}
TutorialManager.currentPhase = 0
TutorialManager.spotlights = {}

function TutorialManager:StartPhase(phase)
    if self.spotlights[self.currentPhase] then
        self.spotlights[self.currentPhase]:Destroy()
    end

    self.currentPhase = phase
    local spotlight = SpotlightUI.new()
    self.spotlights[phase] = spotlight

    if phase == 1 then
        spotlight:SetSteps({
            { UI = gui.MovementKeys, Text = "Use WASD to move" },
            { UI = gui.JumpButton, Text = "Press Space to jump" }
        })
    elseif phase == 2 then
        spotlight:SetSteps({
            { Part = workspace.Chest, Text = "Collect items from chests" },
            { UI = gui.Inventory, Text = "View items in your inventory" }
        })
    elseif phase == 3 then
        spotlight:SetSteps({
            { Part = workspace.QuestNPC, Text = "Talk to NPCs for quests" },
            { UI = gui.QuestLog, Text = "Track your active quests here" }
        })
    end

    spotlight.sequenceCompleted:Connect(function()
        player:SetAttribute("TutorialPhase" .. phase, true)
    end)

    spotlight:Start()
end

player:GetAttributeChangedSignal("Level"):Connect(function()
    local level = player:GetAttribute("Level")
    if level == 5 and not player:GetAttribute("TutorialPhase2") then
        TutorialManager:StartPhase(2)
    elseif level == 10 and not player:GetAttribute("TutorialPhase3") then
        TutorialManager:StartPhase(3)
    end
end)

TutorialManager:StartPhase(1)

Interactive Step Validation

Ensure players complete actions before advancing through tutorial steps.

local spotlight = SpotlightUI.new()
local currentStepIndex = 0
local stepValidations = {}

stepValidations[1] = function()
    return gui.PlayButton.Activated:Wait()
end

stepValidations[2] = function()
    return gui.InventoryButton.Activated:Wait()
end

stepValidations[3] = function()
    local character = player.Character
    local hrp = character and character:FindFirstChild("HumanoidRootPart")
    if not hrp then return end

    repeat
        task.wait(0.5)
    until (hrp.Position - workspace.TargetLocation.Position).Magnitude < 10
end

spotlight:SetSteps({
    { UI = gui.PlayButton, Text = "Click the play button", Shape = "Circle", Pulse = 10 },
    { UI = gui.InventoryButton, Text = "Open your inventory", Shape = "Square" },
    { Part = workspace.TargetLocation, Text = "Walk to this location", Shape = "Circle" }
})

spotlight.stepCompleted:Connect(function(stepIndex)
    currentStepIndex = stepIndex

    if stepValidations[stepIndex] then
        task.spawn(function()
            stepValidations[stepIndex]()
            spotlight:Next()
        end)
    else
        task.wait(2)
        spotlight:Next()
    end
end)

spotlight:Start()

Context-Aware Tutorials

Show different tutorials based on player experience or preferences.

local TutorialSystem = {}
TutorialSystem.hasSeenTutorial = {}

function TutorialSystem:ShouldShowTutorial(tutorialName)
    if player:GetAttribute("TutorialsDisabled") then
        return false
    end

    if self.hasSeenTutorial[tutorialName] then
        return false
    end

    local level = player:GetAttribute("Level") or 1
    if tutorialName == "Basic" and level > 5 then
        return false
    end

    return true
end

function TutorialSystem:ShowShopTutorial()
    if not self:ShouldShowTutorial("Shop") then return end

    local spotlight = SpotlightUI.new()
    spotlight:SetSteps({
        { UI = gui.ShopButton, Text = "Click to open the shop" },
        { UI = gui.ShopFrame.BuyButton, Text = "Purchase items here" },
        { UI = gui.ShopFrame.SellButton, Text = "Sell unwanted items" }
    })

    spotlight.sequenceCompleted:Connect(function()
        self.hasSeenTutorial["Shop"] = true
        player:SetAttribute("SeenShopTutorial", true)
    end)

    spotlight:Start()
end

gui.ShopButton.Activated:Connect(function()
    TutorialSystem:ShowShopTutorial()
end)

Spotlight Hints with Custom Actions

Add buttons or interactive elements to spotlight hints.

local spotlight = SpotlightUI.new()

local hint = spotlight._hint

local skipButton = Instance.new("TextButton")
skipButton.Size = UDim2.new(0, 60, 0, 25)
skipButton.Position = UDim2.new(1, -70, 1, -30)
skipButton.Text = "Skip"
skipButton.BackgroundColor3 = Color3.fromRGB(200, 50, 50)
skipButton.TextColor3 = Color3.new(1, 1, 1)
skipButton.BorderSizePixel = 0
skipButton.Parent = hint

local corner = Instance.new("UICorner")
corner.CornerRadius = UDim.new(0, 4)
corner.Parent = skipButton

skipButton.Activated:Connect(function()
    spotlight:Skip()
end)

spotlight:SetSteps({
    { UI = gui.Button1, Text = "Step 1 - Click or skip", Pulse = 10 },
    { UI = gui.Button2, Text = "Step 2 - Almost done" }
})

spotlight:Start()

Spotlight State Persistence

Save and restore tutorial progress across sessions.

local DataStoreService = game:GetService("DataStoreService")
local TutorialData = DataStoreService:GetDataStore("TutorialProgress")

local TutorialManager = {}

function TutorialManager:SaveProgress(tutorialName, stepIndex)
    local success, err = pcall(function()
        TutorialData:SetAsync(player.UserId .. "_" .. tutorialName, {
            stepIndex = stepIndex,
            timestamp = os.time()
        })
    end)

    if not success then
        warn("Failed to save tutorial progress:", err)
    end
end

function TutorialManager:LoadProgress(tutorialName)
    local success, data = pcall(function()
        return TutorialData:GetAsync(player.UserId .. "_" .. tutorialName)
    end)

    if success and data then
        return data.stepIndex or 0
    end
    return 0
end

function TutorialManager:StartResumableTutorial(tutorialName, steps)
    local spotlight = SpotlightUI.new()
    local savedStep = self:LoadProgress(tutorialName)

    spotlight:SetSteps(steps)

    spotlight.stepCompleted:Connect(function(stepIndex)
        self:SaveProgress(tutorialName, stepIndex)
    end)

    spotlight.sequenceCompleted:Connect(function()
        self:SaveProgress(tutorialName, #steps)
    end)

    spotlight:Start()
    for i = 1, savedStep do
        spotlight:Next()
    end
end

TutorialManager:StartResumableTutorial("MainQuest", {
    { UI = gui.QuestButton, Text = "Open quests" },
    { Part = workspace.QuestGiver, Text = "Talk to the quest giver" },
    { UI = gui.ObjectiveTracker, Text = "Track your objectives" }
})

Dynamic Spotlight Positioning

Adjust spotlight position dynamically based on screen size or orientation.

local spotlight = SpotlightUI.new()
local camera = workspace.CurrentCamera

camera:GetPropertyChangedSignal("ViewportSize"):Connect(function()
    local viewportSize = camera.ViewportSize
    local isPortrait = viewportSize.Y > viewportSize.X

    if isPortrait then
        spotlight._hint.Size = UDim2.fromOffset(250, 80)
    else
        spotlight._hint.Size = UDim2.fromOffset(300, 60)
    end
end)

spotlight:SetSteps({
    { UI = gui.MobileButton, Text = "Tap here on mobile" },
    { Part = workspace.Checkpoint, Text = "Navigate to this point" }
})

spotlight:Start()

Combining Multiple Spotlights

Layer multiple spotlights for complex scenarios (use carefully to avoid confusion).

local primarySpotlight = SpotlightUI.new()
local secondarySpotlight = SpotlightUI.new()

primarySpotlight
    :FocusUI(gui.ObjectiveButton, 20, "Main objective: Click here")
    :SetShape("Circle")
    :EnablePulse(12)
    :Show()

secondarySpotlight
    :FocusUI(gui.HintButton, 10, "Optional: Click for a hint")
    :SetShape("Square")
    :Show()

primarySpotlight.sequenceCompleted:Connect(function()
    secondarySpotlight:Destroy()
end)

Warning

Multiple simultaneous spotlights can overwhelm players. Use this pattern sparingly and ensure the primary spotlight is visually distinct.


Performance Optimization

Tips for maintaining performance with complex tutorials:

Reuse Spotlight Instances

local spotlight = SpotlightUI.new()
for i = 1, 10 do
    spotlight:FocusUI(buttons[i], 10, "Click " .. i):Show()
    task.wait(2)
end
spotlight:Destroy()

Limit Simultaneous Tracking

local spotlight = SpotlightUI.new()
for _, npc in workspace.NPCs:GetChildren() do
    spotlight:FollowPart(npc, "Talk to " .. npc.Name):Show()
    task.wait(5)
end

Destroy When Done

local spotlight = SpotlightUI.new()

spotlight.sequenceCompleted:Connect(function()
    task.wait(1)
    spotlight:Destroy()
end)

spotlight:SetSteps({...}):Start()

Error Handling

Handle edge cases gracefully in production code:

local function SafeStartTutorial(gui, steps)
    local success, err = pcall(function()
        local spotlight = SpotlightUI.new()

        for i, step in steps do
            if step.UI and not step.UI.Parent then
                warn("Step", i, "references a deleted UI element")
                return
            end
            if step.Part and not step.Part.Parent then
                warn("Step", i, "references a deleted Part")
                return
            end
        end

        spotlight:SetSteps(steps)
        spotlight:Start()
    end)

    if not success then
        warn("Tutorial failed to start:", err)
    end
end

SafeStartTutorial(gui, {
    { UI = gui.Button1, Text = "Click here" },
    { Part = workspace.Door, Text = "Go to door" }
})

Integration with Game Systems

Quest System Integration

local QuestSystem = {}

function QuestSystem:StartQuest(questId)
    local questData = self:GetQuestData(questId)

    if questData.hasSpotlight and not player:GetAttribute("Quest_" .. questId) then
        local spotlight = SpotlightUI.new()

        spotlight:SetSteps({
            { Part = questData.npc, Text = "Talk to " .. questData.npcName },
            { World = questData.objective, Radius = 15, Text = questData.objectiveText },
            { Part = questData.npc, Text = "Return to " .. questData.npcName }
        })

        spotlight.sequenceCompleted:Connect(function()
            player:SetAttribute("Quest_" .. questId, true)
        end)

        spotlight:Start()
    end
end

Achievement System

local AchievementSpotlight = {}

function AchievementSpotlight:ShowNewAchievement(achievementName)
    local spotlight = SpotlightUI.new()

    spotlight
        :FocusUI(gui.AchievementPopup, 25, "You unlocked: " .. achievementName)
        :SetShape("Square")
        :EnablePulse(15)
        :Show()

    task.delay(4, function()
        spotlight:Hide()
        task.wait(0.5)
        spotlight:Destroy()
    end)
end

Best Practices Summary

  1. Destroy when finished - Always call :Destroy() when done with a spotlight
  2. Validate references - Check that UI elements and parts exist before creating steps
  3. Limit concurrent spotlights - Avoid showing multiple spotlights simultaneously
  4. Provide skip options - Let players who know the game skip tutorials
  5. Test on all platforms - Ensure spotlights work on mobile, tablet, and desktop
  6. Save progress - For long tutorials, save progress to DataStores
  7. Use appropriate shapes - Circles for focus points, squares for UI elements
  8. Keep text concise - Hint text should be brief and actionable