Генерация мира как в майнкрафт unity

Обновлено: 07.07.2024

Мы начинаем серию уроков, ориентированную на то, чтобы научить вас создавать простую Minecraft-подобную игру, а также изучить различные аспекты движка Unity3D. Так как это вводный урок, алгоритмы и структура объектов, представленные в этой серии, не самые эффективные.

Приступаем к разработке

Скачайте последнюю версию Unity3D отсюда.

Скачайте текстуры, которые вам потребуются в процессе разработки проекта, описанного в этом руководстве.

Вы можете использовать любое из предложенных разрешений. Вы также можете скачать оригинал:

Для начала давайте познакомимся с Unity3D. Когда Вы запустите Unity3D в первый раз, всплывет окно Project Wizard. Вы можете импортировать один из встроенных пакетов Unity. Пакеты — это коллекции различных файлов (кода, моделей, аудио-файлов, текстур и т.д.), которые хранятся в виде иерархической структуры, инкапсулированной в файлы с расширением .unitypackage. Пакеты могут быть экспортированы из любого Unity-проекта. Таким образом можно очень просто переносить различные файлы между проектами, сохраняя их иерархию. Сейчас нам не нужно импортировать какие-либо пакеты.

1_project_wizard

Окно Unity Project Wizard

После того, как вы зададите путь для нового проекта, нажмите кнопку Create, чтобы завершить создание проекта. Если Вы открыли Unity и создали проект заблаговременно, вы всегда можете создать новый проект, нажав File → New Project, чтобы вызвать окно Project Wizard.

2_file_newproj

Создание нового проекта Unity

Интерфейс Unity разделен на несколько вкладок:

Вы можете расположить вкладки, как вам удобно, перетащив их мышкой в нужное место.

3_fullwindow

Любой объект или скрипт, добавленный в проект, может быть сохранен в файле сцены с расширением .unity. Сцены идентичны игровым уровням. Unity-разработчик может разместить игровые файлы на отдельную сцену, когда это необходимо, и загрузить их во время выполения кода. Любой проект может содержать несколько сцен. Чтобы сохранить текущую сцену, нажмите File → Save Scene / Save Scene as… и наберите название в окне проводника.

4_savescene

Сохраните ее в папке Assets — корневой папке Unity-проекта.

5_savescene2

Если вы откроете папку Assets во вкладке Project, вы можете обнаружить там только что созданную сцену. Кликните здесь правой клавишей мыши и создайте три новых папки: Code, Materials и Textures, как показано на картинке:

6_createfolder

Создание новой папки

7_folders

Теперь мы готовы начать! Перетащите текстуры куба и скайбокса в папку Textures.

8_textures

Импортированные в проект текстуры куба и скайбокса

Затем зайдите в папку Materials и создайте четыре материала:

Материалы добавляют цвета на 3D-объекты с помощью программ, называемых шейдерами и обрабатываемых на GPU. Больше информации о материалах Unity и шейдерах вы можете получить здесь. Три материала, которые мы создали, будут применены к сторонам куба, который мы создадим в следующем разделе.

9_createmat

Создание нового материала

10_materials

Материалы для скайбокса и сторон куба

Кликните левой кнопкой мыши на BottomMaterial. Во вкладке Inspector кликните по кнопке Select, расположенной в компоненте Texture материала, а затем, во всплывшем окне, выберите текстуру bottom.

11_material_no_texture

Обззор материала во вкладке Inspector

12_picktex

Выбор компонента текстуры

Затем выберите соответствующие текстуры для SideMaterial и TopMaterial, как показано на картинке ниже.

13_voxelmaterials

Материалы куба с загруженными текстурами

Если вы хотите, чтобы на заднем плане отображался красивый пейзаж, вы можете добавить скайбокс. Для этих целей мы создали SkyboxMaterial, на который мы наложим шесть оставшихся текстур из папки Textures.

Нажмите левой кнопкой мыши на SkyboxMaterial. Во вкладке Inspector, рядом с меткой Shader, кликните на выпадающий список и выберите RenderFX → Skybox. Это встроенные в Unity шейдеры, которые имплементируют базовые (модель освещения Блинна-Фонга, рельефное текстурирование, отражения, прозрачность и т.д.) и несколько продвинутых шейдеров, таких как параллакс-эффект. Вы также можете писать свои шейдеры и добавлять их в проект.

14_skyboxmat

Выбор шейдера для отрисовки скайбокса

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

15_skyboxtexturesmat2

Выбор подходящих текстур скайбокса

