Привет, Хабр! Обратите внимание, Распродажа «Чёрная Пятница» от издательства «Питер» скоро закончится. Поговорим о том, как в известных играх, например, Minecraft и Terraria, из ничего генерируются бесконечные и разнообразные миры. Пост снабжен подробными примерами кода.   Почти любому из нас доводилось хотя бы раз играть в такую игру, в которой автоматически генерируется ландшафт. Подобные игры по-настоящему сильно стимулируют пользователя продолжать игру, постоянно подбрасывая ему новые ситуации. Если вы – разработчик и любите уделять внимание деталям, то, вероятно, задумывались, а как генерируются такие бесконечные миры. Несмотря на всю их сложную структуру, вся такая генерация сводится к тщательно настроенной случайной составляющей. Процедурная генерация – это метод создания данных при помощи алгоритма, а не вручную. В случае с компьютерными играми такой метод применяется для генерации миров и их деталей без обязательного вмешательства разработчика. Данная техника широко используется в таких играх как Minecraft, Terraria или No Man’s Sky, в основе которых лежат открытые миры. Пользователь может как ему угодно исследовать такие миры и взаимодействовать с ними.  Процедурная генерация миров принципиально превосходит генерацию, выполняемую вручную – поскольку на разработку требуется меньше времени и меньше расходов. Размеры загружаемых файлов обычно удается существенно уменьшить, поскольку при поставке в игре отсутствуют огромные заранее построенные карты. Более того, обычно внедряются без труда, как только доведена до конца разработка движка, отвечающего за генерацию миров. Теперь перейдем к технической части посмотрим, как на самом деле устроен этот процесс. Я покажу вам, как сгенерировать двумерный мир. Код для примеров я решил написать при помощи Processing — это графическая библиотека и IDE на основе языка Java. Я выбрал Processing из соображений простоты, но вы можете воспользоваться любой технологией, которая вам удобна. В конце статьи я оставлю ссылку на GitHub-репозиторий, где выложен весь исходный код.  Обратите внимание: в примерах я использую файловое расширение .java, чтобы код хорошо раскрашивался. На самом деле у всех этих файлов расширение .pde. Всем нам известна концепция «шум» — обычно так называют беспорядочные раздражающие звуки. Теперь давайте познакомимся с численным шумом. Он состоит из множества точек, на значения которых влияют значения окружающих точек.  Факт взаимосвязанности этих значений друг с другом – ключевой аспект генерации миров, поскольку логично ожидать, что участки, расположенные рядом, будут выглядеть похоже, а не по-разному. Например, не предполагаешь, что посреди неба тебе может встретиться река. Из всех типов шумов особенно часто используется шум Перлина, названный так в честь его создателя, Кена Перлина. Шум Перлина позволяет генерировать псевдослучайные органические паттерны для любого количества измерений. Я не буду подробно разбирать математику, лежащую в основе этого алгоритма, поскольку во многих языках программирования предусмотрены встроенные функции для этой цели. В любом случае, если вы хотите подробнее познакомиться с этой темой, вот полное объяснение. Карты шумов – это графические представления шума в заданном n-мерном регионе пространства, обычно в 2D- или 3D-играх. В случае процедурной генерации они представляют конкретную характеристику мира, например, высоту, температуру, влажность и т.д. Каждая точка на карте определяет конкретное значение в финальном произведении. Рассмотрим, как можно сгенерировать карту шума – в данном случае для высот ландшафта – на языке Processing: Код для генерации двумерной карты шумов по высоте. Как вы, вероятно, уже догадались, также можно генерировать сразу много карт шумов для каждой характеристики, которой должен обладать наш мир. Предыдущее изображение шума было сгенерировано при помощи именно этой функции. Итак, какие еще характеристики должны быть у мира? Я решил остановиться на температуре и влажности. Процесс генерации этих карт похож на тот, что мы уже рассмотрели выше. Вся разница в том, что влажность в конкретной точке также зависит от температуры в этой точке, а на температуру влияет и высота ландшафта – все это совершенно логично. Например, в реальном мире температура на высоте 3000 м обычно ниже, чем на уровне моря. Код для генерации двумерных карт шумов, описывающих температуру и влажность. Для начала выясним, что же такое биом? Согласно National Geographic, биом – это обширная область, характеризующаяся определенной флорой, почвами, климатом и фауной. Как вытекает из этого определения, биом характеризуется конкретным набором характеристик, которые можно сгенерировать на основе карт шумов. Остается соотнести эти значения с разными биомами в зависимости от того, какие характеристики они должны иметь. Чтобы прояснить этот процесс, рассмотрим конкретный пример: саванны. Это относительно низкая равнина. В саванне растет высокая трава, поэтому этот ландшафт не должен быть засушливым, а также не должен промерзать. Настройка этих значений – обязанность программиста, и такая настройка полностью зависит от реализации. Процесс поиска наилучших значений связан с генерацией различных миров со слегка отличающимися друг от друга параметрами, после чего вы выбираете те результаты, которые вам наиболее понравятся. В моем примере я предполагаю, что диапазон высот в саванне должен быть в диапазоне от 135 и 169 – это значения, к которым я пришел методом проб и ошибок. Кроме того, полагаю, что температура должна быть в диапазоне от 0 до 50, влажность – также в диапазоне от 0 до 50.  Примечание: эти значения выражены не в метрах, градусах или процентах. Это просто числа, означающие конкретные координаты на соответствующей карте.  Для удобства давайте создадим на Processing классы Range и Biome: Классы Range и Biome. Теперь мы можем представить саванны как объект (и то же касается других биомов) и хранить их в массиве, который станет нашим списком биомов: Массив биомов с их свойствами. Как только определен список биомов мира, приходит время создавать карту биомов. Карта биомов, как и те карты, о которых мы говорили выше – это множество точек, каждой из которых присвоены конкретные значения. В данном случае, значение координат x,y – это биом, лучше всего соответствующий параметрам данной точки на картах шума.  В качестве пояснения рассмотрим другой пример. Допустим, в координатах x,y карты высоты, температуры и влажности имеют значения 140, 25 и 25 соответственно. Поскольку все эти значения вписываются в заданный для саванны диапазон высот 135–169 и диапазон 0–50, соответствующий температуре и влажности, биом с такими координатами должен относиться к саванне.  Теперь, чтобы не проверять вручную, какой набор значений какому биому соответствует,  давайте напишем для этого следующую функцию: Конечно же, это не самый эффективный способ определения биома, но я старался оставить код максимально простым. Как только каждой из точек присвоен нужный биом, переходим к созданию графического представления. Именно для этого используется свойство color класса Biome. Напишем на Processing функцию, которая отрисовывает на экране готовую карту:   Вот какой рисунок получается в результате: Случайные начальные значения – это просто числа, используемые для инициализации генератора псевдослучайных значений. Первое псевдослучайное значение генерируется из начального, и все последующие значения в случайной последовательности зависят от него. Начальные значения полезны в тех случаях, когда нужна предсказуемость результатов. Фактически, все сгенерированные последовательности псевдослучайных чисел, выведенные из одного и того же начального значения, гарантированно будут равны.   Таким образом, если вы подыщете по-настоящему красивый мир, который хотите сохранить и использовать для игры, то можете просто сохранить его случайное начальное значение. Более того, если бы вы хотели передать кому-то этот мир, то вам не потребовалась бы широченная полоса передачи данных; достаточно было бы послать начальное число. В Processing можно конкретно указать случайное начальное значение для функции noise() при помощи встроенной функции noiseSeed(). Она принимает в качестве значения число (случайное начальное значение) и не возвращает ничего.  Все карты, сгенерированные из этого начального значения, будут выглядеть совершенно одинаково, при условии, что код генерации мира останется неизменным. Чтобы реализовать в игре процедурную генерацию миров, попробуйте разделить мир на фрагменты, чтобы загружать только нужные ресурсы.  Для перемещения по миру нужно двигать координаты x,y в направлении движения. Функции шумов будут генерировать значения шумов (и, следовательно, карты) в соответствии с новой позицией. Вот снимок проекта, на основе которого написана эта статья. Видно, как генерируется ландшафт, пока вы движетесь по миру. Демо-версия процедурной генерации миров с изменением координат. Если вам будет интересно, загляните в репозиторий на GitHub по этому проекту. Заранее предупреждаю, что код не самый качественный, поскольку здесь я хотел создать всего лишь прототип Minecraft-подобной игры, которую сейчас разрабатываю.  Надеюсь, статья вам понравилась, и вы узнали что-то новое о том, как в играх генерируются миры.