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

Система квестов

Житель даёт задание, на экране видно, что сделать и сколько осталось. Первый квест — собрать мешки с песком.

Структура

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

ReplicatedStorage
└─ Events (Folder)
   └─ CompleteEvent, CancelEvent (RemoteEvent)
ServerScriptService
└─ QuestSystem (Script)
Workspace
├─ Elder (Model)
│  ├─ Head (MeshPart)
│  │  └─ ProximityPrompt (ProximityPrompt) — «Взять задание»
│  └─ QuestGiver (Script)
│     ├─ Title, Description (StringValue)
│     └─ Objective, Reward (IntValue) — 5 и 20
└─ Sandbags (Folder)
   └─ Sandbag (Part)
      ├─ ProximityPrompt (ProximityPrompt)
      └─ QuestItem (Script)
StarterGui
└─ QuestGui (ScreenGui) — ResetOnSpawn выкл.
   ├─ QuestFrame (Frame) — Title, Description, Progress, CancelButton
   ├─ DoneLabel (TextLabel) — Visible выкл.
   └─ QuestLocal (LocalScript)

Шаги

  1. ReplicatedStorage → Folder «Events»: RemoteEvent «CompleteEvent» и «CancelEvent».
  2. ServerScriptService → Script «QuestSystem»: монеты и папка QuestInfo у каждого игрока. Пустой Title — квеста нет.
  3. Награда выдаётся один раз: сначала обнуляется цель, потом всё остальное — сброс прогресса ничего не засчитает.
  4. В Head старосты — ProximityPrompt (ActionText «Взять задание», RequiresLineOfSight выкл.). В модель — Script «QuestGiver», под ним StringValue Title «Мешки с песком», Description «Собери 5 мешков у реки», IntValue Objective 5 и Reward 20.
  5. Мешок: деталь «Sandbag» с ProximityPrompt (ActionText «Взять») и Script «QuestItem». QUEST в скрипте = Title квеста. Сделай 5–6 мешков (Ctrl+D) в папке «Sandbags».
  6. QuestGui: QuestFrame в углу (TextLabel Title, Description, Progress и TextButton CancelButton), отдельно DoneLabel. Внутрь — LocalScript «QuestLocal».
  7. Игрок видит QuestInfo, потому что папка лежит в нём самом — RemoteEvent для прогресса не нужен.

Скрипты

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 · 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)

Проверка

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