Далее, мы должны добавить скайбокс на нашу сцену. Перейдите в Edit → Render Settings. Во вкладке Inspector, рядом с меткой Skybox Material, нажмите на маленький кружок справа и выберите SkyboxMaterial из материалов проекта.

16_renderskybox21

Выбор материала скайбокса в RenderSettings

Если мы приглядимся, мы можем обнаружить грани skybox-куба. Этого можно избежать с помощью наложения текстур. Когда текстура наложена на другую, пиксели, расположенные на краях текстуры, повторяются, заполняясь цветом краев модели, на которой они расположены. Больше информации о наложении текстур можно получить здесь.

17_unclamp

Типичное поведение неналоженных текстур

Перейдите в папку Textures во вкладке Project, выберите все изображения, в пункте Wrap Mode выберите Clamp из выпадающего списка и нажмите Apply.

18_clamp

Установка Wrap Mode для всех текстур проекта

Создаем куб

Было бы очень заманчиво использовать встроенный примитив Unity — куб — как основу для кубов Minecraft, и расположить соответствующие текстуры из текстурного атласа на стороны куба, используя UV-преобразования, но в этом руководстве мы будем придерживаться простых методик (с наименьшим количеством внешних ресурсов) и будем использовать отдельные меши для каждой стороны.

Интенсив «Чат-бот с искусственным интеллектом на Python»

11–13 октября, Онлайн, Беcплатно

В верхнем левом меню кликните на GameObject → Create Other → Quad. Повторите это действие еще пять раз (нам нужно создать шесть сторон куба).

19_quads

Создание граней куба

Теперь назовите каждую из шести сторон соответствующим именем:

Top, Bottom, Right, Left, Front, Back.

Объекты, расположенные на сцене, называются GameObject. Чтобы переименовать GameObject, кликните правой клавишей мыши на нем во вкладке Hierarchy и нажмите Rename.

20_rename

Переименование граней куба

Если вы только начинаете знакомиться с Unity, вам крайне рекомендуется ознакомиться с навигацией в окне Scene и позиционированием GameObject, прежде чем идти дальше.

После создания игровые объекты будут размещены на сцене случайным образом (на самом деле, новые GameObject расположены в точке текущего расположения камеры). Мы должны расположить все стороны куба. Чтобы выровнять их, во вкладке Hierarchy кликните на каждую сторону и модифицируйте её позицию и вращение во вкладке Inspector таким образом:

22_pos_rot_faces

Преобразование значений для каждой грани

Вуаля! Наш серый куб готов:

21_faces

Обычный серый куб

Если куб не центрирован в окне Scene, дважды кликните на одной из его сторон во вкладке Hierarchy, чтобы выровнять камеру.

Во вкладке Project зайдите в папку Materials. Чтобы создать красивый пиксельный куб, мы должны переместить следующие материалы:

  • TopMaterial на верхнюю сторону,
  • BottomMaterial на нижнюю сторону,
  • SideMaterial на левую, правую, заднюю и переднюю сторону во вкладке Hierarchy.

23_dragdrop_mouse

24_voxel

Куб с текстурами

Замечательно! Выглядит, как куб из Minecraft, но сейчас у нас есть шесть разделенных частей, а не автономный GameObject, который мы могли бы разместить на нашей сцене. Мы будем использовать простую систему иерархий Unity, чтобы переместить эти части в один GameObject. Она позволяет любому GameObject стать потомком другого GameObject на сцене с помощью простого перетаскивания объекта-потомка на желаемый объект-родитель. Это чрезвычайно удобно, потому что Transform потомка (позиция, вращение и масштаб объекта) станет относительным родительскому объекту.

В левом верхнем меню выберите Game Object → Create Empty. Это действие создаст пустой GameObject, который будет содержать только компонент Transform.

25_createempty

Создание пустого GameObject

Кликните правой кнопкой на объекте и переименуйте его:

26_voxel

Переименование пустого GameObject

Кликните левой кнопкой на объекте и измените его позицию на (0,0,0).

27_position000

GameObject размещён в центре сцены

Теперь выберите шесть сторон куба и перетащите их в новый пустой GameObject.

28_finalvoxel_mouse

Если вы обнаружили ошибки, как на картинке ниже, учтите, что это обычное явление, когда вы вручную меняете иерархию GameObject. Просто нажмите Clear on Play во вкладке Console, чтобы очистить лог ошибок, когда запускаете игру.

part2_feedback1

Куб готов! Не забудте сохранить сцену!

32_transforms

В следующей части мы завершим создание нашей игры.

