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 — сколько
Где: 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Где: 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Где: 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Где: 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()Где: 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)Где: 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) и убедись: