Roblox · Приключение · урок 7

Общий: вся игра целиком

Итог курса на одной странице: полная структура приключения и финальные версии всех скриптов. Удобно сверить свою игру или собрать её заново.

Структура

Синим — что добавляем на этом уроке.

Workspace — StreamingEnabled выкл.
├─ Elder (Model) — Head → Dialog, ProximityPrompt; QuestGiver (Мешки с песком)
├─ Hunter (Model) — Head → ProximityPrompt; QuestGiver (Уничтожение)
├─ Trader (Model) — Head → ProximityPrompt; TraderScript
├─ Sandbags (Folder) — Sandbag → ProximityPrompt, QuestItem
├─ Enemies (Folder) — Goblin → EnemyScript
├─ Lights (Folder) — фонари
└─ Hints (Folder) — Hint → Text
ReplicatedStorage
└─ Events (Folder) — CompleteEvent, CancelEvent, ShopEvent, BuyFunction
ServerStorage
├─ Drop (Part) — DropScript
└─ Shop (Folder) — HealingPotion, InvisiblePotion (Price)
ServerScriptService
└─ QuestSystem, EnemyManager, BuyScript, DayNight (Script)
StarterPack
└─ Sword (Tool) — Handle, SwordScript
StarterGui — все ResetOnSpawn выкл.
├─ QuestGui (ScreenGui) — QuestFrame, DoneLabel, QuestLocal
├─ ShopGui (ScreenGui) — ShopFrame → Items, Message, CloseButton; ShopLocal
└─ HintGui (ScreenGui) — HintLabel, HintLocal

Шаги

  1. Сверяй сверху вниз: сначала Explorer со «Структурой», потом каждый скрипт целиком.
  2. Имена связывают всё: Title квеста = QUEST в QuestItem = KILL_QUEST в EnemyManager; имя кнопки магазина = имя Tool в Shop.
  3. Жители — HumanoidRootPart Anchored вкл., гоблины — выкл. (иначе не ходят).
  4. Все Handle у инструментов — Anchored выкл.
  5. Все ScreenGui — ResetOnSpawn выкл.; Workspace → StreamingEnabled выкл.

Скрипты

QuestSystem

Где: ServerScriptService · Script
Зачем: Монеты и квесты.

-- игроки
local Players = game:GetService("Players")
-- общее хранилище
local RS = game:GetService("ReplicatedStorage")
-- папка событий
local events = RS:WaitForChild("Events")
-- квест выполнен — сообщить игроку
local completeEvent = events:WaitForChild("CompleteEvent")
-- игрок отменил квест
local cancelEvent = events:WaitForChild("CancelEvent")

-- создаёт контейнер со значением
local function Value(parent, class, name, value)
    -- контейнер
    local v = Instance.new(class)
    -- имя
    v.Name = name
    -- значение
    v.Value = value
    -- на место
    v.Parent = parent
    -- отдаём
    return v
-- конец функции
end

-- у игрока нет квеста
local function ClearQuest(quest)
    -- сначала цель: теперь сброс прогресса ничего не засчитает
    quest.Objective.Value = 0
    -- прогресс
    quest.Progress.Value = 0
    -- название пустое = квеста нет
    quest.Title.Value = ""
    -- описание
    quest.Description.Value = ""
    -- награда
    quest.Reward.Value = 0
-- конец функции
end

-- игрок зашёл
Players.PlayerAdded:Connect(function(player)
    -- таблица по Tab
    local leaderstats = Instance.new("Folder")
    -- имя строго такое
    leaderstats.Name = "leaderstats"
    -- в игрока
    leaderstats.Parent = player
    -- монеты
    local coins = Value(leaderstats, "IntValue", "Coins", 0)
    -- текущий квест игрока
    local quest = Instance.new("Folder")
    -- имя
    quest.Name = "QuestInfo"
    -- в игрока — клиент тоже видит
    quest.Parent = player
    -- название
    Value(quest, "StringValue", "Title", "")
    -- что сделать
    Value(quest, "StringValue", "Description", "")
    -- сколько нужно
    Value(quest, "IntValue", "Objective", 0)
    -- сколько уже
    local progress = Value(quest, "IntValue", "Progress", 0)
    -- награда в монетах
    Value(quest, "IntValue", "Reward", 0)
    -- прогресс изменился
    progress.Changed:Connect(function(value)
        -- цель
        local objective = quest.Objective.Value
        -- квеста нет или ещё не всё
        if objective <= 0 or value < objective then return end
        -- запоминаем награду
        local reward = quest.Reward.Value
        -- и название
        local title = quest.Title.Value
        -- квест закрыт — второй раз награда не выдастся
        ClearQuest(quest)
        -- награда
        coins.Value += reward
        -- сообщение на экране
        completeEvent:FireClient(player, title, reward)
    -- конец функции
    end)
-- конец функции
end)

-- кнопка «Отменить»
cancelEvent.OnServerEvent:Connect(function(player)
    -- квест игрока
    local quest = player:FindFirstChild("QuestInfo")
    -- очищаем
    if quest then ClearQuest(quest) end
-- конец функции
end)

QuestGiver

Где: Workspace → Elder / Hunter · Script
Зачем: Выдаёт квест.

-- персонаж, который даёт квест
local npc = script.Parent
-- «Взять задание»
local prompt = npc:WaitForChild("Head"):WaitForChild("ProximityPrompt")

-- игрок нажал E
prompt.Triggered:Connect(function(player)
    -- квест игрока
    local quest = player:FindFirstChild("QuestInfo")
    -- уже есть другой квест
    if not quest or quest.Title.Value ~= "" then return end
    -- название — из значений под скриптом
    quest.Title.Value = script.Title.Value
    -- описание
    quest.Description.Value = script.Description.Value
    -- награда
    quest.Reward.Value = script.Reward.Value
    -- с нуля
    quest.Progress.Value = 0
    -- цель — последней: квест начался
    quest.Objective.Value = script.Objective.Value
-- конец функции
end)

QuestItem

Где: Workspace → Sandbags → Sandbag · Script
Зачем: Мешок для квеста.

-- мешок с песком
local item = script.Parent
-- «Взять»
local prompt = item:WaitForChild("ProximityPrompt")
-- для какого квеста — как Title у квестодателя
local QUEST = "Мешки с песком"
-- через сколько секунд мешок вернётся
local RESPAWN = 60

-- игрок нажал E
prompt.Triggered:Connect(function(player)
    -- квест игрока
    local quest = player:FindFirstChild("QuestInfo")
    -- мешки нужны только с этим квестом
    if not quest or quest.Title.Value ~= QUEST then return end
    -- +1 к прогрессу
    quest.Progress.Value += 1
    -- второй раз не взять
    prompt.Enabled = false
    -- мешок пропал
    item.Transparency = 1
    -- и не мешает
    item.CanCollide = false
    -- ждём
    task.wait(RESPAWN)
    -- вернулся
    item.Transparency = 0
    -- снова твёрдый
    item.CanCollide = true
    -- можно взять
    prompt.Enabled = true
-- конец функции
end)

QuestLocal

Где: StarterGui → QuestGui · LocalScript
Зачем: Окно квеста.

-- игроки
local Players = game:GetService("Players")
-- общее хранилище
local RS = game:GetService("ReplicatedStorage")
-- этот игрок
local player = Players.LocalPlayer
-- его квест
local quest = player:WaitForChild("QuestInfo")
-- события
local events = RS:WaitForChild("Events")
-- квест выполнен
local completeEvent = events:WaitForChild("CompleteEvent")
-- отмена
local cancelEvent = events:WaitForChild("CancelEvent")
-- QuestGui
local gui = script.Parent
-- окно квеста
local frame = gui:WaitForChild("QuestFrame")
-- надпись «Выполнено»
local doneLabel = gui:WaitForChild("DoneLabel")

-- обновить окно
local function Update()
    -- квест есть — окно видно
    frame.Visible = quest.Title.Value ~= ""
    -- название
    frame.Title.Text = quest.Title.Value
    -- описание
    frame.Description.Text = quest.Description.Value
    -- прогресс: 2 / 5
    frame.Progress.Text = quest.Progress.Value .. " / " .. quest.Objective.Value
-- конец функции
end

-- все значения квеста
for _, value in ipairs(quest:GetChildren()) do
    -- изменилось — обновляем
    value.Changed:Connect(Update)
-- конец перебора
end
-- сразу при входе
Update()

-- «Отменить»
frame.CancelButton.MouseButton1Click:Connect(function()
    -- просим сервер
    cancelEvent:FireServer()
-- конец функции
end)
-- квест выполнен
completeEvent.OnClientEvent:Connect(function(title, reward)
    -- текст
    doneLabel.Text = "Квест «" .. title .. "» выполнен! +" .. reward .. " монет"
    -- показываем
    doneLabel.Visible = true
    -- 3 секунды
    task.wait(3)
    -- прячем
    doneLabel.Visible = false
-- конец функции
end)

EnemyScript

Где: Workspace → Enemies → Goblin · Script
Зачем: Противник.

-- игроки
local Players = game:GetService("Players")
-- модель противника
local enemy = script.Parent
-- Humanoid
local humanoid = enemy:WaitForChild("Humanoid")
-- центр
local root = enemy:WaitForChild("HumanoidRootPart")
-- Animator
local animator = humanoid:FindFirstChildOfClass("Animator") or Instance.new("Animator", humanoid)
-- видит игроков ближе 40 studs
local SIGHT = 40
-- бьёт ближе 5 studs
local ATTACK_RANGE = 5
-- урон
local DAMAGE = 10
-- секунд между ударами
local COOLDOWN = 1

-- ходьба
local walkAnim = Instance.new("Animation")
-- стандартная R15
walkAnim.AnimationId = "rbxassetid://507777826"
-- удар
local attackAnim = Instance.new("Animation")
-- стандартный взмах R15
attackAnim.AnimationId = "rbxassetid://522635514"
-- загружаем
local walkTrack = animator:LoadAnimation(walkAnim)
-- по кругу
walkTrack.Looped = true
-- загружаем
local attackTrack = animator:LoadAnimation(attackAnim)
-- поверх ходьбы
attackTrack.Priority = Enum.AnimationPriority.Action
-- куда возвращаться
local home = root.Position
-- время прошлого удара
local lastAttack = 0
-- физику считает сервер
pcall(function() root:SetNetworkOwner(nil) end)

-- скорость изменилась
humanoid.Running:Connect(function(speed)
    -- пошёл
    if speed > 0.5 and not walkTrack.IsPlaying then
        -- шагаем
        walkTrack:Play()
    -- встал
    elseif speed <= 0.5 and walkTrack.IsPlaying then
        -- стоим
        walkTrack:Stop()
    -- конец проверки
    end
-- конец функции
end)

-- ближайший живой видимый игрок
local function FindTarget()
    -- пока никого
    local best, bestRoot, bestDistance = nil, nil, SIGHT
    -- все игроки
    for _, player in ipairs(Players:GetPlayers()) do
        -- персонаж
        local character = player.Character
        -- его Humanoid
        local target = character and character:FindFirstChildOfClass("Humanoid")
        -- его центр
        local targetRoot = character and character:FindFirstChild("HumanoidRootPart")
        -- жив и не невидим
        if target and targetRoot and target.Health > 0 and not character:GetAttribute("Invisible") then
            -- расстояние
            local distance = (targetRoot.Position - root.Position).Magnitude
            -- ближе прошлого
            if distance < bestDistance then
                -- запоминаем
                best, bestRoot, bestDistance = target, targetRoot, distance
            -- конец проверки
            end
        -- конец проверки
        end
    -- конец перебора
    end
    -- цель, её центр, расстояние
    return best, bestRoot, bestDistance