Хинт для программистов: если зарегистрируетесь на соревнования Huawei Cup, то бесплатно получите доступ к онлайн-школе для участников. Можно прокачаться по разным навыкам и выиграть призы в самом соревновании.

Перейти к регистрации

Создание Minecraft на Unity3D. Часть вторая. Генерация мира

Это вторая часть руководства по созданию собственной Minecraft-подобной игры. В ней мы напишем генератор мира и добавим персонажа, который сможет перемещаться по миру, ставить и удалять блоки. В предыдущей части мы создали куб с текстурой.

Добавляем функциональность мыши

Прежде чем мы приступим к программированию, давайте добавим на нашу сцену направленный свет. Нам нужен источник света, чтобы лучше видеть наш 3D-мир.

35_light

Вы можете поменять направление света, если хотите:

scene-light2

Позиционируем источник света

33_newscript

Мы назовем их WorldGenerator и ClickOnFaceScript.

34_scripts

Прежде чем мы продолжим, вам крайне рекомендуется ознакомиться со скриптингом в Unity.

Теперь откройте WorldGenerator.cs в MonoDevelop (которая уже установлена вместе с Unity) двойным щелчком по нему и введите следующий код:

Мы используем статический метод, чтобы создать клон нашего куба. Unity позволяет нам задать имя и позицию клона. Больше информации о методе Instantiate вы можете получить здесь.

В Minecraft, если вы находитесь достаточно близко к кубу и кликаете по нему левой кнопкой мыши, он исчезает и появляется в вашем инвентаре. Когда вы кликаете правой клавишей на любую из сторон куба, новый экземпляр появится на той стороне, по которой вы кликнули, в случае, если у вас достаточно материала в инвентаре. В данном уроке мы не будем ставить перед собой ограничения, так что у нас будет бесконечное количество кубов и бесконечная дистанция клика.

Интенсив «Чат-бот с искусственным интеллектом на Python»

11–13 октября, Онлайн, Беcплатно

Откройте ClickOnFaceScript.cs и введите туда этот код:

Теперь переместите скрипт на каждую из шести сторон куба на сцене.

36_dragscript2

Применяем скрипт к GameObject

Скрипт должен появиться на каждой стороне во вкладке Inspector.

37_inspector_script1

39_test

Тестируем нажатия кнопок мыши

Запомните! В режиме игры любые изменения, которые вы произвели с элементами во вкладке Scene, будут отменены. Не меняйте ничего, пока игра запущена. Нажмите еще раз, чтобы остановить игру.

Теперь у нас есть все что нужно, чтобы создавать новые кубы по клику правой клавишей мыши. Мы должны определить точную позицию нового GameObject, который будет появляться при клике. На нашей сцене расстояние между центрами двух соседних блоков равно единице.

Для простоты мы назовем центр куба, по которому кликают, буквой C, а центр блока, который должен появиться — N. Мы рассматриваем эти центры как позиции в 3D-пространстве.

  • Если мы кликаем на верхнюю грань: N = C + (0, 1, 0);
  • На нижнюю: N = C + (0, -1, 0);
  • На правую: N = C + (1, 0, 0);
  • На левую: N = C + (-1, 0, 0);
  • На переднюю: N = C + (0, 0, 1);
  • На заднюю: N = C + (0, 0, -1).

Мы можем обобщить сказанное выше в простую формулу: N = C + delta, где delta — это смещение, требуемое для расчета центра нового блока. Каждая из шести сторон содержит свой экземпляр ClickOnFaceScript и разное значение delta.

Мы должны изменить ClickOnFaceScript.cs, чтобы реализовать функционал, описанный выше. Откройте скрипт и измените файл таким образом:

Вернитесь в редактор и поменяйте значения delta в соответствии с картинками:

40_values

Вся необходимая информация обведена

Проверьте, что все работает. Запустите игру несколько раз, задавая разные позиции камере (изменяя ее Transform во вкладке Inspector), чтобы проверить, что введенные нами значения delta верны.

41_tempcam

Настраиваем позицию камеры и нажимаем на стороны кубов

Создаём персонажа

Если все работает, как задумано, мы можем перейти к созданию персонажа, чтобы мы могли свободно двигаться в нашей игре. К счастью для нас, Unity предоставляет готовый пакет с контроллером персонажа от первого лица, так что нам не нужно будет создавать его с нуля. Перейдите в Assets → Import Package и выберите Character Controller.

42_addcc

Импортируем пакет Character Controller

В окне Importing package выберите следующее:

43_importcc

