Roblox · Песочница · урок 5

Инвентарь

Сломанное попадает в инвентарь: дерево, камень, блоки — со счётчиками. Ставить можно только то, что есть в инвентаре.

Структура

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

ServerScriptService
├─ Inventory (ModuleScript) — Count, Add, Take
├─ InventoryServer (Script)
└─ BreakServer, PlacementServer (Script) — обновлены
ReplicatedStorage
└─ Items (ModuleScript) — названия и рецепты
StarterGui
└─ InventoryGui (ScreenGui) — ResetOnSpawn выкл.; вместо PlacementGui
   └─ MainFrame (Frame)
      ├─ ItemFrame (ScrollingFrame) — UIGridLayout
      │  └─ Sample (ImageButton) — ItemName, Count (TextLabel); Visible выкл.
      └─ InventoryLocal (LocalScript)
Players → игрок
└─ Inventory (Folder) — IntValue на каждый предмет: имя — предмет, Value — сколько

Шаги

  1. Инвентарь — папка «Inventory» в игроке, внутри IntValue-счётчики: «Wood» = 3, «Stone» = 5. Клиент видит, менять может только сервер.
  2. ServerScriptService → ModuleScript «Inventory» (Count, Add, Take) и Script «InventoryServer» — создаёт папку.
  3. ReplicatedStorage → ModuleScript «Items»: названия предметов по-русски (и рецепты — для крафта).
  4. InventoryGui → MainFrame → ScrollingFrame «ItemFrame» с UIGridLayout и шаблоном ImageButton «Sample» (внутри TextLabel «ItemName» и «Count»). LocalScript «InventoryLocal» в MainFrame — он же ставит блоки, PlacementGui больше не нужен.
  5. Картинка ячейки — Texture «Texture» внутри блока в ObjectFolder (свойство Texture). Нет картинки — видно название.
  6. I — открыть или закрыть инвентарь. Клик по ячейке — ставить этот блок. Сервер забирает блок из инвентаря.
  7. BreakServer теперь кладёт добычу (атрибут Drop) в инвентарь того, кто сломал.

Скрипты

Inventory

Где: ServerScriptService · ModuleScript
Зачем: Счётчики предметов.

-- таблица модуля
local Inventory = {}

-- папка инвентаря игрока
local function Folder(player)
    -- создаёт InventoryServer
    return player:FindFirstChild("Inventory")
-- конец функции
end

-- сколько у игрока предмета
function Inventory.Count(player, name)
    -- инвентарь
    local folder = Folder(player)
    -- ячейка
    local slot = folder and folder:FindFirstChild(name)
    -- число или 0
    return slot and slot.Value or 0
-- конец функции
end

-- добавить
function Inventory.Add(player, name, amount)
    -- инвентарь
    local folder = Folder(player)
    -- нет инвентаря
    if not folder then return end
    -- ячейка
    local slot = folder:FindFirstChild(name)
    -- такого ещё нет
    if not slot then
        -- новая ячейка-счётчик
        slot = Instance.new("IntValue")
        -- имя = предмет
        slot.Name = name
        -- в инвентарь
        slot.Parent = folder
    -- конец проверки
    end
    -- прибавляем
    slot.Value += amount
-- конец функции
end

-- забрать; false — не хватает
function Inventory.Take(player, name, amount)
    -- не хватает
    if Inventory.Count(player, name) < amount then return false end
    -- ячейка
    local slot = Folder(player)[name]
    -- вычитаем
    slot.Value -= amount
    -- закончился — ячейка пропадает
    if slot.Value <= 0 then slot:Destroy() end
    -- получилось
    return true
-- конец функции
end

-- отдаём модуль
return Inventory

InventoryServer

Где: ServerScriptService · Script
Зачем: Папка инвентаря.

-- игроки
local Players = game:GetService("Players")

