ROBLOX — Создание ресурсов для добычи — Creating resources for mining

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

В продолжении темы RPG игры рассмотрим тему спавна ресурсов с их последующей добычей. Генерацию бесконечного мира мы уже освоили. Вот в нём и будем создавать ресурсы, которые после будем добывать.

Итак, наш алгоритм:

  • создать ресурс
  • определить его объём добычи
  • добыча ресурса
  • восстановление ресурса после его истощения

Не так чтобы и уж совсем сложно? Для примера возьмём две противоположности — дерево и камень.

Как заспавнить ресурс? Определяем координату и спавним — что тут сложного? А сложность в том, что ресурсы не должны перекрываться, по крайней мере сильно… Откуда следует что сперва мы должны генерировать «низкии» ресурсы, т.е. те, что находятся возле земли и только потом тех что высоки..

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

Итак, алгоритм простой:

  • определяемся с координатой
  • с верху опускаем рейкаст
  • если он попал в terrain, то создаём ресурс

Для начала выберем несколько деревьев и камушков в тулбоксе. Само собой нам нужны mesh модели. У деревьев зададим PrimaryPart на ствол, а камней — на сам mesh. Разместим всё это в своих папках, после поместим в папку Resource и по итогу перенесём её в ReplicatedStorage.

Пора бы уже и к спавну приступить, но есть ещё одна деталька. По итогу у нас должны различаться модельки в мире. Как же это сделать?

  • Деревья. Поворот вокруг вертикальной оси и конечно изменение размера модели.
  • Камни. Смена размера модели и поворот по любой оси?

А ещё, в идеале, нам разместить парт привязки в каждой модели, чтобы на него ориентироваться о размещении объекта в мире. В камнях это просто будет парт 1х1х1 в центре. А вот в деревьях… В деревьях этот парт должен быть где-то в центре пня. Естественно в моделях деревьев у нас нет пенька, так что просто поместим парт около низа ствола. Естественно что этот парт нужно заякорить (Anchore) и указать модели что он и является теперь PrimaryPart. Получим что-то такое:

И да, нам как-то наплевать на то, как называются наши модельки в папке ресурсов. Имена мы им будем задавать при помещении клонов в мире игры.

Подготовка закончена, теперь нужно определиться, в какой момент мы их будем «сажать» на карте. Само собой карта у нас уже должна присутствовать, т.е. только после генерации. Второе, мы можем их размещать случайно, а значит нам нужен параметр плотности. Третье, на нужно задать в каких пределах будет меняться их размер. И, конечно же нам нужно задать пределы получаемых с них ресурсов. Для этого мы создадим модуль конфигурации нашего спавна. Разместим его там же, где и сами ресурсы, чтобы у нас оставалось ощущение цельности. Да и сразу будет понятно, к чему этот конфиг относится. Почему мы вынесли настройки из скрипта? Это принципиально — таким образом нам не нужно будет лезть в скрипт при подборе значений, а значит наш будущий «оптимизатор карты» не поломает сам скрипт пока будет экспериментировать.

Обращаю внимание, имена групп в конфиге соответствуют именам папок с самими ресурсами.

Теперь то всё, подготовка закончена? В принципе, да — этого нам достаточно чтобы начать программировать.

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

WaitForChild() используется в начале скрипта для гарантированной прогрузки ресурсов на клиенте. И да, на сервере это не обязательно, но лучше привыкнуть что первичная подгрузка ресурсов производится через ожидание потомка, а не через поиск первого подходящего.

Внизу у нас болванка функции для будущей генерации. Вызывать мы её будем из CreateChunk(x, z), ведь именно в ней у нас гарантировано будет создана земля, на которой и надо будет будет разместиться ресурсу. Вот только присмотревшись к самой функции мы видим что.. у нас уже имеются координаты земли и чем она заполнена.

А значит медленный рейкаст нам как бы и не к чему, да? Проверим в деле как поведёт себе генератор случайных чисел…

Чего нам ещё не хватает? Нам нужен список всех доступных ресурсов с их шансами на спавн. Добавим выборку ресов перед функцией их спавна.

-- заполним массив доступных ресурсов
local resource = {}					-- индексный массив доступных ресурсов
for name, res in pairs(config) do	-- будем брать только описанные ресурсы
	-- print(name, res)				-- его имя и описание 
	local find = folder:FindFirstChild(name)
	if find:IsA("Folder") then		-- найдено в ресурсах
		local tmp = find:GetChildren()
		if #tmp > 0 then			-- сами ресурсы присутствуют
			for a, b in pairs(tmp) do
				if b:IsA("Model") then				-- проверка что это ресурс
					if b.PrimaryPart ~= nil then	-- имеется точка привязки
						local element = {}			-- запоминаем новый элемент ресурса
						element = table.clone(res)	-- скопируем данные в элемент
						element.name = name			-- добавим название
						element.model = b			-- добавим ссылку на саму модель
						
						-- ну наконец-то добавим сам элемент в список доступных ресурсов
						table.insert(resource, element)
					end
				end
			end
		end
	end
end

Ну теперь то можем уже заспавнить наши ресурсы? По идее — да. Приступаем. Для начала добавим две строчки в начале скрипта?

math.randomseed( tick() )									-- задаём стартовое зерно
local resourceFolder = workspace:WaitForChild("Resource")	-- место размещения ресурсов

А теперь уже будем заниматься размещением наших моделек. Накидаем на скорую руку начинку функции и посмотрим на результат.

-- создание ресурса на карте по координатам
function CreateResource(x, y, z, material)
	local shans = math.random()	-- получаем значение от 0 до 1
	local num = math.random(1, #resource)	-- случайный ресурс
	if shans <= resource[num].Shans then	-- выпал шанс для размещения
		print("spawn")
		local object = resource[num].model:Clone()	-- клонируем модель
		object:PivotTo(CFrame.new(x * GRID, y, z * GRID))
		object.Name = resource[num].name
		object.Parent = resourceFolder
	end
end

Кхм… Кажется это не совсем то, что ожидалось. Во первых слишком много объектов. Во вторых у нас камни под землёй… Уменьшим шансы в конфиге.. в 10 раз и исправим вертикальное размещение.

Уже лучше, но однообразно… Добавим поворот и смену размера моделей.

-- создание ресурса на карте по координатам
function CreateResource(x, y, z, material)
	local shans = math.random()	-- получаем значение от 0 до 1
	local num = math.random(1, #resource)	-- случайный ресурс
	if shans <= resource[num].Shans then	-- выпал шанс для размещения
		print("spawn")
		local object = resource[num].model:Clone()	-- клонируем модель
		object:PivotTo(CFrame.new(x * GRID, y + GRID*2, z * GRID))
		object.Name = resource[num].name
		
		-- смена размера
		local sizer = math.random(resource[num].SizeMin * 100, resource[num].SizeMax * 100)
		object:ScaleTo(sizer/100)
		
		-- поворот по Y
		local rotation = CFrame.Angles(0, math.rad(math.random(360)), 0)
		object:PivotTo(object:GetPivot() * rotation)
		
		-- размещение в мире
		object.Parent = resourceFolder
	end
end

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

-- создание ресурса на карте по координатам
function CreateResource(x, y, z, material)
	local shans = math.random()	-- получаем значение от 0 до 1
	local num = math.random(1, #resource)	-- случайный ресурс
	if shans <= resource[num].Shans then	-- выпал шанс для размещения
		local object = resource[num].model:Clone()	-- клонируем модель
		object:PivotTo(CFrame.new(x * GRID, y + GRID*2, z * GRID))
		object.Name = resource[num].name
		
		-- смена размера
		local sizer = math.random(resource[num].SizeMin * 100, resource[num].SizeMax * 100)
		object:ScaleTo(sizer/100)
		
		-- поворот по Y
		local rotation = CFrame.Angles(0, math.rad(math.random(360)), 0)
		object:PivotTo(object:GetPivot() * rotation)
		
		-- добавим проверку на пересечение с другими объектами
		local part = Instance.new("Part")
		local orient, size = object:GetBoundingBox()	-- получаем расположение и размер
		part.Size = size
		part.CFrame = orient
		local check = workspace:GetPartsInPart(part)	-- возвращает nil или массив пересечений 
		if check[1] ~= nil then
			-- удаление как не прошедшее проверку
			print(check)
			object:Destroy()
		else
			-- размещение в мире
			print("spawn")
			object.Parent = resourceFolder
		end
		part:Destroy()
	end
end

Ну вот, совсем другое дело. Остаётся поиграться с конфигом и наборами элементов. Вот я, например, добавил папочку с цветочками…

Как вы заметили (заметили же?) на карте резко сократилось число деревьев и камней. Всё потому что теперь шанс спавна распределяется и на цветы, которых я добавил столько же сколько и деревьев с камнями. Этот вопрос я пока что оставлю вам на растерзание.

Не буду мучить копи-пастами (мы же не только кодом занимались), а потому — вот сохранённый плейс:


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

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

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