Во вкладке Project перейдите в Standard Assets → Character Controllers, выберите First Person Controller.prefab и перетащите его во вкладку Hierarchy.

44_firstpersoncontroller

Заготовка First Person Character Controller

Расположите его близко к центру сцены.

45_positioncc

Настраиваем местоположение заготовки

Мы должны защитить нашего персонажа от падения, прежде чем перейти к тестированию. Мы вернем значение обратно, когда закончим алгоритм генерации мира.

46_gravityoff

Гравитация не нужна!

Проверьте, что все работает.

47_test

48_disablecamera

Отключаем главную камеру

Мы почти закончили! Откройте скрипт WorldGenerator.cs и модифицируйте его:

Скрипт будет запускаться только тогда, когда он прикреплен к какому-нибудь GameObject на сцене. Создайте пустой Empty GameObject и перетащите WorldGenerator.cs на него.

49_dragndrop

Перетаскиваем WorldGenerator.cs на новый GameObject

Перетащите объект Voxel на соответствующее поле в скрипте. Эта версия алгоритма генерации мира хранит все блоки в памяти, так что не рекомендуется задавать большие значения полям Size X, Size Y и Size Z, иначе вам грозит низкая производительность или, что еще хуже, Unity может вылететь.

50_values

Размеры больше указанных выставлять не стоит

51_cc_valback

И всё-таки гравитация важна

Готово! Нажмите и веселитесь!

52_final

Хинт для программистов: если зарегистрируетесь на соревнования Huawei Cup, то бесплатно получите доступ к онлайн-школе для участников. Можно прокачаться по разным навыкам и выиграть призы в самом соревновании.

Перейти к регистрации

image

В этих обучающих статьях мы создадим процедурно генерируемые карты, похожие на такие:

image

  • тепловая карта (левая верхняя)
  • карта высот (правая верхняя)
  • карта влажности (правая нижняя)
  • карта биомов (левая нижняя)

Генерирование шума

В Интернете есть множество различных генераторов шума, большинство из них имеют открытые исходники, поэтому здесь не нужно изобретать велосипед. Я позаимствовал портированную версию библиотеки Accidental Noise.

Для правильной работы в Unity в портированную версию внесены незначительные изменения.

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

Начало работы

Сначала нам необходимо создать контейнер для хранения данных, которые мы будем генерировать.

Начнем с создания класса MapData. Переменные Min и Max нужны для отслеживания нижнего и верхнего пределов генерируемых значений.


Также мы создадим класс Tile, который будет позже использоваться для создания игровых объектов Unity из генерируемых данных.


Чтобы посмотреть, что происходит, нам необходимо графическое представление данных. Для этого мы создадим новый класс TextureGenerator.

Пока этот класс будет создавать черно-белое отображение наших данных.


Скоро мы расширим этот класс.

Генерирование карты высот

Я решил, что карты будут фиксированного размера, поэтому нужно указать ширину (Width) и высоту (Height) карты. Также нам понадобятся настраиваемые параметры для генератора шума.

Мы сделаем эти данные отображаемыми в Unity Inspector, чтобы настройка карт была намного проще.

Класс Generator инициализирует модуль Noise, генерирует данные карты высот, создает массив тайлов, а затем генерирует текстурное представление этих данных.

Вот код с комментариями:


После запуска кода мы получим следующую текстуру:

image

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

Теперь нам нужно придать значимости нашим данным. Например, пусть все, что меньше 0,4, будет считаться водой. Мы можем изменить следующее в нашем TextureGenerator, назначив все значения ниже 0,4 синими, а выше — белыми:


После этого мы получил следующее конечное изображение:

image

У нас уже что-то получается. Появляются фигуры, соответствующие этому простому правилу. Давайте сделаем следующий шаг.

Добавим других настраиваемых переменных в наш класс Generator. Они будут указывать на параметры, с которыми связаны значения высот.


Также добавим новые цвета в генератор текстур:


Добавив таким образом новые правила, мы получим следующие результаты:

image

У нас получилась интересная карта вершин с представляющей ее текстурой.

Исходники кода первой части вы можете скачать отсюда: World Generator Part1.

image

В первой части цикла мы настроили небольшой фреймворк, который поможет нам создавать карты. Карту высот, которую мы создали ранее, невозможно склеить встык.

Так произошло, потому что мы создали двухмерные данные шума, которые не могут обеспечить нам необходимое. Для бесшовного свертывания нашего мира мы добавим новое измерение в наш генератор шума.

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

image

Представьте, что мы разрезали этот цилиндр и расстелили на плоскости. Именно этим мы и займемся. Края, по которым мы разрезали цилиндр, при склейке не будут иметь видимого шва.