-- игрок зашёл
local function Setup(player)
    -- инвентарь
    local folder = Instance.new("Folder")
    -- имя — его ищут скрипты
    folder.Name = "Inventory"
    -- в игрока — клиент видит свои предметы
    folder.Parent = player
-- конец функции
end
-- новые игроки
Players.PlayerAdded:Connect(Setup)
-- кто зашёл раньше скрипта
for _, player in ipairs(Players:GetPlayers()) do Setup(player) end

Items

Где: ReplicatedStorage · ModuleScript
Зачем: Названия и рецепты.

-- таблица модуля
local Items = {}

-- как предметы называются для игрока
Items.Names = {
    -- блоки
    Wood = "Дерево", Stone = "Камень", Glass = "Стекло", Fence = "Забор",
    -- инструменты
    Pickaxe = "Кирка", Axe = "Топор", Shovel = "Лопата",
-- конец таблицы
}

-- рецепты: предмет → что нужно
Items.Recipes = {
    -- стекло из камня
    Glass = {Stone = 2},
    -- забор из дерева
    Fence = {Wood = 2},
    -- лопата
    Shovel = {Wood = 2},
    -- топор
    Axe = {Wood = 2, Stone = 1},
    -- кирка
    Pickaxe = {Wood = 1, Stone = 2},
-- конец таблицы
}

-- отдаём модуль
return Items

InventoryLocal

Где: StarterGui → InventoryGui → MainFrame · LocalScript
Зачем: Ячейки и размещение.

-- игроки
local Players = game:GetService("Players")
-- общее хранилище
local RS = game:GetService("ReplicatedStorage")
-- клавиатура
local UIS = game:GetService("UserInputService")
-- каждый кадр
local RunService = game:GetService("RunService")
-- этот игрок
local player = Players.LocalPlayer
-- куда указывает курсор
local mouse = player:GetMouse()
-- просим сервер поставить блок
local placementEvent = RS:WaitForChild("PlacementEvent")
-- образцы блоков
local objects = RS:WaitForChild("ObjectFolder")
-- названия предметов
local Items = require(RS:WaitForChild("Items"))
-- предметы игрока
local inventory = player:WaitForChild("Inventory")
-- MainFrame — окно инвентаря
local frame = script.Parent
-- список ячеек
local itemFrame = frame:WaitForChild("ItemFrame")
-- шаблон ячейки
local sample = itemFrame:WaitForChild("Sample")
-- I — открыть или закрыть инвентарь
local TOGGLE_KEY = Enum.KeyCode.I
-- шаг сетки — размер блока
local GRID = 4
-- полупрозрачный блок под курсором
local preview = nil
-- какой блок ставим
local currentName = nil
-- поворот в градусах
local rotation = 0

-- закончить размещение
local function Stop()
    -- убираем призрак
    if preview then preview:Destroy() end
    -- ничего не ставим
    preview = nil
    -- блока нет
    currentName = nil
-- конец функции
end

-- начать ставить блок
local function Start(name)
    -- старый призрак убираем
    Stop()
    -- образец
    local object = objects:FindFirstChild(name)
    -- такого блока нет
    if not object then return end
    -- копия только у этого игрока
    preview = object:Clone()
    -- все детали
    for _, part in ipairs(preview:GetDescendants()) do
        -- деталь
        if part:IsA("BasePart") then
            -- полупрозрачная (MainPart так и остаётся невидимой)
            part.Transparency = math.max(part.Transparency, 0.6)
            -- сквозь
            part.CanCollide = false
            -- курсор её не замечает
            part.CanQuery = false
        -- конец проверки
        end
    -- конец перебора
    end
    -- показываем
    preview.Parent = workspace
    -- mouse.Hit смотрит сквозь призрак
    mouse.TargetFilter = preview
    -- запоминаем
    currentName = name
-- конец функции
end

-- каждый кадр
RunService.RenderStepped:Connect(function()
    -- ничего не ставим
    if not preview or not preview.PrimaryPart then return end
    -- точка под курсором
    local hit = mouse.Hit.Position
    -- по сетке
    local x = math.floor(hit.X / GRID + 0.5) * GRID
    -- по сетке
    local z = math.floor(hit.Z / GRID + 0.5) * GRID
    -- стоит на поверхности, а не утоплен
    local y = hit.Y + preview.PrimaryPart.Size.Y / 2
    -- двигаем призрак
    preview:PivotTo(CFrame.new(x, y, z) * CFrame.Angles(0, math.rad(rotation), 0))
-- конец функции
end)

-- нажали клавишу — подписка одна
UIS.InputBegan:Connect(function(input, processed)
    -- печатаем в чате
    if processed then return end
    -- R
    if input.KeyCode == Enum.KeyCode.R then
        -- поворот на 45°
        rotation = (rotation + 45) % 360
    -- Q
    elseif input.KeyCode == Enum.KeyCode.Q then
        -- отмена
        Stop()
    -- I
    elseif input.KeyCode == TOGGLE_KEY then
        -- инвентарь открыть / закрыть
        frame.Visible = not frame.Visible
    -- клик по миру (клик по кнопке отсеян processed)
    elseif input.UserInputType == Enum.UserInputType.MouseButton1 and preview then
        -- сервер решает, можно ли
        placementEvent:FireServer(currentName, preview:GetPivot())
        -- призрак убираем
        Stop()
    -- конец проверки
    end
-- конец функции
end)

-- перерисовать ячейки инвентаря
local function Refresh()
    -- старые ячейки
    for _, card in ipairs(itemFrame:GetChildren()) do
        -- убираем (шаблон оставляем)
        if card:IsA("ImageButton") and card ~= sample then card:Destroy() end
    -- конец перебора
    end
    -- каждый предмет
    for _, slot in ipairs(inventory:GetChildren()) do
        -- копия шаблона
        local card = sample:Clone()
        -- имя
        card.Name = slot.Name
        -- название по-русски
        card.ItemName.Text = Items.Names[slot.Name] or slot.Name
        -- сколько
        card.Count.Text = "× " .. slot.Value
        -- блок, который можно поставить
        local object = objects:FindFirstChild(slot.Name)
        -- картинка (Texture внутри блока)
        local icon = object and object:FindFirstChild("Texture")
        -- на кнопку
        card.Image = icon and icon.Texture or ""
        -- шаблон был скрыт
        card.Visible = true
        -- в список
        card.Parent = itemFrame
        -- клик — ставить этот блок
        card.MouseButton1Click:Connect(function() Start(slot.Name) end)
    -- конец перебора
    end
-- конец функции
end
-- новый предмет
inventory.ChildAdded:Connect(function(slot)
    -- число изменилось — перерисовать
    slot.Changed:Connect(Refresh)
    -- сразу
    Refresh()
-- конец функции
end)
-- предмет закончился
inventory.ChildRemoved:Connect(Refresh)
-- предметы, что уже есть
for _, slot in ipairs(inventory:GetChildren()) do slot.Changed:Connect(Refresh) end
-- сразу при входе
Refresh()

PlacementServer

Где: ServerScriptService · Script
Зачем: Ставит, если блок есть.

-- общее хранилище
local RS = game:GetService("ReplicatedStorage")
-- теги
local CS = game:GetService("CollectionService")
-- клиент просит поставить
local placementEvent = RS:WaitForChild("PlacementEvent")
-- образцы
local objects = RS:WaitForChild("ObjectFolder")
-- куда ставим постройки
local placed = workspace:WaitForChild("Placed")
-- дальше ставить нельзя
local MAX_DISTANCE = 40
-- инвентарь
local Inventory = require(game:GetService("ServerScriptService"):WaitForChild("Inventory"))

-- клиент прислал блок и место
placementEvent.OnServerEvent:Connect(function(player, name, cf)
    -- пришёл мусор
    if typeof(name) ~= "string" or typeof(cf) ~= "CFrame" then return end
    -- образец
    local sample = objects:FindFirstChild(name)
    -- нет такого или слишком далеко
    if not sample or player:DistanceFromCharacter(cf.Position) > MAX_DISTANCE then return end
    -- блока нет в инвентаре — не ставим
    if not Inventory.Take(player, name, 1) then return end
    -- копия
    local object = sample:Clone()
    -- на место
    object:PivotTo(cf)
    -- в мир — теги копируются, блок можно сломать
    object.Parent = placed
-- конец функции
end)

BreakServer

Где: ServerScriptService · Script
Зачем: Добыча в инвентарь.

-- теги: все ломаемые объекты помечены «Breakable»
local CS = game:GetService("CollectionService")
-- игроки
local Players = game:GetService("Players")
-- тег
local TAG = "Breakable"
-- через сколько секунд вырастает дерево или камень
local RESPAWN = 60
-- инвентарь
local Inventory = require(game:GetService("ServerScriptService"):WaitForChild("Inventory"))

-- настраиваем один ломаемый объект
local function Setup(model)
    -- образец в хранилище или уже настроен
    if not model:IsDescendantOf(workspace) or model:GetAttribute("Ready") then return end
    -- MainPart — невидимая коробка вокруг объекта
    local main = model:IsA("Model") and model.PrimaryPart
    -- сколько ударов выдержит
    local hits = model:FindFirstChild("HitsToBreak")
    -- объект собран неправильно
    if not main or not hits then return end
    -- копия — чтобы вырастить снова
    local sample = model:Clone()
    -- настроен
    model:SetAttribute("Ready", true)
    -- полная прочность
    local maxHits = hits.Value
    -- полоска прочности
    local gui = main:FindFirstChild("Health")
    -- пока не били — не видно
    if gui then gui.Enabled = false end
    -- инструмент → номер последнего засчитанного взмаха
    local lastSwing = {}
    -- чего-то коснулись
    main.Touched:Connect(function(hit)
        -- инструмент
        local tool = hit:FindFirstAncestorOfClass("Tool")
        -- идёт ли удар
        local mining = tool and tool:FindFirstChild("Mining")
        -- не удар или не тот инструмент
        if not mining or not mining.Value or tool.Name ~= model:GetAttribute("Tool") then return end
        -- номер взмаха
        local swing = tool:GetAttribute("Swing")
        -- этот взмах уже засчитан
        if lastSwing[tool] == swing then return end
        -- запоминаем
        lastSwing[tool] = swing
        -- удар
        hits.Value -= 1
        -- прочность инструмента
        local wear = tool:FindFirstChild("Wear")
        -- изнашивается
        if wear then wear.Value -= 1 end
        -- полоска есть
        if gui then
            -- показываем
            gui.Enabled = true
            -- сколько осталось
            gui.Green.Size = UDim2.fromScale(math.max(hits.Value, 0) / maxHits, 1)
        -- конец проверки
        end
        -- ещё держится
        if hits.Value > 0 then return end
        -- кто сломал
        local player = Players:GetPlayerFromCharacter(tool.Parent)
        -- добыча — в инвентарь
        if player then Inventory.Add(player, model:GetAttribute("Drop") or model.Name, 1) end
        -- где стоял
        local parent = model.Parent
        -- сломан
        model:Destroy()
        -- природный объект
        if model:GetAttribute("Respawn") then
            -- через минуту вырастет снова
            task.delay(RESPAWN, function() sample:Clone().Parent = parent end)
        -- конец проверки
        end
    -- конец функции
    end)
-- конец функции
end

-- все, что уже есть
for _, model in ipairs(CS:GetTagged(TAG)) do Setup(model) end
-- новые: поставленные блоки, выросшие деревья
CS:GetInstanceAddedSignal(TAG):Connect(Setup)

Проверка

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