Всем привет!
Наверное в большинстве игр, где есть враги, каждый разработчик задается вопросом "Как создать генератор волн противников? Чтобы было удобно и легко настраивать?"
В интернете врятли получится найти подробный ответ. В большинстве случаев результат будет "Задайте массив врагов и генерируйте их с задержкой".
Но сегодня я попробую подробно описать один из вариантов генерации противников.
Для начала, надо понимать, что нам нужно от генератора:
1. Игроку требуется пройти раунд;
2. Каждый раунд состоит из набора волн;
3. Каждая волна состоит из противников одного типа;
4. Генератор волн должен генерировать новую волну при определенных условиях;
5. Расположение противников генерируется случайно в некоторой области игрового пространства.
Распутывать этот клубок лучше всего с конца.
Расположение противников
В нашей игре мы определили некую область (большой куб), внутри которой могут генерироваться противники.
Каждая новая волна будет создавать врагов только в какой то малой части этой области (малый куб). Размеры этой малой части определяются случайным образом, но при этом, они не выходят за рамки большого куба.
Пример генерации малого куба
Как можно расположить противников внутри малого куба? Тут может быть множество вариантов, но лучше всего начать с простых:
1. Задать случайные позиции;
2. Задать одинаковые позиции (все противники появляются из одной точки);
3. Задать позиции по горизонтали слева направо (или справа налево);
4. Задать позиции по вертикали сверху вниз (или снизу вверх).
Схематичные примеры расположения, вид спереди
При этом, в вариантах 3 и 4 можно дополнительно отражать расположение по горизонтали или вертикали относительно центра малого куба. Схематично это выглядит так:
Между генерацией противников можно указать задержку. Это положительно скажется на производительности, т.к. одновременное создание множества объектов требует значительного времени и пользователь может заметить такие лаги. Зная количество противников, можно рассчитать расстояние между точками (Количество противников будет случайным в диапазоне, но об этом чуть ниже).
Итого, из примитивных вариантов генераций у нас уже получается хороший набор :)
В игре такие способы расположения выглядят весьма неплохо!
Пример горизонтального отражения
Пример горизонтального и одновременно вертикального отражения
Когда генерировать новую волну?
На самом деле, это достаточно сложный вопрос, от которого напрямую зависит, насколько интересно будет играть. В большинстве игр типа Tower Defense, волны генерируются через определенное время. Такой вариант нас не устроил, т.к. в этом случае противников может накопиться очень много и игра начнет тормозить.
Поэтому мы придумали другой вариант: мы будем начинать генерировать новые волны, когда текущее кол-во противников опустится ниже определенного минимума. Останавливать генерацию новых волн мы будем, когда кол-во противников превысит определенный максимум.
Таким образом, мы будем иметь кол-во кораблей в неком диапазоне минимум-максимум. Такой подход поможет держать игрока поочередно то в напряжении, то в расслаблении и не даст заскучать :)
Сами волны могут генерироваться последовательно друг за другом, либо параллельно. Это мы укажем в настройках волны.
Указание типа противника
Для каждой волны нам нужно знать, какого противника нужно генерировать и в каком количестве. Для этого нам достаточно указать ссылку на объект-противника (префаб) и задать минимальное и максимальное количество. Еще можно указать уровень противника. Это некий показатель, от которого будут зависеть урон, жизнь, скорость передвижения и другие параметры противника.
Если постараться это всё обобщить, то получится следующая структура для волны противников:
Данные генерации
-генерация волны: последовательная или параллельная;
-начальная задержка перед генерацией волны.
Данные расположения
-тип расположения противников: один из шести видов;
-задержка между генерацией;
-размер малого куба (относительно большого);
-позиция малого куба (относительно большого);
-горизонтальное отражение: да\нет;
-вертикальное отражение: да\нет.
Данные противника
-ссылка на объект (префаб);
-уровень противника;
-минимальное и максимальное количество.
Настройка раунда
Для хранения списка волн мы решили использовать специальный объект в Unity - ScriptableObject. Данный объект представляет собой что то типа профиля настроек.
Пример заполнения настройки раунда
Тестирование
После создания десяти раундов с постепенным увеличением сложности, мы успешно протестировали их. И после этого сделали сборку игры для тестировщиков :) Почти все тестировщики попросили сделать раунды более сложными, т.к. в текущих раундах было очень мало волн (5-10) и они быстро кончались.
Вот тут началось самое интересное. Мы осознали, что нам очень сложно управлять такой подробной настройкой. Она хоть и оказалась очень гибкой, но при этом на ее изменение ушло бы очень много времени. Особенно если учесть, что мы планировали 3 кампании по 20 раундов.
Пришлось думать, как можно это упростить.
Упрощение
Через несколько дней ко мне в голову пришла светлая мысль: игрокам не важно, как расположены противники, в каком месте и в какой последовательности они появляются. Поэтому мы можем использовать случайны числа! Выбирать случайный тип корабля, случайное количество и т.д. Но при этом оставалась проблема с нарастающей сложностью, ведь надо было как-то высчитывать, сколько кораблей должно быть в волне, сколько всего в раунде, как они будут изменяться...
Но вскоре и эта проблема была решена. Я придумал использовать игровые очки!
Когда система генерирует противника, противник будет использовать некоторое количество очков. Различные типы противников будут использовать различное количество очков, например:
Истребитель - 1 очко;
Фрегат - 2 очка;
Крейсер - 4 очка и т.д.
При этом, каждый раунд будет иметь заранее рассчитанное количество очков, которые тратятся на создание противников. Эти очки для каждого следующего раунда будут увеличиваться.
Например, 1 раунд - 30 очков, 2 раунд - 35 очков и т.д.
Раунд считается завершенным, когда потрачены все очки раунда.
Так как раунд состоит из волн, то каждая волна также будет "запрашивать" некоторое случайное количество очков и на полученные очки генерировать противников. К примеру, от 5 до 10 очков на волну. Если у волны будет 10 очков, то на них можно создать 10 истребителей или 5 фрегатов. Чтобы в первом раунде не появились сразу мощные противники - крейсеры, то можно указать номер раунда, после которого волна будет доступна для генерации. Также, с каждым новым раундом, очки для волн будут увеличиваться.
Теперь, если всё обобщить, то для настройки одной кампании потребуется:
Настройка раундов
-количество раундов в кампании;
-количество очков первого раунда;
-инкремент очков раунда за каждый раунд;
Настройка волн
-количество очков волны;
-инкремент очков волны за каждый раунд;
Настройка генерации
-уровень противника;
-инкремент уровня противника за каждый раунд;
-минимальное и максимальное кол-во активных противников;
-инкремент минимального и максимального кол-ва противников за каждый раунд;
-ограничение лимита минимального и максимального кол-ва активных противников;
-список типов кораблей.
Пример заполнения профиля кампании
В упрощенной системе нам уже не понадобится куча настроек для каждого типа противника, поэтому выберем самое необходимое:
Настройка типов противников
-ссылка на объект-противника (префаб);
-минимальный номер раунда, в котором появляется волна;
-требуемые очки за одного противника;
-минимальное количество кораблей;
-максимальное количество кораблей (опционально);
-минимальная задержка между генерацией противников;
-максимальная задержка между генерацией противников.
Пример заполнения профиля типа противника
После того, как мы внедрили новую систему генерации, настраивать уровни стало гораздо проще и быстрее! Но оставалась одна проблема: т.к. мы использовали случайные числа, то у нас каждый раз генерировались разные последовательности противников при повторном прохождении раунда. Чтобы решить эту проблему, нам нужна была последовательность случайных чисел, которую всегда можно было бы повторить. Тут нам очень помог системный генератор случайных чисел System.Random. Этот генератор чисел будет каждый раз повторять псевдослучайную последовательность чисел, если каждый раз задавать ему одно и то же начальное значение. После внедрения System.Random, наш генератор волн стал выглядеть идеально! :)
А самое главное - мы смогли получить позитивные отзывы от тестировщиков!
На этой приятной ноте я завершаю вторую статью, спасибо всем, кто дочитал :) Надеюсь, эта статья поможет тем, кто хочет сделать свой генератор волн.
В следующей части попробую рассказать о противниках, которые используются в игре, об их особенностях и основных настройках.
P.S.
Я намеренно не выкладываю примеры кода, чтобы не усложнять повествование. Очень надеюсь, что понимая структуру данных для генератора, многие смогут самостоятельно написать код для его работы :) Но если возникнут затруднения или будут какие то вопросы - конечно, пишите :)
Ссылка игры в Google Play: Space Turret: Defense Point