Roblox · Сбор-а-тон · урок 7

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

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

Структура

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

Workspace — StreamingEnabled выкл.
├─ Map (Model) — зоны
├─ Items (Folder)
│  └─ предмет (Part) — Decal, Difficulty, Description, BadgeID
└─ Teleports (Folder) — площадки с Decal
ReplicatedStorage
└─ Events (Folder) — CollectEvent, GameOverEvent
ServerScriptService
└─ CollectServer (Script)
StarterGui — все ResetOnSpawn выкл.
├─ CollectGui (ScreenGui) — Message, Found, CollectLocal
├─ ListGui (ScreenGui) — ListButton, ListFrame (Progress, SearchBox, ScrollingList → Sample), DescriptionFrame, TeleportButton, TeleportFrame, ListLocal, TeleportLocal
└─ GameOverGui (ScreenGui) — GameOverFrame, GameOverLocal

Шаги

  1. Сверяй сверху вниз: сначала Explorer со «Структурой», потом каждый скрипт целиком.
  2. Имена связывают всё: ListGui → ListFrame → ScrollingList → Sample → ItemImage, ItemName, Difficulty; TeleportFrame → ScrollingList → Sample → LocationImage, LocationName.
  3. Предметы — прямые дети папки Items, у каждого уникальное имя: по имени игра помнит находку.
  4. Все ScreenGui — ResetOnSpawn выкл.; Workspace → StreamingEnabled выкл.
  5. Картинки (Decal) и звук находки пустые — вставь свои.

Скрипты

CollectServer

Где: ServerScriptService · Script
Зачем: Находки, сохранение, конец игры.

-- игроки
local Players = game:GetService("Players")
-- общее хранилище
local RS = game:GetService("ReplicatedStorage")
-- сохранение
local DSS = game:GetService("DataStoreService")
-- нашивки (по желанию)
local BadgeService = game:GetService("BadgeService")
-- все предметы
local items = workspace:WaitForChild("Items")
-- события
local events = RS:WaitForChild("Events")
-- «нашёл предмет»
local collectEvent = events:WaitForChild("CollectEvent")
-- «нашёл всё»
local gameOverEvent = events:WaitForChild("GameOverEvent")
-- хранилище находок
local store = DSS:GetDataStore("CollectData")
-- у кого находки загрузились
local loaded = {}

-- сохранить находки
local function Save(player)
    -- не загрузились — не затираем
    if not loaded[player] then return end
    -- имена найденных
    local list = {}
    -- каждая находка
    for _, mark in ipairs(player.Found:GetChildren()) do
        -- в список
        table.insert(list, mark.Name)
    -- конец перебора
    end
    -- сохраняем
    pcall(function() store:SetAsync("found_" .. player.UserId, list) end)
-- конец функции
end

-- игрок нашёл предмет
local function Collect(player, item)
    -- его находки
    local found = player:FindFirstChild("Found")
    -- уже нашёл
    if not found or found:FindFirstChild(item.Name) then return end
    -- отметка
    local mark = Instance.new("BoolValue")
    -- имя = предмет
    mark.Name = item.Name
    -- в папку игрока
    mark.Parent = found
    -- сообщение на экране
    collectEvent:FireClient(player, item.Name)
    -- ID нашивки
    local badge = item:FindFirstChild("BadgeID")
    -- нашивка настроена
    if badge and badge.Value > 0 then
        -- не задерживаем остальное
        task.spawn(function()
            -- выдаём; в Studio может не сработать
            pcall(function() BadgeService:AwardBadge(player.UserId, badge.Value) end)
        -- конец функции
        end)
    -- конец проверки
    end
    -- найдено всё
    if #found:GetChildren() >= #items:GetChildren() then
        -- через секунду — экран победы
        task.delay(1, function() gameOverEvent:FireClient(player) end)
    -- конец проверки
    end
-- конец функции
end

-- игрок зашёл
local function Setup(player)
    -- его находки
    local found = Instance.new("Folder")
    -- имя
    found.Name = "Found"
    -- загружаем
    local ok, list = pcall(function() return store:GetAsync("found_" .. player.UserId) end)
    -- загрузка удалась
    if ok then
        -- каждое сохранённое имя
        for _, name in ipairs(list or {}) do
            -- такой предмет ещё есть
            if items:FindFirstChild(name) then
                -- отметка
                local mark = Instance.new("BoolValue")
                -- имя
                mark.Name = name
                -- в папку
                mark.Parent = found
            -- конец проверки
            end
        -- конец перебора
        end
        -- можно сохранять
        loaded[player] = true
    -- конец проверки
    end
    -- в игрока — клиент видит
    found.Parent = player
-- конец функции
end

-- новые игроки
Players.PlayerAdded:Connect(Setup)
-- кто зашёл раньше скрипта
for _, player in ipairs(Players:GetPlayers()) do task.spawn(Setup, player) end
-- игрок вышел
Players.PlayerRemoving:Connect(function(player)
    -- сохраняем
    Save(player)
    -- забываем
    loaded[player] = nil
-- конец функции
end)
-- сервер закрывается
game:BindToClose(function()
    -- сохраняем всех
    for _, player in ipairs(Players:GetPlayers()) do Save(player) end
-- конец функции
end)

-- каждый предмет — один раз
for _, item in ipairs(items:GetChildren()) do
    -- клик
    local clicker = item:FindFirstChildOfClass("ClickDetector") or Instance.new("ClickDetector")
    -- кликнуть можно только рядом
    clicker.MaxActivationDistance = 20
    -- в предмет
    clicker.Parent = item
    -- кто-то кликнул
    clicker.MouseClick:Connect(function(player)
        -- засчитываем
        Collect(player, item)
    -- конец функции
    end)
-- конец перебора
end

CollectLocal

Где: StarterGui → CollectGui · LocalScript
Зачем: Сообщение о находке.

-- общее хранилище
local RS = game:GetService("ReplicatedStorage")
-- «нашёл предмет»
local collectEvent = RS:WaitForChild("Events"):WaitForChild("CollectEvent")
-- CollectGui
local gui = script.Parent
-- надпись
local message = gui:WaitForChild("Message")
-- звук находки
local sound = gui:WaitForChild("Found")
-- номер последнего сообщения
local shown = 0

-- сервер засчитал находку
collectEvent.OnClientEvent:Connect(function(name)
    -- новое сообщение
    shown += 1
    -- его номер
    local my = shown
    -- текст
    message.Text = "Найдено: " .. name .. "!"
    -- показываем
    message.Visible = true
    -- звук
    sound:Play()
    -- ждём
    task.wait(2.5)
    -- прячем, если не пришло новое
    if shown == my then message.Visible = false end
-- конец функции
end)

ListLocal

Где: StarterGui → ListGui · LocalScript
Зачем: Журнал, описание, поиск.

-- игроки
local Players = game:GetService("Players")
-- этот игрок
local player = Players.LocalPlayer
-- все предметы
local items = workspace:WaitForChild("Items")
-- что игрок уже нашёл
local found = player:WaitForChild("Found")
-- ListGui
local gui = script.Parent
-- кнопка журнала
local listButton = gui:WaitForChild("ListButton")
-- журнал
local listFrame = gui:WaitForChild("ListFrame")
-- список с прокруткой
local scrolling = listFrame:WaitForChild("ScrollingList")
-- шаблон карточки
local sample = scrolling:WaitForChild("Sample")
-- «Найдено: 2 / 6»
local progress = listFrame:WaitForChild("Progress")
-- поиск
local searchBox = listFrame:WaitForChild("SearchBox")
-- окно описания
local description = gui:WaitForChild("DescriptionFrame")
-- цвет сложности
local COLORS = {Easy = Color3.fromRGB(80, 220, 100), Medium = Color3.fromRGB(255, 150, 40), Hard = Color3.fromRGB(240, 60, 60)}
-- лёгкие — первыми
local ORDER = {Easy = 1, Medium = 2, Hard = 3}
-- найденная карточка
local FOUND_COLOR = Color3.fromRGB(150, 230, 120)
-- ещё не найденная
local HIDDEN_COLOR = Color3.fromRGB(60, 60, 75)
-- имя предмета → карточка
local cards = {}

-- перекрасить карточки и обновить счёт
local function Refresh()
    -- сколько найдено
    local count = 0
    -- каждая карточка
    for name, card in pairs(cards) do
        -- найден ли предмет
        local isFound = found:FindFirstChild(name) ~= nil
        -- цвет
        card.BackgroundColor3 = isFound and FOUND_COLOR or HIDDEN_COLOR
        -- считаем
        if isFound then count += 1 end
    -- конец перебора
    end
    -- счёт
    progress.Text = "Найдено: " .. count .. " / " .. #items:GetChildren()
-- конец функции
end

-- строчные буквы, включая русские
local function Lower(text)
    -- буквы результата
    local out = {}
    -- каждая буква по номеру
    for _, code in utf8.codes(text) do
        -- А…Я
        if code >= 1040 and code <= 1071 then
            -- → а…я
            code += 32
        -- Ё
        elseif code == 1025 then
            -- → ё
            code = 1105
        -- конец проверки
        end
        -- в результат
        table.insert(out, utf8.char(code))
    -- конец перебора
    end
    -- латиница — обычным lower
    return string.lower(table.concat(out))
-- конец функции
end

-- окно описания предмета
local function ShowDescription(item)
    -- StringValue с описанием
    local text = item:FindFirstChild("Description")
    -- название
    description.Title.Text = item.Name
    -- предмет найден
    if found:FindFirstChild(item.Name) then
        -- описание
        description.Body.Text = text and text.Value or ""
    -- ещё нет
    else
        -- загадка
        description.Body.Text = "Найди этот предмет, чтобы прочитать о нём."
    -- конец проверки
    end
    -- показываем
    description.Visible = true
-- конец функции
end

-- карточка на каждый предмет
for _, item in ipairs(items:GetChildren()) do
    -- копия шаблона
    local card = sample:Clone()
    -- имя
    card.Name = item.Name
    -- подпись
    card.ItemName.Text = item.Name
    -- картинка предмета
    local decal = item:FindFirstChildOfClass("Decal")
    -- на кнопку
    card.ItemImage.Image = decal and decal.Texture or ""
    -- фон — цвет предмета
    if item:IsA("BasePart") then card.ItemImage.BackgroundColor3 = item.Color end
    -- сложность
    local difficulty = item:FindFirstChild("Difficulty")
    -- Easy, если не указана
    local level = difficulty and difficulty.Value or "Easy"
    -- полоска цвета сложности
    card.Difficulty.BackgroundColor3 = COLORS[level] or COLORS.Easy
    -- порядок в списке
    card.LayoutOrder = ORDER[level] or 1
    -- шаблон был скрыт
    card.Visible = true
    -- в список
    card.Parent = scrolling
    -- запоминаем
    cards[item.Name] = card
    -- клик — описание
    card.ItemImage.MouseButton1Click:Connect(function() ShowDescription(item) end)
-- конец перебора
end

-- нашёл новый — обновляем
found.ChildAdded:Connect(Refresh)
-- сразу при входе
Refresh()
-- кнопка журнала
listButton.MouseButton1Click:Connect(function()
    -- открыть / закрыть
    listFrame.Visible = not listFrame.Visible
-- конец функции
end)
-- закрыть описание
description.CloseButton.MouseButton1Click:Connect(function()
    -- прячем
    description.Visible = false
-- конец функции
end)
-- ввели текст в поиск
searchBox:GetPropertyChangedSignal("Text"):Connect(function()
    -- что ищем
    local query = Lower(searchBox.Text)
    -- каждая карточка
    for name, card in pairs(cards) do
        -- plain = true: точки и скобки — просто символы
        card.Visible = query == "" or string.find(Lower(name), query, 1, true) ~= nil
    -- конец перебора
    end
-- конец функции
end)

TeleportLocal

Где: StarterGui → ListGui · LocalScript
Зачем: Телепорты.

-- игроки
local Players = game:GetService("Players")
-- этот игрок
local player = Players.LocalPlayer
-- точки телепорта
local teleports = workspace:WaitForChild("Teleports")
-- ListGui
local gui = script.Parent
-- кнопка карты
local button = gui:WaitForChild("TeleportButton")
-- список мест
local frame = gui:WaitForChild("TeleportFrame")
-- прокрутка
local scrolling = frame:WaitForChild("ScrollingList")
-- шаблон
local sample = scrolling:WaitForChild("Sample")

-- кнопка
button.MouseButton1Click:Connect(function()
    -- открыть / закрыть
    frame.Visible = not frame.Visible
-- конец функции
end)

-- каждое место
for _, point in ipairs(teleports:GetChildren()) do
    -- копия шаблона
    local card = sample:Clone()
    -- имя
    card.Name = point.Name
    -- подпись
    card.LocationName.Text = point.Name
    -- картинка места
    local decal = point:FindFirstChildOfClass("Decal")
    -- на кнопку
    card.LocationImage.Image = decal and decal.Texture or ""
    -- фон — цвет площадки
    card.LocationImage.BackgroundColor3 = point.Color
    -- показываем
    card.Visible = true
    -- в список
    card.Parent = scrolling
    -- клик по месту
    card.LocationImage.MouseButton1Click:Connect(function()
        -- персонаж
        local character = player.Character
        -- он есть
        if character and character:FindFirstChild("HumanoidRootPart") then
            -- переносим целиком над площадкой
            character:PivotTo(point.CFrame + Vector3.new(0, 4, 0))
            -- закрываем список
            frame.Visible = false
        -- конец проверки
        end
    -- конец функции
    end)
-- конец перебора
end

GameOverLocal

Где: StarterGui → GameOverGui · LocalScript
Зачем: Экран победы.

-- плавные появления
local TS = game:GetService("TweenService")
-- игроки
local Players = game:GetService("Players")
-- общее хранилище
local RS = game:GetService("ReplicatedStorage")
-- «нашёл всё»
local gameOverEvent = RS:WaitForChild("Events"):WaitForChild("GameOverEvent")
-- этот игрок
local player = Players.LocalPlayer
-- экран победы
local frame = script.Parent:WaitForChild("GameOverFrame")
-- «Поздравляем!»
local congrats = frame:WaitForChild("Congrats")
-- сколько найдено
local foundText = frame:WaitForChild("Found")
-- выйти
local exitButton = frame:WaitForChild("ExitButton")
-- играть дальше
local continueButton = frame:WaitForChild("ContinueButton")
-- полторы секунды, равномерно
local FADE = TweenInfo.new(1.5, Enum.EasingStyle.Linear)

-- плавно показать
local function Show(element)
    -- виден (пока прозрачный)
    element.Visible = true
    -- проявляется
    TS:Create(element, FADE, {BackgroundTransparency = 0, TextTransparency = 0}):Play()
-- конец функции
end
-- спрятать сразу
local function Hide(element)
    -- не виден и не нажимается
    element.Visible = false
    -- прозрачный фон
    element.BackgroundTransparency = 1
    -- прозрачный текст
    element.TextTransparency = 1
-- конец функции
end

-- кнопки подписываем один раз
exitButton.MouseButton1Click:Connect(function()
    -- выход из игры
    player:Kick("До встречи!")
-- конец функции
end)
-- играть дальше
continueButton.MouseButton1Click:Connect(function()
    -- убираем экран
    frame.Visible = false
-- конец функции
end)

-- сервер: найдено всё
gameOverEvent.OnClientEvent:Connect(function()
    -- всё спрятано
    for _, element in ipairs({congrats, foundText, exitButton, continueButton}) do Hide(element) end
    -- текст
    foundText.Text = "Ты нашёл все предметы: " .. #player.Found:GetChildren()
    -- фон прозрачный
    frame.BackgroundTransparency = 1
    -- экран включён
    frame.Visible = true
    -- затемнение
    TS:Create(frame, FADE, {BackgroundTransparency = 0.25}):Play()
    -- ждём
    task.wait(1.5)
    -- поздравление
    Show(congrats)
    -- ждём
    task.wait(1.5)
    -- счёт
    Show(foundText)
    -- ждём
    task.wait(1.5)
    -- кнопки
    Show(exitButton)
    -- кнопки
    Show(continueButton)
-- конец функции
end)

Проверка

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

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