Чтобы сделать это, необходимо изменить функцию GetData в классе Generator.


Выполнение этого кода создаст нам отличную текстуру, которая может сворачиваться на оси X.

image

Свертывание карты на обеих осях

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

Вместо одного цилиндра у нас будут два цилиндра, соединенных в четырехмерном пространстве.

Следует учесть, что создание четырехмерных данных занимает гораздо больше времени, чем двухмерных.

Обновленная функция GetData() будет выглядеть следующим образом:


Этот код создает бесшовную текстуру, процедурно сгенерированную из четырехмерного шума.

image

Если вы хотите узнать больше о том, как это работает, изучите эту и эту статьи.

Поиск соседних элементов

Теперь у нас есть бесшовная карта высот, и мы начинаем приближаться к нашей цели. Сейчас мы сконцентрируемся на классе Tile.

Было бы очень полезно, если бы каждый объект Tile имел указатель на каждый из соседних объектов (верхний, нижний, правый и левый). Это удобно для решения таких задач, как создание путей, битовых масок и заливки. Позже мы рассмотрим эти аспекты в статье.

Сначала нам нужно создать переменные в классе Tile:


Следующая часть довольно проста. Мы проходим по каждому тайлу, получая соседние к нему тайлы. Сначала мы создадим несколько функций внутри класса Generator, чтобы упростить получение соседей объекта Tile.


MathHelper.Mod() сворачивает значения x и y на основании ширины и высоты карты. Таким образом мы никогда не выйдем за пределы карты.

Затем нам нужно создать функцию, назначающую соседей.


Визуально она пока делает не так уж много. Однако теперь каждый объект Tile «знает» своих соседей, что очень важно для дальнейших шагов.

Битовые маски

Я решил добавить эту часть в статью в основном из эстетических соображений. Создание битовой маски в данном контексте — это установка значения каждого тайла на основании значений его соседей. Взгляните на эту иллюстрацию:

image

На основании данных соседей тайла мы увеличивает битовую маску, как показано слева на иллюстрации. Все варианты показаны в правой части. Заметьте, что все значения уникальны. Это позволяет очень быстро определить конфигурацию блока.

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

Еще одно удобство битовой маски в том, что если значение битовой маски тайла не равно 15, мы знаем, что он не является крайним тайлом карты.

Добавим функцию в класс Tile для выполнения вычислений. Мы берем в расчет только тех соседей, которые имеют тот же тип высоты, что и анализируемый тайл.


Поскольку мы уже имеем указатели на соседние тайлы, а также назначили тип высоты (HeightType), этот расчет довольно тривиален. Сейчас мы добавим функцию в класс Generator для выполнения этого расчета для всех тайлов:


Теперь если мы изменим наш TextureGenerator следующим образом:


Мы увидим четкую границу между типами высот:

image

Было бы здорово определиться со следующими вопросами:

  • где озера?
  • где океаны?
  • где массивы суши?
  • какого размера каждый из них?

Мы можем ответить на этот вопрос с помощью простого алгоритма заливки.

Сначала мы создадим объект, в котором будет храниться информация о наших тайлах:


Класс TileGroup будет хранить указатель на список тайлов. Также он будет сообщать нам, является ли конкретная группа водой или сушей.

Принцип состоит в разбиении соединенных частей суши и воды на коллекции TileGroup.

Также мы немного изменим класс Tile, добавив две новые переменные:


Collidable устанавливается в методе LoadTiles(). Все, что не является водным тайлом, будет присваивать значение «истина» переменной Collidable. Переменная FloodFilled служит для отслеживания тайлов, уже обработанных алгоритмом заливки.

Для добавления алгоритма заливки в класс Generator сначала нужна пара переменных TileGroup:


Теперь мы готовы обозначить массивы суши и воды на нашей карте.

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


С помощью вышеприведенного кода мы разделяем массивы суши и воды, и помещаем их в TileGroups

Я сгенерировал пару текстур, чтобы показать, как полезны могут быть эти данные.

image
image

На левом изображении все тайлы суши залиты черным. Тайлы океана синие, а тайлы озер голубые.

На правом изображении все тайлы воды синие. Большие массивы суши имеют темно-зеленый цвет, а острова — светло-зеленый.

Как вы видите, теперь у нас гораздо больше информации о сгенерированной карте, и эта информация отвечает на все поставленные нами вопросы.

Исходники кода второй части вы можете скачать с github: World Generator Part 2.

Читайте также: