Итак, в предыдущем уроке сделано уменьшение сытости от времени. Теперь же нужно реализовать как эту сытость восполнять. Само собой что это значит, что нужно что-то съесть. (Ну и много ещё чего попутно…)
Наш текущий инвентарь не позволяет использовать что-либо на прямую из него. Поэтому у нас есть пара вариантов, первый — переделать инвентарь, чтобы можно было в нём выбирать вещь и использовать её. Второй вариант добавить кнопку на экран чтобы автоматически есть пригодное в инвентаре. И второй вариант гораздо проще (кажется) но в игре будет мало применим. И потому нам придётся переделывать сам инвентарь. Пусть это будет нечто примитивное (на вроде экрана крафта), но более применимое.
Но что в данный момент раздражает, так это отсутствие индикатора сытости на экране. Поэтому сделаем простейший вариант HUD для него с локальным скриптом. Для этого у нас всё есть — текущий уровень сытости и его максимальное значение. Поделив одно на другое, мы получим число от 0 до 1. Это именно то, что задаём относительные размеры элементов GUI. Добавим наш индикатор в HUD:

Со скриптом:
-- отображение прогресс бара текущего уровня сытости local Players = game:GetService("Players") local ReplicatedStorage = game:GetService("ReplicatedStorage") local player = Players.LocalPlayer local config = require( ReplicatedStorage:WaitForChild("GameConfig") ) local maxSatiety = config.SatietyMaximum -- максимальная сытость local timer = config.SatietyInterval / 2 -- половина интервала обновления while task.wait(timer) do local statuses = player:FindFirstChild("Statuses") if statuses then -- скрипт может загрузиться в любой момент времени local satiety = statuses:FindFirstChild("Satiety") if satiety then local procent = math.clamp(satiety.Value / maxSatiety, 0, 1) -- только относительное обращение script.Parent.Procent.Text = tostring( math.floor(procent * 100) ) .. "%" -- размер промежутка от 0 до 1 script.Parent.Bar.Size = UDim2.new(1, 0, procent,0) end end end
Ну вот, от раздражающего фактора избавились. Займёмся вторым — переделкой инвентаря… Итак, с учётом что мы не только для ПК, но и для носимых гаджетов игры делаем, то нам нужна будет кнопка использования, кроме самого списка того, что в нём имеется. Естественно, после того как мы это выберем. Самое простое — просто продублируем GUI крафта и уже в нём будем делать правки. Разве что сразу заменим фоновый цвет у копии, чтобы не путать с оригиналом. Ну и надпись на кнопке поменяем с Craft, на Use. Получим нечто такое:

