Roblox — Выживание — Survival

Автор: | 7 марта, 2025
Поделиться...

Ну вот мы и добрались до практического применения механики выживания. Собственно что такое выживание в играх? Это когда есть некий фактор, что понижает уровень здоровье до нуля и наступает смерть персонажа, а так же есть возможность этот фактор нивелировать и/или восстанавливать здоровье на постоянку или хотя бы временно.

ТЗ или техническое задание.

Для «простоты» возьмём питание персонажа, т.е. его сытость. Само собой она будет постоянно понижаться с течением времени. Если она падает до нуля, то начинает снижаться уровень здоровья. Если здоровье падает до нуля, то персонаж умирает.

Итак, первое что нужно решить, это как мы будем.. понижать здоровье. Конечно напрашивается решение что в персонаже игрока должен быть скрипт который будет этим заниматься. И это.. не верное решение. Хорошо когда у нас несколько игроков в игре. А если их 20-50-100? Это значит что и скриптов будет столько же. Поэтому у нас будет один скрипт, который будет обслуживать всех игроков. И конечно он будет в SSS чтобы не был доступен для модификации со стороны клиентов. А ещё нам нужны два значения: на сколько и как часто будем задействовать уменьшение здоровье. И тут мы, наконец, переходим к созданию конфигурации игры. Т.е. создадим файл, в котором будут настройки игры для геймдизайнера. И тут встаёт выбор где его хранить? Для простоты можно использовать один и хранить его в ReplicatedStorage, а для секьюрности — нужно создавать два и второй хранить в ServerStorage. Мне хватит и одного в RS. И по простому он будет выглядеть вот-так:

return {
	
	--== Здоровье ==--
	HealthInterval = 1,		-- Интервал изменения здоровья
	HealthDelta = -1,		-- Дельта изменения здоровья 
	
}

После этого можно приступать и к самому скрипту, что будет перебирать всех живых персонажей в игре:

-- управление здоровьем персонажей игроков

local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local config = require( ReplicatedStorage:WaitForChild("GameConfig") )

while task.wait(config.HealthInterval) do
	for _, player in pairs( Players:GetPlayers() ) do	-- для всех игроков
		local character = player.Character				-- персонаж игрока
		if character and character.Parent then			-- персонаж в игре
			local humanoid = character:FindFirstChild("Humanoid")
			if humanoid then
				if humanoid.Health > 0  then	-- персонаж живой
					humanoid.Health = humanoid.Health + config.HealthDelta
				end
			end
		end
	end
end

Простенько. И всего лишь одним скриптом мы обработаем любое число игроков в игре. Но что мы видим когда запустим игру? Здоровье у нас не меняется. Всё дело в том, что по умолчанию в игрока добавляется скрипт Health — регенерации персонажа. Как следствие нам его нужно удалить. И наш скрипт станет таким:

-- управление здоровьем персонажей игроков

local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local config = require( ReplicatedStorage:WaitForChild("GameConfig") )

while task.wait(config.HealthInterval) do
	for _, player in pairs( Players:GetPlayers() ) do	-- для всех игроков
		local character = player.Character				-- персонаж игрока
		if character and character.Parent then			-- персонаж в игре
			local toDel = character:FindFirstChild("Health")
			if toDel then
				toDel:Destroy()							-- удалить скрипт регенерации 
			end
			local humanoid = character:FindFirstChild("Humanoid")
			if humanoid then
				if humanoid.Health > 0  then			-- персонаж живой
					humanoid.Health = humanoid.Health + config.HealthDelta
				end
			end
		end
	end
end

Теперь скрипта нет и наше здоровье постепенно уменьшается. Теперь пора бы переходить к сытости, ведь в ТЗ сказано, здоровье уменьшается если сытость равна нулю. А потому, для начала добавим эту самую сытость в нашего игрока. Сделаем это обычной переменной на этапе загрузки персонажа. В будущем мы сможем сохранять/брать это значение в нашей базе данных, чтобы хранить характеристику между сессиями игры. Сейчас же сделаем это в скрипте PlayerAdded аналогично тому, как добавили папку инвентаря:

-- будет работать при подключении игрока и спавне/респавне персонажей

local Players = game:GetService("Players")

local function onCharacterAdded(character)
	print(character.Name .. " has spawned")
end

local function onCharacterRemoving(character)
	print(character.Name .. " is despawning")
end

local function onPlayerAdded(player)
	
	player.CharacterAdded:Connect(onCharacterAdded)
	player.CharacterRemoving:Connect(onCharacterRemoving)
	
	-- добавить папку инвентаря
	local invent = Instance.new("Folder")
	invent.Name = "Inventory"
	invent.Parent = player
	
	-- добавить папку статусов
	local statuses = Instance.new("Folder")
	statuses.Name = "Statuses"
	statuses.Parent = player
	
	-- добавить значение сытости
	local satiety = Instance.new("NumberValue")
	satiety.Name = "Satiety"
	satiety.Value = 100			-- первичное значение
	satiety.Parent = statuses
	
end

Players.PlayerAdded:Connect(onPlayerAdded)

И что теперь? Теперь нам надо поправить наш конфиг и скрипт управления здоровьем. Интервал станет общим и добавится переменная изменения сытости. Конфиг станет таким:

return {
	
	ChectInterval = 1,		-- интервал обработки статусов персонажа
	
	--== Статусы персонажа ==--
	HealthDelta = -1,		-- Дельта изменения здоровья
	SatietyDelta = -1,		-- Дельта изменения сытости

}

Ну а со скриптом придётся повозиться. Причина банальная — все объекты в игре загружаются в случайном порядке, как бог на душу положит. Поэтому нам нужно проконтролировать, что переменная сытости уже есть и только тогда продолжать исполнение скрипта:

-- управление здоровьем персонажей игроков

local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local config = require( ReplicatedStorage:WaitForChild("GameConfig") )

while task.wait(config.ChectInterval) do
	for _, player in pairs( Players:GetPlayers() ) do		-- для всех игроков
		local character = player.Character			-- персонаж игрока
		if character and character.Parent then		-- персонаж в игре
			
			-- персонаж может появится и раньше чем переменная сытости
			local toDel = character:FindFirstChild("Health")
			if toDel then
				toDel:Destroy()						-- удалить скрипт регенерации 
			end
			
			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	-- уменьшаем значение
					else										-- иначе уже влияет на здоровье
						satiety.Value = 0

						local humanoid = character:FindFirstChild("Humanoid")
						if humanoid then
							if humanoid.Health > 0  then		-- персонаж живой
								humanoid.Health = humanoid.Health + config.HealthDelta
							end
						end
					end
				end
			end
		end
	end
end

Прекрасно. Теперь у нас здоровье будет уменьшаться только когда сытость упадёт до нуля. Казалось бы всё — задача выполнена? Нет. Для начала, мы избавились от интервала изменения здоровья. А кто сказал что они у нас должны совпадать? И ещё, за тем, чтобы уровень здоровья персонажа не превышал максимальный следит сам движок. А вот за уровнем максимального питания он не следит. Его тоже придётся добавить. Т.к. позже мы должны будем, как это ни странно, кормить нашего персонажа (хотя на данном этапе оно нам и не требуется). Значит нужно исправлять и конфиг и код:

return {

	--== Статусы персонажа ==--
	HealthInterval = 1,		-- интервал изменения здоровья
	HealthDelta = -1,		-- Дельта изменения здоровья
	
	SatietyInterval = 1,	-- интервал изменения сытости
	SatietyDelta = -1,		-- Дельта изменения сытости
	SatietyMaximum = 100,	-- Максимальный уровень сытости

}

А уж с кодом точно придётся повозиться. Нам нужно чтобы таймеры были раздельные для здоровья и.. любого другого статуса. А изменение здоровья может происходить не только из-за сытости, но и, например, жажды.

-- управление здоровьем персонажей игроков

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 = false				-- флаг для изменения здоровья
			
			-- наступило время по таймеру сытости
			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	-- уменьшаем значение
						else										-- иначе уже влияет на здоровье
							satiety.Value = 0		-- сытость в ноль
							changeHealth = true		-- нужно менять здоровье
						end
					end
				end
			end
			
			-- а в финале скрипта - обработка здоровья
			if changeHealth == true 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 + config.HealthDelta
						end
					end
				end
			end
		end
	end
end

Вот теперь ТЗ можно считать выполненным.

  • С течением времени сытость падает
  • Если сытость на нуле, то начинает меняться изменяться здоровье
  • Если здоровье станет нулём — персонаж умирает

Единственное, что в ТЗ не было указано каким будет сытость персонажа при возрождении после смерти. Но так как мы уже добавили в конфиг максимальный уровень сытости, то можем и добавить его установку/восстановление после появления персонажа в игре. Как мы это сделаем? Конечно исправим скрипт PlayerLoader.

-- будет работать при подключении игрока и спавне/респавне персонажей

local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local config = require( ReplicatedStorage:WaitForChild("GameConfig") )

local function onCharacterAdded(character)
	print(character.Name .. " has spawned")
	
	local player = Players:GetPlayerFromCharacter(character)
	local statuses = player:FindFirstChild("Statuses")
	if statuses then
		-- для установки стартового значения сытости после воскрешения
		local satiety = statuses:FindFirstChild("Satiety")
		if satiety then
			satiety.Value = config.SatietyMaximum
		end
	end
end

local function onCharacterRemoving(character)
	print(character.Name .. " is despawning")
end

local function onPlayerAdded(player)
	
	player.CharacterAdded:Connect(onCharacterAdded)
	player.CharacterRemoving:Connect(onCharacterRemoving)
	
	-- добавить папку инвентаря
	local invent = Instance.new("Folder")
	invent.Name = "Inventory"
	invent.Parent = player
	
	-- добавить папку статусов
	local statuses = Instance.new("Folder")
	statuses.Name = "Statuses"
	statuses.Parent = player
	
	-- добавить значение сытости
	local satiety = Instance.new("NumberValue")
	satiety.Name = "Satiety"
	satiety.Value = config.SatietyMaximum	-- первичное значение
	satiety.Parent = statuses
	
end

Players.PlayerAdded:Connect(onPlayerAdded)

Ну вот, теперь можно считать что ТЗ выполнено на 101%. Можно, конечно, ещё добавить индикатор сытости на экран, но это уже задача для мастера по GUI, а не скриптера. С нашей стороны предоставлена переменная доступная не клиенте, где и будет отображаться индикатор.

Ну и.. прикладываю плейс того, что получилось.


Поделиться...

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *