ФЕДЕРАЛЬНОЕ АГЕНТСТВО ЖЕЛЕЗНОДОРОЖНОГО ТРАНСПОРТА Федеральное государственное бюджетное образовательное учреждение высшего образования «ИРКУТСКИЙ ГОСУДАРСТВЕННЫЙ УНИВЕРСИТЕТ ПУТЕЙ СООБЩЕНИЯ (ФГБОУ ВО ИрГУПС)» Факультет «Управление на транспорте и информационные технологии» Кафедра «Информационные системы и защита информации» Программный модуль процедурной генерации Курсовая работа КР.420000.09.03.02. -2024.ПЗ Выполнил студент гр. ИС 1-22-1 Батанов А.Д. Проверил Ст. преподаватель Вергасов А.С. Иркутск 2024 Содержание Содержание .................................................................................................... 1 1. Введение ................................................................................................ 3 2. Основная часть ..................................................................................... 3 2.1. Постановка задачи ............................................................................ 3 2.2. Программная реализация ................................................................. 6 2.3. Краткая характеристика инструментальной программной среды 6 2.4. Описание программных модулей .................................................... 6 2.4.1. Математический алгоритм процедурной генерации .................. 7 2.5. Инструкция пользователю ............................................................. 11 2.5.1. Взаимодействие с API программного модуля .......................... 12 2.6. Контрольный пример ...................................................................... 19 3. Заключение ......................................................................................... 21 1. Введение Проект курсовой работы представляет собой модуль для проекта, который является компьютерной игрой. Для достижения большей реиграбельности и интереса игрока к прохождению игр, разработчики используют разные инструменты. Потому как компьютерная игра, для которой разрабатывается проект данной курсовой работы, является игрой мир, которой полностью генерируется с нуля, а сложность состоит в том, что, интерес изучения и в целом получаемое удовольствие от игры зависит от используемых в проектке алгоритмов процедурной генерации таких миров. Способности таких генераторов создать по-настоящему интересный виртуадьный игровой мир. 2. Основная часть 2.1. Постановка задачи Для понимания сути и необходимости данного программного модуля необходимо описать что представляет из себя игра, частью которой является данный модуль. Игра разработана на движке Unity, и представляет собой 2D платформер с видом сбоку, мир данной игры состоит из блоков. Данный модуль в свою очередь реализует генерацию мира в этой игре. Терминология: Блок-элементарная часть мира. Чанк-совокупность блоков, шириной 16 и высотой 256 блоков. Unity-среда разработки в которой разрабатывается игра Задачи модуля: Генерация поверхности земли, с неровностями и холмами. Также размещать камень ниже поверхности земли, формируя таким образом слой земли и подземелья Генерация пещер, таких, которые связаны между собой, и которые дают игроку возможность передвигаться в нижней части мира Генерация деревьев. Генератор должен уметь размещать деревья разных видов и размеров на поверхности земли, в зависимости от текущего биома (совокупность параметров, таких как в толще имеют свои температура и влажность) Генерация подземных ископаемых. Размещение подземелий полезных ископаемых, которые собственные параметры генерации, такие как высота, с которой начинается генерация руды, кучность, количество руды в одном кучу. Генерация должна быть одинаковой при вводе одного ключа, который должен определять характер генерации, то есть один раз воспользовавшись одним ключом, следующий раз, получим точно такую же генерацию Модуль процедурной генерации должен быть модульным, то есть, при дальнейшей разработке, модуль мог быть расширен новыми возможностями Модуль должен иметь возможность интеграции с другими модулями проекта Время работы алгоритма процедурной генерации не должно превышать 100мс на один чанк Генерация должна быть однородной и бесшовной, то есть должна прослеживаться общая тенденция и закономерность в генерации мира на любом чанке Аспекты использования модуля: Все параметры процедурной генерации должны быть легко доступны из редактора Unity, позволяя при этом легко и эффективно подбирать наиболее оптимальные значения для генерации. Геймплейные аспекты модуля: Сгенерированная поверхность не должна создавать препятствия при передвижении по ней, также должны быть плоские пространства для размещения на них построек. Также желательно наличие открытых входов в подземелье. Сеть пещерных каналов должна быть связана, и образовывать единую структуру, по которой можно перемещаться. API программного модуля: ChunkGenerator – основной класс генератора, в котором происходит основная часть процедурной генерации GenerateChunk(BlocksCounter blocksCounter, ChunkComponent chunk, GenerationData genData, Vector2Int sizeChunk = default, int seed = 0) – Метод для непосредственно генерации, o blocksCounter – модуль для подсчета установленных блоков o chunk – компонент чанка в пределах которого происходит генерация o genData – класс, содержащий параметры для генерации o sizeChunk – размеры чанка o seed – ключ генерации GenerateBackBack (EcsPackedEntityWithWorld chunkEntity, GenerationData genData) – генерация задних блоков, для формирования наиболее сложного мира, он состоит из нескольких слоев (передний, фоновый, задний), данный метод создает фон камня в подземелье 2.2. Программная реализация 2.3. Краткая характеристика инструментальной программной среды Программный модуль разработан в программной среде JetBrains Rider. В свою очередь игра, для которой данный модуль был разработан, разрабатывается в программной среде Unity. Unity - кроссплатформенная среда разработки компьютерных игр, разработанная американской компанией Unity Technologies. Unity позволяет создавать приложения, работающие на более чем 25 различных платформах, включающих персональные компьютеры, игровые консоли, мобильные устройства, веб-приложения и другие. Релиз Unity состоялся в 2005 году. JetBrains разработки Rider — кроссплатформенная интегрированная обеспечения платформы .NET, Поддерживаются языки программного разрабатываемая компанией JetBrains. для среда программирования C#, VB.NET и F#. Проект анонсирован в январе 2015 года. В его основе лежит другой продукт JetBrains — ReSharper. Среда поддерживает платформы .NET Framework, .NET и Mono. Работает на операционных системах Windows, macOS, Linux. 2.4. Описание программных модулей ChunkGenerator основной программный модуль процедурной генерации, объединяет все элементы процедурной генерации и производит её. GenerationData – класс, содержащий в себе параметры генерации, такие как параметры шума, высоту начала генерации поверхности и конца генерации поверхности. MainNoiseData – класс, содержащий в себе параметры шума, такие как тип шума (OpenSimplex2, OpenSimplex2S, Cellular, Perlin, ValueCubic, Value), частота, минимальное значение, параметры фрактала. При помощи данного класса, можно тонко регулировать поведение и внешний вид получаемой генерации. Генерация мира происходит поэтапно: сперва генерация поверхности (также нижнего слоя), генерация пещер, генерация деревьев, генерация руд, генерация ограничителей. Для каждого этапа генерации необходимы параметры шума, за который отвечает класс MainNoiseData, в котором и есть данные параметры. То есть для каждого этапа генерации создается соответствующий класс данных шума. GenTerrain – модуль генерации поверхности и нижнего слоя. GenOres – модуль генерации полезных ископаемых в нижнем слое. 2.4.1. Математический алгоритм процедурной генерации При разработке программного модуля процедурной генерации использовался математический алгоритм генерации процедерной текстуры псевдослучайным методом, называемый шумом Перлина. Шум Перлина создан Кеном Перлином в 1983 году и впоследствии был назван в честь своего создателя. Удобство в использовании данного алгоритма состоит в том, что он генерирует бесшовную и однородную по структуре текстуру, переходы по которой сглаженные и близки к естественным, благодаря которому можно сгенерировать красивый ландшафт. При помощи данного алгоритма создаются такие визуальные эффекты, как дым, облака, туман, огонь и т. д. Он также очень часто используется как простая текстура, покрывающая трехмерную модель. Рисунок 1 Текстура, сгенерированная при помощи алгоритма Шума Перлина Потому как разработать свою программу реализующая математический алгоритм процедурной генерации является весьма сложной задачей, используется готовая библиотека для работы с Шумом Перлина. Fast Noise Lite – библиотека для работы с шумом Перлина, с открытым исходным кодом, также помимо шума Перлина в этой библиотеке реализованы и другие методы процедурной генерации, которые можно использовать в разных целях. Например, Cellular можно использовать для точечной генерации. Для понимания работы алгоритма, рассмотрим принцип его работы. Для создания шума необходимы две вещи, это функция шума и функция интерполяции. Функция шума это просто напросто генератор случайных чисел. На один аргумент, выдается всегда одно и тоже число. На данном графике показан пример функции шума, где случайное значение от 0 до 1 присваивается каждой точке на оси X. Рисунок 2 Функция шума Используя функцию интерполяции, мы можем преобразовать исходную функцию шума в непрерывную, сглаженную функцию. Рисунок 3 Интерполяция функции шума Прежде чем начать объяснение алгоритма, необходимо внести определения объясняющие элементы работы алгоритма. Это такие понятия как частота и амплитуда. Частоту находим как обратное от длины волны: 1 длина волны . Рисунок 4 График функции sin(x) Теперь, если взять много таких гладких функций, с различной частотой и амплитудой, и объединить в одну, можно получить шум. Это и есть функция шума Перлина. Рисунок 5 Множество функий с разным частотами и амплитудами Рисунок 6 Совмещение всех функций (Шум Перлина) Количество этих функций называется октававми, шум, составленный из большего числа октав будет иметь более неоднородную структуру. Изменяя следующие параметры Шума Перлина, можно достичь уникальных результатов в генерации ландшафта: Лакунарность (lacunarity) - контролирует изменение частоты. Постоянство (persistence) - контролирует изменение амплитуды. Октавы (octaves) – число функций шума 2.5. Инструкция пользователю Для получения более реалистичной и более интересного ландшафта, данный модуль имеет множество элементов графического взаимодействия, таким образом пользователь может эффективно и наглядно редактировать параметры процедурной генерации, получая результат в виде удобного графического представления его элементов. Для класса MainNoiseData реализовано следующая графическая оболочка (рис. 1), при редактировании параметров шума, изображение внизу, изменяется в зависимости данных параметров. В данном случае показан MainNoiseData для генератора пещер. Рисунок 7 Класс MainNoiseData для генератора пещер Рисунок 8 MainNoiseData для генератора руд 2.5.1. Взаимодействие с API программного модуля Прежде всего для того, чтобы воспользоваться возможностями процедурной генерации ChunkGenerator, и необходимо вызвать метод взаимодействовать GenerateChunk, с классом однако можно самостоятельно делать то, что, реализует метод GenerateChunk. Рассмотрим пример использования процедурной генерации при загрузке чанков. Метод LoadChunk, загружает соответствующий чанк, переданный в параметры данного метода. При загрузке чанка, необходимо проверить является ли данный игрок хостом (сервер-клиент), если да, то загружаем данные из сохранения, если нет, используем данные, переданные в аргументы метода. Если информация о чанке, пуста, это происходит, когда чанк еще не загружен, либо данные сохранения были удалены, именно в этот момент и происходит генерация чанка, в противном случае загружаем чанк. Перед вызовом метода генерации чанка, программист, работающий с данным программным модулем, должен приготовить необходимые для работы данные (такие как параметры генерации, текущий чанк и то прочее), и только потом происходит вызов метода генерации чанка. Потому как движок Unity не в полной мере поддерживает многопоточность, необходимо вызывать такие функции лишь в основном потоке, как например в методе LoadChunk есть строка: “_chunkVisibleManager.Clear(chunkPool.Get(chunkEntity));”, она заносится в список функций, которые вызываются позже всех в основном потоке. В методе LoadChunk объявляется переменная listActionsOnMainThread, которая в свою очередь и содержит те самые функции, вызываемые в основном потоке, и также LoadChunk возвращает данный список. В свою очередь класс ChunkGenerator, имеет функции, вызываемые лишь в основном потоке Unity, поэтому он также заносится в данный список. В данном проекте используется фреймворк ECS, компонентносущностная система, в которой сущностями являются чанки, а при работе с данным фреймворком, есть особенности при передачи таких сущностей, поэтому их запаковывают в EcsPackedEntityWithWorld. private Action LoadChunk(EcsPackedEntityWithWorld chunkPacked, LoadChunkEventComponent load, ChunkData chunkDataFromSave) { var listActionsOnMainThread = new Action(() => { }); if (!chunkPacked.Unpack(out var chunkWorld, out var id)) return listActionsOnMainThread; var chunkPool = chunkWorld.GetPool<ChunkComponent>(); ref var chunk = ref chunkPool.Get(id); if (chunk.ChunkGlobalPos == load.ChunkGlobalPos && load.Inclusion) return listActionsOnMainThread; listActionsOnMainThread += () => { if (!chunkPacked.Unpack(out _, out var chunkEntity)) _chunkVisibleManager.Clear(chunkPool.Get(chunkEntity)); }; chunk.ClearBlocks(); chunk.ChunkGlobalPos = load.ChunkGlobalPos; var chunkDataInfo load.ChunkData; = !load.IsClient ? chunkDataFromSave : var genData = _dataBaseManager.GetMainData(); _chunkGenerator.GenerateBackBack(chunkPacked, listActionsOnMainThread); genData,ref if (chunkDataInfo == null) { Debug.Log("ChunkData is null, generating chunk"); listActionsOnMainThread += _chunkGenerator.GenerateChunk( load.BlocksCounter, chunk, chunkPacked, genData, seed: _worldManager.WorldDataCurrent.Seed); } else LoadChunkFromSave(chunk); return listActionsOnMainThread; void LoadChunkFromSave(ChunkComponent chunkLocal) { var blocks = chunkDataInfo.Blocks; load.BlocksCounter.MaxCount = blocks.Count; foreach (var chunkBlock in blocks) { var blockData _dataBaseManager.GetBlock(chunkBlock.blockName); var block = chunkBlock; = listActionsOnMainThread += () => { //Debug.Log("SetBlockEvent"); _sharedData.EventsBus.NewEvent<BlockSetEvent>() = new BlockSetEvent(chunkPacked, blockData, block.pos, block.memories, chunkLocal.ChunkPosToGlobalPosVector2Int(block.pos), Vector2.one, block.blockLayer, false, onSuccessBlockEvent: () => { load.BlocksCounter.Counter++; }, onFailBlockEvent: () => { load.BlocksCounter.MaxCount--; Debug.Log("fail"); }); }; } } } 2.5.2. Использование компонентов модуля В следущем классе не происходит обращение к основному модулю ChunkGenerator, иначе, происходит использование отдельных классов программного модуля, отвечающих за отдельные этапы генерации чанков. GenerateTester используется для тестирования генерации, в нем можно не входя в игру, тестировать параметры генерации мира, и видеть графическое представление генерации мира, которые как раз таки представлены в контрольном примере. Суть генератора состоит в том, что он создает n-чанков, и генерирует массив блоков, из которых состоят данные чанки, затем беря средний цвет текстуры данных блоков строит текстуру, которая и отображается в редкторе Unity. GenerateTester затрагивает не все этапы генерации чанка, а лишь основные: var listGens = new List<GenBase> { new GenTerrain(genData.stoneNoise, genData.dirtNoise), new GenOres(genData.oresNoise), new GenCaves(genData.caveNoise), new GenTrees(genData.treesNoise), new GenLimiters() }; Следующая часть кода строит текстуру по сгенерированному массиву блоков. for (var i = 0; i < chunkW; i++) for (var j = 0; j < chunkH; j++) { if (chunkDataGen.ChunkFront[i, j] != null) Set(ColorS.GetAverageColorForSprite(chunkDataGen.ChunkFront[i, j].GetSprite())); void Set(Color color) { texture.SetPixel(pos * chunkW + i, j, color); } } [Bind] public class GenerateTester : MonoBehaviour { [SerializeField] private short startPosX; [SerializeField] [Range(1, 16)] private int chunksWidth; [SerializeField] private RawImage mapWorld; [SerializeField] private StandartGenerationData baseGenData; public bool randomSeed; public int seed; public GenerationData genData; public bool generateOnValidate; private void OnValidate() { #if UNITY_EDITOR if (generateOnValidate) Generate(); #endif } [ButtonMethod] public void Generate() { if (randomSeed) seed = Random.Range(0, 100000000); mapWorld.rectTransform.sizeDelta = new Vector2(chunksWidth * 16, ChunksConstants.ChunkHeight); var texture = new Texture2D(chunksWidth ChunksConstants.ChunkHeight) { filterMode = FilterMode.Point, anisoLevel = 0 }; * 16, var sizeChunk = ChunksConstants.ChunkSize; var chunkW = sizeChunk.x; var chunkH = sizeChunk.y; for (var i = 0; i < chunksWidth * 16; i++) for (var j = 0; j < ChunksConstants.ChunkHeight; j++) texture.SetPixel(i, j, Color.black); var sharedData = new SharedData() { EventsBus = new EventsBus() }; for (short pos = 0; pos < chunksWidth; pos++) { var ecsWorld = new EcsWorld(); var chunkEntity = ecsWorld.NewEntity(); var verChunks = new int[16]; for (byte j = 0; j < 16; j++) verChunks[j] = ecsWorld.NewEntity(); var chunkPool = ecsWorld.GetPool<ChunkComponent>(); var verChunkPool ecsWorld.GetPool<VerticalChunkComponent>(); ref var chunk = ref chunkPool.Add(chunkEntity); chunk.Set(ecsWorld.PackEntityWithWorld(chunkEntity), verChunks.Select(s=>ecsWorld.PackEntityWithWorld(s)).ToArray()); chunk.ChunkGlobalPos = (short)(startPosX + pos); for (byte j = 0; j < 16; j++) = { var entity = verChunks[j]; var min = new Vector2Int(0, j * 15); var max = min + new Vector2Int(16, 16); ref var verChunk = ref verChunkPool.Add(entity); verChunk.Set(new Leopotam.Group.Math.Bounds2i(min, max), j, ecsWorld.PackEntityWithWorld(entity), ecsWorld.PackEntityWithWorld(chunkEntity)); } var chunkDataGen = new ChunkDataGeneration { ChunkFront = new BlockData[chunkW, chunkH], ChunkBack = new BlockData[chunkW, chunkH] }; var listGens = new List<GenBase> { new GenTerrain(genData.stoneNoise, genData.dirtNoise), new GenOres(genData.oresNoise), new GenCaves(genData.caveNoise), new GenTrees(genData.treesNoise), new GenLimiters() }; var listActionOnMainThread = new List<Action>(); foreach (var listGen in listGens) listGen.Init(sizeChunk, genData, new BlocksCounter(), listActionOnMainThread, ecsWorld.PackEntityWithWorld(chunkEntity), ecsWorld, chunkDataGen, baseGenData, seed, pos, sharedData); foreach (var gen in listGens) gen.OnGeneration(); for (var i = 0; i < chunkW; i++) for (var j = 0; j < chunkH; j++) { if (chunkDataGen.ChunkFront[i, j] != null) Set(ColorS.GetAverageColorForSprite(chunkDataGen.ChunkFront[i, j].GetSprite())); void Set(Color color) { texture.SetPixel(pos * chunkW + i, j, color); } } } sharedData.EventsBus.Destroy(); texture.Apply(); mapWorld.texture = texture; } 2.6. Контрольный пример В данный момент в проекте используется параметры процедурной генерации, которые генерируют Рисунок 9 Пример генерации мира мир следующим образом: Рисунок 10 Пример генерации мира из окна игры Рисунок 11 Скриншот из самой игры Рисунок 12 Общая картина генерации мира 3. Заключение При помощи алгоритмов процедурной генерации можно создать интересный и реалистичный ландшафт, комбинируя различные методы генерации шума. 4. Приложение 4.1. Исходный код программы 5. ChunkGenerator using System; using System.Collections.Generic; using Game.Blocks.BlockData; using Game.Chunks.ChunksECS.Components; using Game.Chunks.ChunksECS.Components.Events; using Game.Chunks.Generation.Gen; using Game.Chunks.Other; using Game.ECS; using Leopotam.EcsLite; using SurvDI.Core.Common; using UnityEngine; namespace Game.Chunks.Generation { public class ChunkDataGeneration { public BlockData[,] ChunkBack; public BlockData[,] ChunkFront; } public class ChunkGenerator { [Inject] private StandartGenerationData _baseGenData; [Inject] private SharedData _sharedData; public Action GenerateChunk(BlocksCounter blocksCounter, ChunkComponent chunk, EcsPackedEntityWithWorld chunkPacked, GenerationData genData, Vector2Int sizeChunk = default, int seed = 0) { if (!chunkPacked.Unpack(out var world, out var chunkEntity)) { Debug.LogWarning("ChunkPack fail"); return () => {}; } //Debug.Log($"Seed {seed}"); var listActionOnMainThread = new List<Action>(); if (sizeChunk == default) sizeChunk = ChunksConstants.ChunkSize; var chunkW = sizeChunk.x; var chunkH = sizeChunk.y; var chunkDataGen = new ChunkDataGeneration { ChunkFront = new BlockData[chunkW, chunkH], ChunkBack = new BlockData[chunkW, chunkH] }; var listGens = new List<GenBase> { new GenTerrain(genData.stoneNoise, genData.dirtNoise), new CopyFrontToBack(), new GenCaves(genData.caveNoise), new GenTrees(genData.treesNoise), new GenOres(genData.oresNoise), new GenLimiters() }; foreach (var listGen in listGens) listGen.Init(sizeChunk, genData, blocksCounter, listActionOnMainThread, chunkPacked, world, chunkDataGen, _baseGenData, seed, chunk.ChunkGlobalPos, _sharedData); foreach (var gen in listGens) gen.OnGeneration(); //Debug.Log($"GeneratingChunk: {chunk.ChunkGlobalPos}"); #region BuildChunk var setBlockPos = Vector2Int.zero; for (var i = 0; i < chunkW; i++) for (var j = 0; j < chunkH; j++) { setBlockPos.x = i; setBlockPos.y = j; var block = chunkDataGen.ChunkFront[i, j]; var blockBack = chunkDataGen.ChunkBack[i, j]; if (block != null) BlockSet(block, setBlockPos); if (blockBack != null) BlockSet(blockBack, setBlockPos, BlockLayer.Back); } void BlockSet(BlockData blockData, Vector2Int pos, BlockLayer blockLayer = BlockLayer.Front) { blocksCounter.MaxCount++; listActionOnMainThread.Add(() => { //Debug.Log("SetBlockInGen"); _sharedData.EventsBus.NewEvent<BlockSetEvent>() = new BlockSetEvent(chunkPacked, blockData, pos, null, chunk.ChunkPosToGlobalPosVector2Int(setBlockPos), Vector2.one, blockLayer, onSuccessBlockEvent: () => { blocksCounter.Counter++; }, onFailBlockEvent: () => { blocksCounter.MaxCount--; }); }); } #endregion var action = new Action(() => { }); foreach (var action1 in listActionOnMainThread) action += action1; return action; } public void GenerateBackBack(EcsPackedEntityWithWorld chunkEntity, GenerationData genData, ref Action actionsOnMainThread) { if (!chunkEntity.Unpack(out var world, out var id)) { Debug.LogWarning("Error, unpacking chunk"); return; } var chunkPool = world.GetPool<ChunkComponent>(); var size = ChunksConstants.ChunkSize; var stone = _baseGenData.stone; var chunk = chunkPool.Get(id); //Debug.Log("GeneratingBackBack"); //Генерация фонового камня var generationStoneBackBack = genData.minSurfaceHeight; //Высота от вершины мира до уровня камней for (var i = 0; i < size.x; i++) for (var j = 0; j < generationStoneBackBack; j++) { var pos = new Vector2Int(i, j); actionsOnMainThread += () => { //Debug.Log("SetBackBack"); _sharedData.EventsBus.NewEvent<BlockSetEvent>() = new BlockSetEvent(chunkEntity, stone, pos, null, chunk.ChunkPosToGlobalPosVector2Int(pos), Vector2.one, BlockLayer.BackBack); }; } } private static void InitChunkSize(ref Vector2Int size) { if (size == default) size = ChunksConstants.ChunkSize; } } } 6. CopyFrontToBack namespace Game.Chunks.Generation.Gen { public class CopyFrontToBack : GenBase { public override void OnGeneration() { for (var i = 0; i < ChunkW; i++) for (var j = 0; j < ChunkH; j++) ChunkDataGeneration.ChunkBack[i, ChunkDataGeneration.ChunkFront[i, j]; } } } j] = 7. GenBase namespace Game.Chunks.Generation.Gen { public abstract class GenBase { private Vector2Int SizeChunk { get; set; } private BlocksCounter BlocksCounter { get; set; } private List<Action> ListActionsOnMainThread { get; set; } private EcsPackedEntityWithWorld ChunkEntity { get; set; } private EcsWorld EcsWorld { get; set; } protected ChunkDataGeneration ChunkDataGeneration { get; set; } protected int ChunkPos { get; set; } protected Noise Noise { get; private set; } protected int Seed { get; private set; } protected ChunkComponent Chunk { get { if (ChunkEntity.Unpack(out var world, out var entity)) return EcsWorld.GetPool<ChunkComponent>().Get(entity); return default; } } protected int ChunkW => SizeChunk.x; protected int ChunkH => SizeChunk.y; protected GenerationData GenData { get; private set; } protected StandartGenerationData BaseGenData { get; private set; } protected SharedData SharedData { get; private set; } public void Init(Vector2Int size, GenerationData generationData, BlocksCounter blocksCounter, List<Action> actions, EcsPackedEntityWithWorld chunkEntity, EcsWorld ecsWorld, ChunkDataGeneration chunkDataGeneration, StandartGenerationData baseGenData, int seed, int chunkPos, SharedData sharedData) { SizeChunk = size; GenData = generationData; BlocksCounter = blocksCounter; ListActionsOnMainThread = actions; ChunkEntity = chunkEntity; EcsWorld = ecsWorld; ChunkDataGeneration = chunkDataGeneration; Seed = seed; ChunkPos = chunkPos; BaseGenData = baseGenData; SharedData = sharedData; if (Noise != null) { Noise.XOffset = ChunkPos * ChunkW; Noise.SetSeed(seed); } OnInit(); } public abstract void OnGeneration(); protected virtual void OnInit() { } protected void SetFront(int x, int y, BlockData blockData) { ChunkDataGeneration.ChunkFront[x, y] = blockData; } protected void SetBlock(Vector2Int pos, BlockData blockData) { BlocksCounter.MaxCount++; ListActionsOnMainThread.Add(() => { SharedData.EventsBus.NewEvent<BlockSetEvent>() = new BlockSetEvent(ChunkEntity, blockData, pos, null, Chunk.ChunkPosToGlobalPosVector2Int(pos), Vector2.one, BlockLayer.Front, false, checkCanSet: true, onSuccessBlockEvent: () => { BlocksCounter.Counter++; }, onFailBlockEvent: () => { BlocksCounter.MaxCount--; }); }); } protected void UpdateMapHeight(BlockLayer blockLayer = BlockLayer.Front) { if (ChunkEntity.Unpack(out var www, out var i)) { var chunkPool = EcsWorld.GetPool<ChunkComponent>(); ref var chunk = ref chunkPool.Get(i); chunk.GenerateMapHeight(blockLayer == BlockLayer.Front ? ChunkDataGeneration.ChunkFront : ChunkDataGeneration.ChunkBack); } } protected void InitNoise(MainNoiseData noiseData) { Noise = new Noise(); NoiseInit(Noise, noiseData); } protected noiseData) { Noise NoiseInit(Noise noise, MainNoiseData return noise.Init(Seed, noiseData, ChunkW * ChunkPos); } } } 8. GenCaves using Game.Chunks.Generation.NoiseInfo; namespace Game.Chunks.Generation.Gen { public class GenCaves : GenBase { public GenCaves(MainNoiseData noiseData) { InitNoise(noiseData); } public override void OnGeneration() { for (var x = 0; x < ChunkW; x++) for (var y = 0; y < GenData.minSurfaceHeight; y++) if (Noise.GetNoiseBool(x,y)) SetFront(x, y, null); } } } 9. GenLimiters namespace Game.Chunks.Generation.Gen { public class GenLimiters : GenBase { public override void OnGeneration() { var limiter = BaseGenData.limiter; for (var i = 0; i < ChunkW; i++) { SetFront(i, 0, limiter); SetFront(i, ChunkH - 1, limiter); } } } } 10.GenOres using Game.Blocks.BlockData; using Game.Chunks.BlocksECS.Components.Blocks; using Game.Chunks.Generation.NoiseInfo; namespace Game.Chunks.Generation.Gen { public class GenOres : GenBase { public GenOres(MainNoiseData noiseData) { InitNoise(noiseData); } public override void OnGeneration() { //Генерация руд foreach (var blockData in BaseGenData.ores) foreach (var data in blockData.Components) if (data is OreBlock oreData) Generate(blockData, oreData.startGenerationOre); void Generate(BlockData blockData, int endHeight) { Noise.GetNoise.mSeed += 1; for (var x = 0; x < ChunkW; x++) for (var y = 0; y < endHeight; y++) if (Noise.GetNoiseBool(x, y)) SetFront(x, y, blockData); } } } } 11.GenTerrain using Game.Chunks.Generation.NoiseInfo; using UnityEngine; namespace Game.Chunks.Generation.Gen { public class GenTerrain : GenBase { private readonly MainNoiseData _dirtNoiseData; private readonly MainNoiseData _stoneNoiseData; private Noise _noiseDirt; private Noise _noiseStone; public GenTerrain(MainNoiseData stoneNoise, MainNoiseData dirtNoise) { _stoneNoiseData = stoneNoise; _dirtNoiseData = dirtNoise; } protected override void OnInit() { _noiseDirt = NoiseInit(new Noise(), _dirtNoiseData); _noiseStone = NoiseInit(new Noise(), _stoneNoiseData); } public override void OnGeneration() { var height = GenData.maxSurfaceHeight GenData.minSurfaceHeight; for (var x = 0; x < ChunkW; x++) { var noiseDirt = _noiseDirt.GetNoiseRaw(x); var noiseStone = _noiseStone.GetNoiseRaw(x); - if (noiseDirt < 0f) noiseDirt = Mathf.Abs(noiseDirt); if (noiseStone < 0f) noiseStone = Mathf.Abs(noiseStone); var min = GenData.minSurfaceHeight; var max = min; min -= (int)(noiseStone * height); max += (int)(noiseDirt * height); for (var y = 0; y < min; y++) SetFront(x, Mathf.Clamp(y, BaseGenData.stone); for (var y = min; y < max; y++) SetFront(x, Mathf.Clamp(y, BaseGenData.mainDirt); } } } } 12.GenTrees 0, ChunkH - 1), 0, ChunkH - 1), using System.Collections.Generic; using Game.Blocks; using Game.Chunks.Generation.NoiseInfo; using UnityEngine; namespace Game.Chunks.Generation.Gen { public class GenTrees : GenBase { private readonly Noise _trees; private readonly Noise _treesHeights; public GenTrees(MainNoiseData treesNoise) { _trees = NoiseInit(new Noise(), treesNoise); _treesHeights = NoiseInit(new Noise(), treesNoise); InitNoise(treesNoise); } public override void OnGeneration() { UpdateMapHeight(); var mapHeight = Chunk.MapHeight;//Карта высот var listCanCreate = new List<(byte x, byte y)>(); for (var x = 0; x < mapHeight.Length; x++) listCanCreate.Add(((byte) x, mapHeight[x])); for (var i = 0; i < listCanCreate.Count; i ++) { var (x, y) = listCanCreate[i]; //SetBlock(new Vector2Int(x, y+1), BaseGenData.grass); var value = _trees.GetNoiseBool(i); if (value) CreateTree(x, y, GenData.treesData); else if (y + 1 < ChunkH && x < ChunkW) SetFront(x,y+1, BaseGenData.grass); } void CreateTree(int x, int y, TreeData tree) { var startPos = new Vector2Int(x, y); var posLog = startPos; var treeHeidth (int)_treesHeights.GetNoiseClamped(x,0,4,6); for (var i = 0; i < treeHeidth; i++) { posLog.y++; SetBlockLog(posLog, tree); } treeHeidth += y; SetBlockLeaves(x - 1, treeHeidth, tree); SetBlockLeaves(x + 1, treeHeidth, tree); SetBlockLeaves(x, treeHeidth + 1, tree); SetBlockLeaves(x - 1, treeHeidth + 1, tree); SetBlockLeaves(x + 1, treeHeidth + 1, tree); SetBlockLeaves(x - 2, treeHeidth + 1, tree); SetBlockLeaves(x + 2, treeHeidth + 1, tree); SetBlockLeaves(x, treeHeidth + 2, tree); SetBlockLeaves(x - 1, treeHeidth + 2, tree); SetBlockLeaves(x + 1, treeHeidth + 2, tree); SetBlockLeaves(x - 2, treeHeidth + 2, tree); SetBlockLeaves(x + 2, treeHeidth + 2, tree); SetBlockLeaves(x, treeHeidth + 3, tree); SetBlockLeaves(x - 1, treeHeidth + 3, tree); SetBlockLeaves(x + 1, treeHeidth + 3, tree); } void SetBlockLog(Vector2Int pos, TreeData tree) { SetBlock(pos, tree.log); } void SetBlockLeaves(int x, int y, TreeData tree) { SetBlock(new Vector2Int(x, y), tree.leaves); } } } } 13.BiomBase = using System; using Game.Blocks; using Game.Blocks.BlockData; namespace Game.Chunks.Generation.Biomes { [Serializable] public abstract class BiomBase { public string biomeName; public TreeData biomeTree; public BlockData dirt; } } 14.GenerationData using System; using Game.Blocks; using Game.Chunks.Generation.NoiseInfo; using UnityEngine; namespace Game.Chunks.Generation { [Serializable] [CreateAssetMenu(fileName = "GenData", "Data/GenData", order = 0)] public class GenerationData : ScriptableObject { public TreeData treesData; menuName = [Header("World Size Settings")] public int maxSurfaceHeight = 48; public int minSurfaceHeight = 16; public MainNoiseData caveNoise; public MainNoiseData oresNoise; public MainNoiseData stoneNoise; public MainNoiseData dirtNoise; public MainNoiseData treesNoise; } } 15.Noise using Game.Chunks.Generation.NoiseInfo; using Plugins.FastNoise; using UnityEngine; namespace Game.Chunks.Generation { public class Noise { private FastNoiseLite _noise; private MainNoiseData _noiseData; public int XOffset { get; set; } public FastNoiseLite GetNoise => _noise; public Noise Init(int seed, MainNoiseData noiseData, int xOffset) { _noise = noiseData.GetNoise(seed); _noiseData = noiseData; XOffset = xOffset; return this; } public void SetSeed(int seed) { _noise.SetSeed(seed); } public float GetNoiseRaw(int x, int y = 0) { return _noise.GetNoise(x + XOffset, y); } public float GetNoiseApprox(int x, int y = 0) { var v = GetNoiseRaw(x, y); if (v > 0) v += 1; else if (v < 0) v = Mathf.Abs(v); v /= 2; return v; } public float GetNoiseClamped(int x, int y, int startHeight, int endHeight) { var range = endHeight - startHeight; var value = GetNoiseRaw(x); return Mathf.Clamp(startHeight + range * value, startHeight, y > 0 ? y : endHeight); } public bool GetNoiseBool(int x, int y = 0) { if (_noiseData.invert) return GetNoiseApprox(x, y) < _noiseData.thresold; return GetNoiseApprox(x, y) > _noiseData.thresold; } } } 16.StandartGenerationData using Game.Blocks.BlockData; using UnityEngine; namespace Game.Chunks.Generation { [CreateAssetMenu(fileName = "GenData", menuName "Data/StandartGenData", order = 0)] public class StandartGenerationData : ScriptableObject { public BlockData mainDirt; public BlockData stone; public BlockData[] ores; public BlockData grass; public BlockData limiter; } } 17.CellularData using System; using Plugins.FastNoise; using UnityEngine; namespace Game.Chunks.Generation.NoiseInfo { [Serializable] public class CellularData { [Header("Cellular"), Range(0f,1f), Delayed] cellularJitter; public = float public FastNoiseLite.CellularDistanceFunction cellularDistanceFunction = FastNoiseLite.CellularDistanceFunction.EuclideanSq; public FastNoiseLite.CellularReturnType cellularReturnType = FastNoiseLite.CellularReturnType.Distance; } } 18.MainNoiseData using InspectorUtils; using Plugins.FastNoise; using UnityEngine; namespace Game.Chunks.Generation.NoiseInfo { [CreateAssetMenu(fileName = "MainNoiseData", menuName = "Data/Noise/Noise", order = 0)] public class MainNoiseData : ScriptableObject { public FastNoiseLite.NoiseType noiseType; public FastNoiseLite.RotationType3D rotationType3D = FastNoiseLite.RotationType3D.None; [Delayed] public float frequency = 0.2f; [Range(0f,1f), Delayed] public float thresold; public bool invert; public bool isHeight; public int height; public bool toApprox = true; public float scalePreview = 1f; public NoiseFractalData fractalData; [ShowIf(nameof(IsCellular))] public CellularData cellularData; public bool isBool; private bool IsCellular FastNoiseLite.NoiseType.Cellular; => public FastNoiseLite GetNoise(int seed) { var noise = new FastNoiseLite(); noise.SetNoiseType(noiseType); noise.SetFrequency(frequency); noiseType == noise.SetRotationType3D(rotationType3D); noise.SetFractalPingPongStrength(fractalData.pingPongStrength); noise.SetFractalWeightedStrength(fractalData.weightedStrength); noise.SetFractalType(fractalData.fractalType); noise.SetFractalOctaves(fractalData.octaves); noise.SetFractalLacunarity(fractalData.lacunarity); noise.SetFractalGain(fractalData.gain); if (cellularData != null && noiseType FastNoiseLite.NoiseType.Cellular) { noise.SetCellularJitter(cellularData.cellularJitter); == noise.SetCellularDistanceFunction(cellularData.cellularDistanceFunct ion); noise.SetCellularReturnType(cellularData.cellularReturnType); } noise.SetSeed(seed); return noise; } } } 19.NoiseFractalData using System; using Plugins.FastNoise; using UnityEngine; namespace Game.Chunks.Generation.NoiseInfo { [Serializable] public class NoiseFractalData { public FastNoiseLite.FractalType fractalType; [Range(1, 8), Delayed] public int octaves = 2; [Range(0.5f, 8f), Delayed] public float lacunarity = 2f; [Range(0.005f, 4f), Delayed] public float gain = 0.2f; [Range(0f, 1f), Delayed] public float weightedStrength; [Range(0f, 1f), Delayed] public float pingPongStrength; } }