Переходим к скрипту. Первое — заполнять его мы будем уже из инвентаря, что в игроке хранится, а не из таблицы. Точнее не так, заполнять будем из инвентаря, а вот описание будем брать из таблицы. И первое что мы получим — отсутствие количества на экране, а второе — нам не известно что же мы можем использовать, а что нет. С первым справится легко — просто добавим текстовую метку под называнием, а вот для второго нам придётся обновлять таблицу товаров добавив туда флаг возможности использования. Но у нас появляется очередной вопрос — в какую таблицу добавлять? А может создать отдельную таблицу для этого? Сейчас мы так и сделаем, для съестного создадим отдельную таблицу. Забегая наперёд скажу, что для всего этого (ресурсы, крафт, использование) нужна одна таблица в которой будет всё и сразу.
В данный момент что можем добавить? Можем добавить добычу ягод с кустов/цветочков. Соответственно откорректируем и таблицу добычи изменив лишь один момент:
Flowers = { Axe = { -- инструмент для добычи None = 70, -- ничего Berry = 10, -- ягода Leaves = 20, -- листья } -- кирка НЕ подходящий инструмент }
И соответственно создадим новую табличку Used. Банально пропишем что и на сколько будет изменятся при использовании:
return { Berry = { Title = "Ягоды", -- название Desc = "Вполне съедобные ягоды", -- описание Param = "Satiety", -- изменяемый параметр Delta = 10, -- на указанное число }, }
Пора уже переходить к отображению инвентаря. Скрестим старые инвентарь и крафт.
Первичное отличие окна инвентаря от окна крафта, что у нас будет постоянно меняющийся список содержимого инвентаря (ровно как и количество), как следствие нам нужно иметь возможность перерисовать содержимое инвентаря. Второе, у нас будет две таблицы откуда брать информацию из ресурсов и крафта. Третье, только наличие в таблице использования, даст разрешение на отображение кнопки «использовать». Потому и говорил, что нужна будет единая таблица на все ресурсы игры. Но мы же не ищем лёгких пути, изобретая велосипед по пути наименьшего сопротивления?
Потому наш скрипт будет из нескольких частей: отображение актуальной информации, отображение информации о выбранном элементе, кнопка для использование элемента.
-- скрипт инвентаря с использованием local Players = game:GetService("Players") local ReplicatedStorage = game:GetService("ReplicatedStorage") local used = require( ReplicatedStorage:WaitForChild("Resource"):WaitForChild("Used") ) local craftTable = require( ReplicatedStorage:WaitForChild("Resource"):WaitForChild("Craft") ) local config = require( ReplicatedStorage:WaitForChild("GameConfig") ) local player = Players.LocalPlayer screen = script.Parent -- наш экран local invent = player:WaitForChild("Inventory") -- ссылка на инвентарь -- элементы GUI local info = screen:WaitForChild("Info") local selected = info:WaitForChild("Select") local recept = screen:WaitForChild("Recipient") local sample = recept:WaitForChild("Sample"):Clone() local ui = recept:WaitForChild("UIListLayout"):Clone() -- изменение выбранного рецепта local function SelectChange(value) local val = selected.Value -- что именно выбрали local flag = false -- флаг применимости local title = "" -- Название local desc = "" -- описание local comp = "" -- состав -- информация по таблице Craft local craft = craftTable[val] if craft then title = craft.Title local res = "" for a, b in pairs(craft.Resource) do if res ~= "" then -- добавим перевод строки res = res .. "\n" end res = res .. a .. " x " .. tostring(b) end comp = res end -- информация по таблице Used local element = used[val] if element then -- элемент найден if element.Param and element.Delta then -- описано применение title = element.Title desc = element.Desc local status = player:FindFirstChild("Statuses") if status then local param = status:FindFirstChild(element.Param) if param then -- есть что изменять flag = true else warn("Параметр", element.Param, "не найден в игроке") end else warn("Статусы игрока ещё не прогружены") end end end -- не найдено информации if title == "" then title = val end info.Title.Text = title info.Description.Text = desc info.Compound.Text = comp if flag then info.btnUse.Interactable = true info.btnUse.TextColor3 = Color3.new(0, 1, 0) else info.btnUse.Interactable = false info.btnUse.TextColor3 = Color3.new(1, 0, 0) end end selected.Changed:Connect(SelectChange) -- выбор предмета function SelectUse() -- раз сюда попали, значит проверки уже сделаны local val = selected.Value -- что именно выбрали local element = used[val] -- Прибавим в состоянии local param = player.Statuses:FindFirstChild(element.Param) param.Value = param.Value + element.Delta if param.Value > config.SatietyMaximum then -- не более максимума param.Value = config.SatietyMaximum end -- Убавим в количестве local inv = player.Inventory:FindFirstChild(val) inv.Value = inv.Value - 1 if inv.Value == 0 then inv:Destroy() -- уничтожим, раз кончилось end end info.btnUse.Activated:Connect(SelectUse) local flag = Instance.new("BoolValue") local connect = {} local debounce = false -- в состоянии перерисовывания function ReloadScreen() task.wait(0.1) if debounce == false then debounce = true recept:ClearAllChildren() local uis = ui:Clone() uis.Parent = recept for _, nam in pairs(invent:GetChildren() ) do -- имя элемента -- создание элемента списка local element = sample:Clone() -- копия копии элемента element.Names.Text = nam.Name -- укажем имя element.Amount.Text = nam.Value -- укажем значение element.Parent = recept -- поместим в список element.Button.Activated:Connect(function() -- для активации выбора selected.Value = nam.Name end) if connect[nam] == nil then connect[nam] = nam.Changed:Connect(function() flag.Value = not flag.Value -- при измении количества изменится флаг end) end end debounce = false end end invent.ChildAdded:Connect(ReloadScreen) invent.ChildRemoved:Connect(ReloadScreen) ReloadScreen() flag.Changed:Connect(ReloadScreen) -- флаг на изменение количества
Проверяем. Всё вроде как работает, но.. что-то не так. И это самое «не так» обнаружилось благодаря ранее сделанному индикатору текущей сытости. Мы съедаем ягодку, сытость увеличивается и.. тут же начинает уменьшаться от предыдущего значения. Отсюда и выяснилось что мы работает на стороне клиента (как и ранее созданный скрипт крафта) и соответственно изменения не передаются на сервер. В следствии всего этого, все наши изменения ни к чему не приводят, ведь серверные данные не меняются и что творится на клиенте — проблемы клиента.
И тут наступает новый момент просветления — раз мы можем на клиенте делать что хотим, то можем и накрафтить сколько чего угодно. И да, и нет. Т.к. сервер об этом не в курсе, смысла в этом тоже не будет. Ведь в инвентаре сервера этих вещей не появится и соответственно, использовать их не получится.
Уведомить сервер мы можем только через удалённый вызов. А значит нам нужно добавить:
- связующий[ие] RemoteFunction или RemoteEvent в наш ReplicatedStorage
- вызов из скриптов крафта и инвентаря в сторону сервера
- обработчик вызовов на сервере, с проверками на корректность исполнения
По итогу нам нужно будет не только написать серверный скрипт, но и изменить клиентские скрипты (оттуда мы сразу уберём все что в нажатиях кнопок и перенесём в серверный скрипт). Приступаем.
Для начала, мы создадим папочку содержащую все варианты вызовов между скриптами, с соответствующими названиями:

После чего сделаем болванку в ServerScriptService нашего управляющего скрипта:
-- Главный, управляющий игрой, скрипт local Players = game:GetService("Players") local ReplicatedStorage = game:GetService("ReplicatedStorage") -- объявляем все возможные удалённые вызовы на приём local remoteFunction = ReplicatedStorage.Remote.CommandFunction local remoteEvent = ReplicatedStorage.Remote.CommandEvent local remoteBindEvent = ReplicatedStorage.Remote.CommandBindEvent local remoteBindFunction = ReplicatedStorage.Remote.CommandBindFunction -- Это и будет наш главный мозг и контролёр local function Command(player, cmd, par1, par2, par3) local result = nil -- return result end -- объявляем все возможные удалённые вызовы на приём remoteFunction.OnServerInvoke = Command remoteEvent.OnServerEvent:Connect(Command) remoteBindEvent.Event:Connect(Command) remoteBindFunction.OnInvoke = Command
Конечно главное это наша чудная функция. «Чудная» потому что в ней сделано одно допущение — что всегда присутствует player в вызове. На самом же деле это не так — BindableEvent и BindableFunction не передают параметра вызывающего игрока, потому как используются для вызовов внутри связки Sever-Server или Client-Client. Как следствие, если мы таки будем их использовать, то надо это иметь ввиду и передавать какую-нибудь заглушку вместо игрока (когда его у нас реально не будет).
Теперь нам остаётся только добавить пару команд (с проверками) на выполнение запросов от клиентской части игры, т.е. при нажатии кнопки интерфейса. Для начала наша функция принимает вид:
local function Command(player, cmd, par1, par2, par3) local result = nil -- if cmd == "Craft" then -- получена команда крафта -- end if cmd == "Used" then -- получена команда использования -- end return result end
Теперь же начнём её наполнение. И вот тут уже надо будет делать всё по взрослому — со всеми возможными проверками корректности выполнения команды. Ведь если упадёт серверный скрипт, то игра будет сломана для абсолютно всех игроков. В отличии от сломавшегося локального скрипта, что нарушит игру только конкретному игроку.
Для начала добавим в скрипт инвентаря в начало строчку со ссылкой на RemoteEvent:
local remoteEvent = ReplicatedStorage.Remote.CommandEvent
А после изменим часть с нажатием кнопки на экране:
-- выбор предмета function SelectUse() -- раз сюда попали, значит проверки уже сделаны local val = selected.Value -- что именно выбрали remoteEvent:FireServer("Used", val) -- вызываем сервер end info.btnUse.Activated:Connect(SelectUse)
А всё что было в данной функции перенесём в функцию на сервере, добавив контроля:
if cmd == "Used" then -- получена команда использования -- par1 = название того, что мы использовали local element = used[par1] local param = player.Statuses:FindFirstChild(element.Param) if param then -- Убавим в количестве local inv = player.Inventory:FindFirstChild(par1) if inv then inv.Value = inv.Value - 1 if inv.Value == 0 then inv:Destroy() -- уничтожим, раз кончилось end -- Прибавим в состоянии param.Value = param.Value + element.Delta if param.Value > config.SatietyMaximum then -- не более максимума param.Value = config.SatietyMaximum end else warn("Не найдено", par1, "для использования") end -- result = true -- не нужно, т.к. это вызывается по ивенту else warn("Параметр", element.Param, "не найден!") end end
Проверили. Всё работает, теперь как задумано. Произведём аналогичные действия со скриптом для GUI крафта и его серверной реализации. Но будет и отличие — это окно у нас не перерисовывается (ага, косяк с нашей стороны — нет), а потому уже будем использовать вызов удалённой функции вместо события.
Добавляем в начало:
local remoteFunction = ReplicatedStorage.Remote.CommandFunction
И соответственно код нажатия кнопки так же будет изменён:
-- создание предмета function Craft() local val = selected.Value -- что именно выбрали для крафта local flag = remoteFunction:InvokeServer("Craft", val) -- вызываем сервер if flag then info.btnCraft.Interactable = true info.btnCraft.TextColor3 = Color3.new(0, 1, 0) else info.btnCraft.Interactable = false info.btnCraft.TextColor3 = Color3.new(1, 0, 0) end end info.btnCraft.Activated:Connect(Craft)
Теперь очередь куска в серверном скрипте. И в итоге наш серверный скрипт примет такой вид:
-- Главный, управляющий игрой, скрипт local Players = game:GetService("Players") local ReplicatedStorage = game:GetService("ReplicatedStorage") -- объявляем все возможные удалённые вызовы на приём local remoteFunction = ReplicatedStorage.Remote.CommandFunction local remoteEvent = ReplicatedStorage.Remote.CommandEvent local remoteBindEvent = ReplicatedStorage.Remote.CommandBindEvent local remoteBindFunction = ReplicatedStorage.Remote.CommandBindFunction -- подключение таблиц local used = require( ReplicatedStorage:WaitForChild("Resource"):WaitForChild("Used") ) local craftTable = require( ReplicatedStorage:WaitForChild("Resource"):WaitForChild("Craft") ) local config = require( ReplicatedStorage:WaitForChild("GameConfig") ) -- Это и будет наш главный мозг и контролёр local function Command(player, cmd, par1, par2, par3) local result = nil print("Command:", player, cmd, par1, par2, par3) -- получена команда крафта if cmd == "Craft" then -- par1 - что мы собрались крафтить local element = craftTable[par1] local invent = player:FindFirstChild("Inventory") if invent then local correct = true -- проверим наличие ресурсов для крафта for a, b in pairs(element.Resource) do local res = invent:FindFirstChild(a) if res.Value < b then correct = false end end if correct == true then -- заберём ресурсы for a, b in pairs(element.Resource) do local res = invent:FindFirstChild(a) res.Value = res.Value - b -- если кончилось нужно удалить из инвентаря if res.Value == 0 then res:Destroy() end end -- добавим что скрафтили local new = invent:FindFirstChild(par1) if new == nil then -- ещё нет в инвентаре new = Instance.new("NumberValue") new.Name = par1 end new.Value = new.Value + element.Result new.Parent = invent -- проверка можно ли ещё раз крафтить local flag = true -- можно крафтить for a, b in pairs(element.Resource) do local res = invent:FindFirstChild(a) if res then if b > res.Value then flag = false -- не хватает ресурса end else -- нет ресурса flag = false end end result = flag -- можно ли крафтить и далее else -- крафт не удался, не хватает ресурсов result = false end else warn("Проблема с инвентарём игрока", player) end end -- получена команда использования if cmd == "Used" then -- получена команда использования -- par1 = название того, что мы использовали local element = used[par1] local param = player.Statuses:FindFirstChild(element.Param) if param then -- проверим наличие local inv = player.Inventory:FindFirstChild(par1) if inv then -- Убавим в количестве inv.Value = inv.Value - 1 if inv.Value == 0 then inv:Destroy() -- уничтожим, раз кончилось end -- Прибавим в состоянии param.Value = param.Value + element.Delta if param.Value > config.SatietyMaximum then -- не более максимума param.Value = config.SatietyMaximum end else warn("Не найдено", par1, "для использования") end -- result = true -- не нужно, т.к. это вызывается по ивенту else warn("Параметр", element.Param, "не найден!") end end return result end -- объявляем все возможные удалённые вызовы на приём remoteFunction.OnServerInvoke = Command remoteEvent.OnServerEvent:Connect(Command) remoteBindEvent.Event:Connect(Command) remoteBindFunction.OnInvoke = Command
Ну теперь то всё? Запускаем. Да вещи крафтятся, еда естся, но… Здоровье персонажа не восполняется! И правда, мы же скрипт регенерации ранее отключили. Когда у нас должна работать регенерация здоровья? Очевидно, что только тогда, когда мы достаточно сытые. Добавим в GameConfig пару строк:
HealthRegen = 1, -- Регенерация за интервал HealthSatiety = 50, -- выше какого уровня работает регенерация
И полезем править скрипт HealthModifier добавив туда проверку сытости и восполнения здоровья:
-- управление здоровьем персонажей игроков local Players = game:GetService("Players") local ReplicatedStorage = game:GetService("ReplicatedStorage") local config = require( ReplicatedStorage:WaitForChild("GameConfig") ) local timer = {} -- массив таймеров игроков while task.wait() do for _, player in pairs( Players:GetPlayers() ) do -- для всех игроков -- проверим наличие таймера у игрока if not timer[player] then timer[player] = {} -- создадим если его нет end local character = player.Character -- персонаж игрока if character and character.Parent then -- персонаж в игре -- персонаж может появится и раньше чем переменная сытости local toDel = character:FindFirstChild("Health") if toDel then toDel:Destroy() -- удалить скрипт регенерации end -- будем проверять наступление события по таймеру if not timer[player].Satiety then timer[player].Satiety = tick() -- таймер ещё не был создан end local changeHealth = 0 -- флаг для изменения здоровья -- наступило время по таймеру сытости if tick() >= timer[player].Satiety + config.SatietyInterval then timer[player].Satiety = tick() -- обновляем таймер local statuses = player:FindFirstChild("Statuses") -- папка статусов if statuses then local satiety = statuses:FindFirstChild("Satiety") if satiety then -- переменная сытости if satiety.Value > 0 then -- если сытость больше 0 satiety.Value = satiety.Value + config.SatietyDelta -- уменьшаем значение if satiety.Value > config.HealthSatiety then -- достаточно сыт changeHealth = config.HealthRegen -- регенерация end else -- иначе уже влияет на здоровье satiety.Value = 0 -- сытость в ноль changeHealth = config.HealthDelta -- нужно менять здоровье end end end end -- а в финале скрипта - обработка здоровья if changeHealth ~= 0 then -- нужно ли его менять if not timer[player].Health then timer[player].Health = tick() -- таймер ещё не был создан end if tick() >= timer[player].Health + config.HealthInterval then timer[player].Health = tick() -- обновляем таймер local humanoid = character:FindFirstChild("Humanoid") if humanoid then if humanoid.Health > 0 then -- персонаж живой humanoid.Health = humanoid.Health + changeHealth end end end end end end end
Запускаем и проверяем. Ждём пока оголодаем и здоровье станет меньше 100, после чего едим ягодки. Как только сытость становится больше 50, начинается и восстановление здоровья.
Ну вот, теперь точно минимальное всё — примитивнейший выживач готов.

Остаётся следовать традиции и приложить результирующий плейс: