Итак, в предыдущем уроке сделано уменьшение сытости от времени. Теперь же нужно реализовать как эту сытость восполнять. Само собой что это значит, что нужно что-то съесть. (Ну и много ещё чего попутно…)
Наш текущий инвентарь не позволяет использовать что-либо на прямую из него. Поэтому у нас есть пара вариантов, первый — переделать инвентарь, чтобы можно было в нём выбирать вещь и использовать её. Второй вариант добавить кнопку на экран чтобы автоматически есть пригодное в инвентаре. И второй вариант гораздо проще (кажется) но в игре будет мало применим. И потому нам придётся переделывать сам инвентарь. Пусть это будет нечто примитивное (на вроде экрана крафта), но более применимое.
Но что в данный момент раздражает, так это отсутствие индикатора сытости на экране. Поэтому сделаем простейший вариант 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, начинается и восстановление здоровья.
Ну вот, теперь точно минимальное всё — примитивнейший выживач готов.

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