-- конец функции
end

-- пока жив
while humanoid.Health > 0 do
    -- ищем цель
    local target, targetRoot, distance = FindTarget()
    -- нашли
    if target then
        -- идём к ней
        humanoid:MoveTo(targetRoot.Position)
        -- рядом и удар готов
        if distance <= ATTACK_RANGE and os.clock() - lastAttack >= COOLDOWN then
            -- запоминаем время
            lastAttack = os.clock()
            -- взмах
            attackTrack:Play()
            -- урон (ForceField защищает)
            target:TakeDamage(DAMAGE)
        -- конец проверки
        end
    -- никого рядом
    else
        -- возвращаемся на место
        humanoid:MoveTo(home)
    -- конец проверки
    end
    -- 5 раз в секунду
    task.wait(0.2)
-- конец цикла
end

EnemyManager

Где: ServerScriptService · Script
Зачем: Возрождение и награды.

-- здесь лежит награда Drop
local SS = game:GetService("ServerStorage")
-- все противники
local enemies = workspace:WaitForChild("Enemies")
-- через сколько секунд противник вернётся
local RESPAWN_TIME = 10
-- монет за победу
local KILL_REWARD = 3
-- квест, которому засчитываются победы
local KILL_QUEST = "Уничтожение"

-- следим за одним противником
local function Setup(enemy)
    -- копия целого — для возрождения
    local sample = enemy:Clone()
    -- где стоял
    local spot = enemy:GetPivot()
    -- Humanoid
    local humanoid = enemy:WaitForChild("Humanoid")
    -- противник погиб
    humanoid.Died:Connect(function()
        -- метка от меча
        local tag = humanoid:FindFirstChild("creator")
        -- кто победил
        local player = tag and tag.Value
        -- игрок ещё в игре
        if player and player.Parent then
            -- таблица
            local stats = player:FindFirstChild("leaderstats")
            -- монеты
            if stats then stats.Coins.Value += KILL_REWARD end
            -- квест
            local quest = player:FindFirstChild("QuestInfo")
            -- взят квест на противников
            if quest and quest.Title.Value == KILL_QUEST then
                -- +1
                quest.Progress.Value += 1
            -- конец проверки
            end
        -- конец проверки
        end
        -- мешочек с монетами
        local drop = SS:FindFirstChild("Drop")
        -- есть
        if drop then
            -- копия
            local copy = drop:Clone()
            -- там, где упал противник
            copy:PivotTo(CFrame.new(enemy:GetPivot().Position))
            -- в мир
            copy.Parent = workspace
        -- конец проверки
        end
        -- ждём
        task.wait(RESPAWN_TIME)
        -- убираем тело
        enemy:Destroy()
        -- новый противник
        local new = sample:Clone()
        -- на старое место
        new:PivotTo(spot)
        -- в игру — его EnemyScript запустится сам
        new.Parent = enemies
        -- следим и за ним
        Setup(new)
    -- конец функции
    end)
-- конец функции
end

-- все противники
for _, enemy in ipairs(enemies:GetChildren()) do
    -- следим
    Setup(enemy)
-- конец перебора
end

DropScript

Где: ServerStorage → Drop · Script
Зачем: Мешочек с монетами.

-- игроки
local Players = game:GetService("Players")
-- мешочек
local drop = script.Parent
-- сколько монет внутри
local COINS = 5
-- через сколько секунд исчезнет
local LIFETIME = 30
-- уже подобран
local taken = false

-- касание
drop.Touched:Connect(function(hit)
    -- уже взяли
    if taken then return end
    -- игрок?
    local player = Players:GetPlayerFromCharacter(hit.Parent)
    -- его таблица
    local stats = player and player:FindFirstChild("leaderstats")
    -- не игрок
    if not stats then return end
    -- больше не берётся
    taken = true
    -- монеты
    stats.Coins.Value += COINS
    -- мешочек пропал
    drop:Destroy()
-- конец функции
end)

-- ждём
task.wait(LIFETIME)
-- никто не взял — убираем
if drop.Parent then drop:Destroy() end

SwordScript

Где: StarterPack → Sword · Script
Зачем: Меч.

-- игроки
local Players = game:GetService("Players")
-- удаляет объекты через время
local Debris = game:GetService("Debris")
-- меч
local tool = script.Parent
-- клинок
local handle = tool:WaitForChild("Handle")
-- урон за удар
local DAMAGE = 25
-- длина взмаха
local COOLDOWN = 0.6

-- анимация взмаха
local slash = Instance.new("Animation")
-- стандартная R15
slash.AnimationId = "rbxassetid://522635514"
-- идёт взмах
local swinging = false
-- кого уже ударили этим взмахом
local hitList = {}

-- клик мышью
tool.Activated:Connect(function()
    -- взмах ещё идёт
    if swinging then return end
    -- владелец
    local humanoid = tool.Parent:FindFirstChildOfClass("Humanoid")
    -- погибший не бьёт
    if not humanoid or humanoid.Health <= 0 then return end
    -- начали
    swinging = true
    -- новый взмах — список пуст
    table.clear(hitList)
    -- анимации владельца
    local animator = humanoid:FindFirstChildOfClass("Animator")
    -- взмах
    if animator then animator:LoadAnimation(slash):Play() end
    -- ждём
    task.wait(COOLDOWN)
    -- можно снова
    swinging = false
-- конец функции
end)

-- клинок коснулся
handle.Touched:Connect(function(hit)
    -- бьём только во время взмаха
    if not swinging then return end
    -- модель, которой принадлежит деталь
    local model = hit:FindFirstAncestorOfClass("Model")
    -- её Humanoid
    local target = model and model:FindFirstChildOfClass("Humanoid")
    -- не цель, сам себя или уже ударили
    if not target or target.Health <= 0 or model == tool.Parent or hitList[target] then return end
    -- игроков не бьём — только противников
    if Players:GetPlayerFromCharacter(model) then return end
    -- этим взмахом больше не бьём
    hitList[target] = true
    -- прошлая метка
    local old = target:FindFirstChild("creator")
    -- убираем
    if old then old:Destroy() end
    -- метка «кто ударил»
    local tag = Instance.new("ObjectValue")
    -- стандартное имя метки в Roblox
    tag.Name = "creator"
    -- игрок с мечом
    tag.Value = Players:GetPlayerFromCharacter(tool.Parent)
    -- в Humanoid противника
    tag.Parent = target
    -- через 2 секунды метка исчезнет
    Debris:AddItem(tag, 2)
    -- урон
    target:TakeDamage(DAMAGE)
-- конец функции
end)

PotionScript

Где: ServerStorage → Shop → HealingPotion · Script
Зачем: Лечение.

-- удаляет объекты через время
local Debris = game:GetService("Debris")
-- зелье
local tool = script.Parent
-- сколько лечит
local HEAL = 50

-- клик — выпить
tool.Activated:Connect(function()
    -- кто пьёт
    local humanoid = tool.Parent:FindFirstChildOfClass("Humanoid")
    -- погибшим не помочь
    if not humanoid or humanoid.Health <= 0 then return end
    -- +50, но не больше максимума
    humanoid.Health = math.min(humanoid.Health + HEAL, humanoid.MaxHealth)
    -- звук глотка
    local sound = tool.Handle:FindFirstChild("Drink")
    -- звук есть
    if sound and humanoid.RootPart then
        -- переносим в игрока — иначе пропадёт вместе с зельем
        sound.Parent = humanoid.RootPart
        -- буль
        sound:Play()
        -- потом удалим
        Debris:AddItem(sound, 3)
    -- конец проверки
    end
    -- зелье выпито
    tool:Destroy()
-- конец функции
end)

InvisibleScript

Где: ServerStorage → Shop → InvisiblePotion · Script
Зачем: Невидимость.

-- здесь скрипт доработает после того, как зелье пропадёт из рук
local SSS = game:GetService("ServerScriptService")
-- зелье
local tool = script.Parent
-- секунд невидимости
local DURATION = 5
-- уже выпито
local used = false

-- плавно меняет прозрачность
local function Fade(saved, toInvisible)
    -- 10 шагов
    for i = 1, 10 do
        -- доля невидимости
        local k = toInvisible and i / 10 or 1 - i / 10
        -- каждая деталь
        for obj, base in pairs(saved) do
            -- от своей прозрачности к 1
            if obj.Parent then obj.Transparency = base + (1 - base) * k end
        -- конец перебора
        end
        -- полсекунды на всё
        task.wait(0.05)
    -- конец цикла
    end
-- конец функции
end

-- клик — выпить
tool.Activated:Connect(function()
    -- уже
    if used then return end
    -- кто пьёт
    local character = tool.Parent
    -- Humanoid
    local humanoid = character:FindFirstChildOfClass("Humanoid")
    -- погибший не пьёт
    if not humanoid or humanoid.Health <= 0 then return end
    -- выпито
    used = true
    -- убираем из рук, но скрипт продолжает работать
    tool.Parent = SSS
    -- деталь → её прозрачность
    local saved = {}
    -- всё в персонаже
    for _, obj in ipairs(character:GetDescendants()) do
        -- видимые детали и лицо
        if (obj:IsA("BasePart") or obj:IsA("Decal")) and obj.Name ~= "HumanoidRootPart" then
            -- запоминаем
            saved[obj] = obj.Transparency
        -- конец проверки
        end
    -- конец перебора
    end
    -- противники перестают видеть
    character:SetAttribute("Invisible", true)
    -- исчезаем
    Fade(saved, true)
    -- ждём
    task.wait(DURATION)
    -- появляемся
    Fade(saved, false)
    -- снова виден
    character:SetAttribute("Invisible", nil)
    -- теперь зелье можно удалить
    tool:Destroy()
-- конец функции
end)

TraderScript

Где: Workspace → Trader · Script
Зачем: Открывает магазин.

-- общее хранилище
local RS = game:GetService("ReplicatedStorage")
-- открыть магазин
local shopEvent = RS:WaitForChild("Events"):WaitForChild("ShopEvent")
-- «Торговать»
local prompt = script.Parent:WaitForChild("Head"):WaitForChild("ProximityPrompt")

-- игрок нажал E
prompt.Triggered:Connect(function(player)
    -- только у него откроется окно
    shopEvent:FireClient(player)
-- конец функции
end)

ShopLocal

Где: StarterGui → ShopGui · LocalScript
Зачем: Окно магазина.

-- общее хранилище
local RS = game:GetService("ReplicatedStorage")
-- события
local events = RS:WaitForChild("Events")
-- открыть
local shopEvent = events:WaitForChild("ShopEvent")
-- купить и получить ответ
local buyFunction = events:WaitForChild("BuyFunction")
-- окно
local frame = script.Parent:WaitForChild("ShopFrame")
-- ответ магазина
local message = frame:WaitForChild("Message")

-- торговец позвал
shopEvent.OnClientEvent:Connect(function()
    -- без старых сообщений
    message.Text = ""
    -- открываем
    frame.Visible = true
-- конец функции
end)
-- «Закрыть» — подписываемся один раз
frame:WaitForChild("CloseButton").MouseButton1Click:Connect(function()
    -- закрываем
    frame.Visible = false
-- конец функции
end)
-- кнопки товаров
for _, button in ipairs(frame:WaitForChild("Items"):GetChildren()) do
    -- только кнопки
    if button:IsA("TextButton") then
        -- нажатие
        button.MouseButton1Click:Connect(function()
            -- имя кнопки = имя товара; ждём ответ
            local _, text = buyFunction:InvokeServer(button.Name)
            -- «Куплено» или «Не хватает монет»
            message.Text = text
        -- конец функции
        end)
    -- конец проверки
    end
-- конец перебора
end

BuyScript

Где: ServerScriptService · Script
Зачем: Покупка.

-- общее хранилище
local RS = game:GetService("ReplicatedStorage")
-- товары лежат здесь — клиент их не видит и не подменит
local SS = game:GetService("ServerStorage")
-- покупка
local buyFunction = RS:WaitForChild("Events"):WaitForChild("BuyFunction")
-- товары
local shop = SS:WaitForChild("Shop")

-- клиент хочет купить
buyFunction.OnServerInvoke = function(player, itemName)
    -- пришло не имя
    if typeof(itemName) ~= "string" then return false, "Ошибка" end
    -- товар
    local item = shop:FindFirstChild(itemName)
    -- его цена
    local price = item and item:FindFirstChild("Price")
    -- монеты игрока
    local stats = player:FindFirstChild("leaderstats")
    -- рюкзак
    local backpack = player:FindFirstChild("Backpack")
    -- что-то не так
    if not price or not stats or not backpack then return false, "Такого товара нет" end
    -- денег мало
    if stats.Coins.Value < price.Value then
        -- ответ
        return false, "Не хватает монет: нужно " .. price.Value
    -- конец проверки
    end
    -- списываем
    stats.Coins.Value -= price.Value
    -- товар в рюкзак
    item:Clone().Parent = backpack
    -- ответ
    return true, "Куплено!"
-- конец функции
end

DayNight

Где: ServerScriptService · Script
Зачем: День и ночь.

-- освещение
local Lighting = game:GetService("Lighting")
-- фонари
local lights = workspace:WaitForChild("Lights")
-- часов игрового времени за секунду: сутки = 8 минут
local STEP = 0.05

-- бесконечно
while true do
    -- время идёт; после 24 — снова 0
    Lighting.ClockTime = (Lighting.ClockTime + STEP) % 24
    -- с 18 до 6 — ночь
    local night = Lighting.ClockTime >= 18 or Lighting.ClockTime < 6
    -- всё в фонарях
    for _, light in ipairs(lights:GetDescendants()) do
        -- ночью свет горит
        if light:IsA("Light") then light.Enabled = night end
    -- конец перебора
    end
    -- секунда
    task.wait(1)
-- конец цикла
end

HintLocal

Где: StarterGui → HintGui · LocalScript
Зачем: Подсказки.

-- игроки
local Players = game:GetService("Players")
-- этот игрок
local player = Players.LocalPlayer
-- зоны подсказок
local hints = workspace:WaitForChild("Hints")
-- надпись
local label = script.Parent:WaitForChild("HintLabel")
-- секунд на экране
local SHOW_TIME = 4
-- какие подсказки уже были
local shown = {}

-- следим за одной зоной
local function Watch(trigger)
    -- касание
    trigger.Touched:Connect(function(hit)
        -- уже было или это не мы
        if shown[trigger] or hit.Parent ~= player.Character then return end
        -- StringValue с текстом
        local text = trigger:FindFirstChild("Text")
        -- текста нет
        if not text then return end
        -- больше не показываем
        shown[trigger] = true
        -- текст
        label.Text = text.Value
        -- показываем
        label.Visible = true
        -- через 4 секунды
        task.delay(SHOW_TIME, function()
            -- прячем, если не появилась другая
            if label.Text == text.Value then label.Visible = false end
        -- конец функции
        end)
    -- конец функции
    end)
-- конец функции
end

-- все зоны
for _, trigger in ipairs(hints:GetChildren()) do Watch(trigger) end
-- и новые
hints.ChildAdded:Connect(Watch)

Проверка

Запусти Play (или Run) и убедись:

Собрать всю игру одной командой