Вступление
В статье речь пойдет об одном очень старом проекте, который создавался совсем в другое время и совсем в других условиях. Это моя старенькая RTS под названием Земля онимодов (Onimod land). Чтобы было сразу понятно, что она собой представляет, можно посмотреть коротенькое видео:
У этой статьи существует продолжение. Официальный сайт игры Cтраница на Steam. (Версия с моего официального сайта никак не связана с платформой Steam)Пожалуй, будет правильным сразу же уточнить следующее: Данная игра была запрограммирована когда-то лично мною с нуля без движков, конструкторов и прочих современных инструментов. Проект достаточно большой и делался на чистом энтузиазме несколько лет. Вопреки всем законам экономики мне-таки удалось довести эту работу до конца, правда, к тому моменту численность команды разработчиков уже уменьшилась до одного человека. К сожалению, на момент своего выхода эта игра не могла по многим причинам оказаться востребованной широкими массами игроков - рынок игр поменялся сначала в сторону 3D, потом в сторону казуальных игр. Долгое время эта игра лежала у меня, так сказать, "на полке", да и сам я давно перестал быть разработчиком игр. Тем не менее, вспомнить о том, как всё это делалось, мне достаточно интересно. Тем более, что лично я играю в собственную игру с большим удовольствием. Я совершенно не уверен, что всем известный Blizzard делал свой Starcraft по той же схеме, что и я, но я точно помню, что по ходу разработки я периодически натыкался на те же самые проблемы, на которые в своё время натыкались разработчики Blizzard-а. Это можно было понять по тем решениям, которые и им, и мне пришлось использовать, чтобы выпутаться из одинаковой "ситуации". Свой вариант о том, как я всё это делал, я и предлагаю вашему вниманию. В конце я коротко опишу сами условия, в которых происходила разработка, а также те "правила игры", которые на тот момент устанавливали российские издатели.
В статье делается попытка описать общее устройство классической RTS. Естественно, что описание будет не особенно подробным, так как нет никакой возможности описать подробно такой объем кода (да и, честно говоря, даже для автора удержать в голове тонкости реализации нереально, особенно учитывая, сколько уже лет минуло). Однако, хочется надеяться, что какое-то пусть и поверхностное представление о том, как всё это делается с нуля, мне осветить удастся. Кое-где я буду вставлять небольшие куски исходного кода, но делать это буду, скорее, для демонстрации принципа, чем для чего-то большего.
Будут рассмотрены следующие механизмы, которые, на мой взгляд, являются фундаментальными:
- Графика (в каком виде, что и как).
- Игровые ресурсы / редактор ресурсов - подготовка спрайтов для игры, создание игровых объектов (юнитов и зданий).
- Управление объектами - сказ о том, как заставить юнитов что-то делать.
- Алгоритм поиска пути.
- AI или Искусственный интеллект - раздел сначала разбирает действия Инстинктов или врожденную сообразительность юнитов, а потом уже пытается пояснить процесс глобального управления командой.
- Сеть.
- Коротко об авторах и о проделанной работе.
- Стартовые условия и их последствия - это лирический раздел, не имеющий отношения к сути статьи. Я решил, что он не будет лишним, так как наши представления о том времени могут быть различными. Поэтому я кратенько освещу свой вариант ситуации, в которой начинался этот проект.
RTS
В качестве игрового жанра была однозначно выбрана RTS с типом графики 2D в изометрии. Такой выбор был обоснован исключительно нашими личными предпочтениями. Основным языком программирования я выбрал для себя C++, который после длительного программирования на ассемблере на процессоре типа Z80 (ZX-Spectrum) показался мне очень простым, так как по своей сути сильно напоминал очень высокоуровневый ассемблер. Хотя должен признать, что я долго не мог привыкнуть использовать виртуальные функции, а без этого C++ сильно похож на обычный C.
На мой взгляд, если вы делаете игру с нуля, то первое, что нужно сделать - это получить хоть какое-то изображение на экране. Имеется в виду, конечно, что изображение должно быть осмысленным, т.е. своим появлением тестировать игровые механизмы. Основной момент здесь - это способ попадания графики в игру и её последующая отрисовка. Именно с этих двух моментов нужно всё начинать.
Графика
В тот момент подавляющее большинство игр использовало 8-битное цветовое разрешение, и мы логично решили, что мы пойдем дальше и наша игра будет использовать 16-битные цвета. Почему не 24-битные ? На тот момент на видеокартах в качестве нормы присутствовал 1 Мб памяти. Количество памяти, которое потребуется под одну экранную поверхность 800x600, соответственно, составит 800 x 600 x 2 = 960000 байт или 937,5 КБайт. Т.е. под одну экранную поверхность задействовалась почти вся видеопамять тогдашних видеокарт. Изначально наша игра должна была уметь работать в режиме экрана 640x480, но в дальнейшем этот вопрос был пересмотрен в настоящий момент игра работает с любым разрешением, которое позволяет монитор.
Для хранения графики требуется память. Понятно, что хранить что-то на видеокарте в моем случае не получится, да это и невозможно с учетом предъявляемых требований. Поэтому спрайты должны лежать в ОЗУ.
К самому спрайту предъявлялись следующие требования:
- Спрайт должен обладать именем.
- Сжатый спрайт может иметь любую форму, а не только прямоугольную. Пиксели, которые на изначальной прямоугольной форме полностью прозрачны, обозначаются специальным цветом, чтобы их можно было отбросить.
- Полупрозрачные пикселы не используются.
- Спрайт может содержать идентификационный цвет, который можно менять во время рисования. Это требуется, чтобы "отмечать" юниты/здания, принадлежащие к одной команде, своим цветом.
- Вместо 16-битного цвета закодированный спрайт должен содержать индекс цвета в палитре, который кодируется 1 байтом. Палитры будут строится отдельно для большого количества спрайтов. Такой подход позволял сильно экономить на хранении цвета пикселей, но требовал возможности переключать текущую палитру во время визуализации. Однако чаще всего спрайту вполне хватало одной палитры.
- Спрайт должен содержать, так называемый, pivot (координату начала спрайта), относительно которой производится рисование.
- Спрайт может обладать размерами в "клетках". Игровое поле - это клеточный мир, поэтому иногда полезно знать то пространство, которое занимает спрайт на игровом поле. На картинке показаны размеры спрайтов в "клетках" и положение спрайта относительно клеток, т.е. pivot.
В результате по спрайтам я принял такое решение: хранить графику в памяти в сжатом виде и раскодировать её в момент вывода на экран. Главным плюсом этого варианта является малое количество памяти, которое требуется спрайту, главным минусом, естественно, будет постоянное раскодирование процессором каждого спрайта в момент рисования. Что для этого требуется ? Нужен формат сжатия и алгоритм, который будет сжимать и разжимать картинку.
Для сжатия был выбран следующий способ. Алгоритм проходил по прямоугольной области спрайта по вертикали, начиная от левого верхнего угла. Основной для рисования служили вертикальные неразрывные полоски пикселей (вертикаль была выбрана из того простого соображения, что юниты в основном имеют больший размер по высоте). Каждая такая полоска запоминалась как массив, о котором известно количество пикселей и цвета этих пикселей. Когда полоска прерывалась "пустым" пикселем, то происходил поиск следующей вертикальной полоски. Между окончание одной полоски и началом следующей сохранялось смещение координат, что позволяло избежать хранения и, самое главное, рисования "пустых" пикселей. Ко всему этому добавлялись трюки с палитрами, которые могли переключаться на ходу. В упрощенном виде формат сжатия представлял из себя следующие команды:
- Смещение координат относительно предыдущей порции пикселей
- Массив цветов пикселей по вертикали (количество, затем цвета)
- Установка новой палитры
Всё это неплохо, но возникает вопрос, а как быть с цветом, который идентифицирует команду, т.е. постоянно изменяется ? Решение в данном случае достаточно простое. Этот идентификационный цвет можно всегда кодировать 0-ым индексом в палитре. Тогда нулевой цвет в палитре будет как бы зарезервирован под изменчивость. Зато перед рисованием, спрайта достаточно в используемых палитрах заменить самый первый цвет на цвет команды, и вопрос решен.
В качестве дополнительной оптимизации был использован такой прием. Многие здания содержали анимацию, которая занимала куда больше места, чем занимал бы просто статический спрайт здания. Но так как само здание всегда отображалось статическим спрайтом, а анимация накладывалась поверх спрайта, то из анимации можно было выбросить те пиксели, которые по цвету совпадали с цветом статического спрайта здания. Обычно это удаляло большую часть пикселей из спрайта с анимацией.
Например, в игре Плантация выглядела так:
В реальности же графика хранилась так:
Теперь надо решить вопрос с раскодировкой сжатого изображения. А эта задача оказывается значительно сложнее, чем само сжатие. Принципиальное отличие состоит в том, что сжимать изображение можно заранее, т.е. не заботясь о скорости этого процесса. Раскодирование же должно выполняться "на лету" причем постоянно и для каждого спрайта, выводимого на экран. И именно в этом моменте и заключалась главная трудность. Вторая трудность, вытекающая из первой состояла в том, что спрайты должны раскодироваться разными способами. Например, представим, что какой-то юнит включил себе режим "маскировки", который визуально выглядит так, что он начинает быстро становиться прозрачным почти до полного растворения. Или, допустим, у летающих юнитов должна присутствовать тень на поверхности ландшафта, причем тень на деле являлась отдельным спрайтом, чтобы летающий юнит мог изменять высоту, а тень при этом оставалась на земле. Тень всегда черная и всегда обладает прозрачностью, а значит её можно раскодировать по собственному алгоритму.
Или еще один пример, направленный на оптимизацию - если заранее известно, что спрайт использует только одну палитру, то достаточно выставить её один раз и при раскодировании вообще не проверять на изменение палитры. В общем, вариантов по улучшению алгоритма раскодирования для частных случаев вполне хватает. Присутствовал даже такой экзотический вариант раскодировки спрайта, который проверял на наличие пикселя по указанным координатам, что требовалось для проверки на точное попадание мышью.
Кроме того, все варианты раскодировки дублировались для следующих случаев:
- Зеркальное отражение спрайта по горизонтали.
- Спрайт частично оказывается за пределами окна. Т.е. было необходимо ручками срезать кусок, который не попадал в видимую область экрана.
Для раскодирования было решено применять ассемблер, так как данный алгоритм являлся самым узким местом во всей программе. Ассемблер же позволяет программисту эффективно использовать регистры процессора, а не размещать переменные в стеке, как это обычно делает компилятор. В результате на ассемблере было написано несколько десятков функций, которые, по сути, делали одно и то же раскодирование, но с разными нюансами. Нужная функция вызывалась из C++ получая в качестве аргументов информацию о сжатом спрайте и тонкостях его визуализации.
В качестве ассемблера использовался tasm32.exe. Командная строка для сборки ассемблера выглядела так: tasm32 /l /ml /zi modul_s.asm, где modul_s.asm исходный код на ассемблере. В результате образовывался объектный файл modul_s.obj, который уже можно было линковать к проекту на C++.
Само обращение к функциям из C++ выглядело примерно так: extern "C" bool AsmSprite(параметры);
Ресурсы
Вопрос хранения и редактирования ресурсов - это, пожалуй, самый важный вопрос, который определяет в дальнейшем всё остальное. Поэтому, на мой взгляд, создание любой не самой маленькой игры нужно начинать с создания редактора ресурсов. В идеале у вас должен быть визуальный редактор, который позволит вносить почти любые изменения в игру без помощи программиста. Т.е. при создании игры заниматься самой игрой нужно, как ни странно, в последнюю очередь, а первый ваш штурм должен быть направлен на создание такого редактора. Редактор ресурсов не нужно путать с редактором карт - у них принципиально разные назначения. Редактор карт - это задача, наоборот, уже "под занавес", так как чтобы рисовать карты вам, практически, требуется, чтобы сама игра уже функционировала.
Итак, вернемся к ресурсам...
В моем случае в игре должны были обрабатываться ресурсы следующих видов:
- Спрайты (служат изображением для всех остальных видов ресурсов)
- Неживые объекты (деревья, камни, минералы, разные украшательства)
- Живые объекты: юниты и здания
- Снаряды (стрелы, ядра, ракеты, магия)
- Апгрейды, т.е. исследования, которые можно произвести во время игры с целью улучшения характеристик юнита или здания.
Со спрайтами всё было более-менее понятно. Их надо загружать, сжимать и сохранять большими группами в отдельный файл, который потом игра сможет открывать уже самостоятельно. Так как спрайт обладает именем, то в некоторых случаях вполне можно идентифицировать по этому имени принадлежность спрайта к Неживым объектам.
Как я уже говорил ранее, на начальной стадии очень важно получить изображение, поэтому редактор ресурсов можно начинать с возможности загружать и сжимать спрайты.
Далее необходимо было подумать о том, чтобы научить редактор ресурсов создавать Живые объекты. Для этого недостаточно просто иметь набор картинок, необходимо объединить эти картинки в анимации и объяснить игре, для чего именно используется та или иная анимация.
Давайте разберемся, какие обычно действия может выполнять боевой юнит в RTS. Очевидно, что юнит должен уметь находиться в состоянии "ничегонеделания" - назовем его Ожидание. Далее юнит должен уметь перемещаться - назовем это действие Перемещение. Юнит должен уметь сражаться - назовем это действие Воздействие (вообще воздействие может быть и положительное, например, Излечивание). И, наконец, юнит, которому не повезло с командиром, должен иметь действие Смерть. Эти действия представляют собой базовый набор истинного воина.
На самом деле ситуация с Воздействием была гораздо сложнее, так как существовали и дополнительные стадии. Полный цикл Воздействия мог выглядеть примерно так:
- Подготовка к Воздействию - это, практически, подготовка к стрельбе (Арбалетчик присел для выстрела).
- Воздействие - собственно, сам выстрел.
- Перезарядка оружия - в настройках юнита можно указать количество выстрелов без перезарядки, затем оружие нужно было перезарядить (Арбалетчик закладывает новую стрелу).
- Пауза между Воздействиями - здесь могла быть любая анимация, но самое главное, что данную стадию можно ускорять за счет Апгрейдов, тогда юнит начинает стрелять чаще.
- Выход из Воздействия - цель уничтожена и новая цель отсутствует, значит можно войти в обычный режим (Арбалетчик, который присел для стрельбы, выпрямляется).
Дополнительно вся эта схема сильно осложняется тем, что у нас не 3D, а стало быть нельзя просто крутить модель, чтобы показать её с разных сторон. Поэтому у анимации обязательно должно быть еще и Направление (вверх, вниз, вправо, влево и т.д.). Направления эти, естественно, должны быть фиксированными, причем надо учитывать, что каждое Направление, практически, требует дублирования всех спрайтов юнита. Исходя из этого, было выбрано 8 направлений движения для юнитов, причем в реальности их получалось сократить до 5-и, так как не зеркализуются в противоположные стороны только направления "вверх" и вниз".
Для юнитов, которые слишком резко меняли своё направление, было добавлено действие Поворот, например, Танк поворачивался на месте для смены направления движения.
Свойства, которыми обладает анимация:
- Действие - показывает игре возможности объекта (Ожидание, Перемещение, Воздействие и т.д.).
- Направление движения (5 шт) - требуется, чтобы объект визуально мог отображаться в разных направлениях. Обычно для каждого направления движения потребуется дублирование анимации со всеми её свойствами, кроме, собственно, спрайтов.
- Завершение анимации:
- Окончание - анимация завершается (если делать больше нечего, то обычно запускается анимация на Ожидание)
- Зацикливание - анимация начинается с начала (полезно для Зданий, на которых может бесконечно воспроизводиться анимация Ожидание)
- Переход - запуск другой анимации, дополнительно требуется выбрать анимацию (позволяет комбинировать анимации одну с другой, можно использовать случайную анимацию из нескольких вариантов).
- Новый объект - в момент окончания текущий объект превращается в новый, который должен быть указан (полезная штука для случая, когда, например, Шаман оживляет Статую, т.е. требуется превратить здание в юнита).
- Смерть - текущий объект уничтожается в памяти игры.
- Прерывание анимации - признак того, что выполнение анимации может быть прервано действиями пользователя (ткнул куда-то мышкой). Дополнительно можно указать, анимацию, которая будет выполнена, если пользователь попытался прервать текущую анимацию. (Например, Солдат в состоянии отдыха держит автомат перед собой, а когда шагает, то автомат у него за спиной - мгновенное переключение между этими состояниями смотрелось бы не очень хорошо, поэтому есть промежуточная анимация, которая визуально убирает автомат за спину, именно она и сработает прежде чем Солдат сдвинется с места).
- Скорость - позволяет указать скорость выполнения анимации в процентах.
- Иконка - изображение для панели управления, куда щелкает пользователь для выполнения данного действия.
- Стоимость - некоторые действия не бесплатны, например, чтобы маг мог колдовать, ему нужна энергия.
- Кадры - анимация состоит из спрайтов (кадров). Это самое важное свойство анимации. Чаще всего кадр лишь отображает соответствующий спрайт, но есть и большой дополнительный функционал:
- Пауза - определяет задержку анимации на данном кадре. Есть возможность использовать случайные задержки. Паузы хорошо помогают изобразить неторопливое разложение убитого воина, так как обычно труп присутствует на карте еще очень долго, после его смерти.
- Звук - можно проиграть какой-нибудь звуковой файл. Данное свойство хорошо себя зарекомендовало в анимациях стрельбы из автомата или долбания противника мечом.
- Функция - это конкретное действие, которое выполняется на данном кадре анимации. Можно было одновременно задать несколько функций на одном кадре (например, выпустить ракеты из двух стволов сразу). Перечислим основные функции (хотя их немного больше):
- Воздействие - это и есть тот самый момент, когда враг получает топориком по голове. Это, конечно, самое простое использование функции Воздействия, на деле же дополнительные параметры позволяют указать тип этого воздействия. А здесь в списке есть не только Физический удар, но и Лечение, и Воровство магии, и еще куча всего. Также в тип воздействия входят такие обыденные дела, как Добыча ресурса (Работник стукает топориком по ёлочке и на нужном кадре анимации функция Добыча ресурса вытряхивает ему из дерева дровишки), а еще здесь же банальный Ремонт поломанного Здания, когда Работник возвращает Зданию жизнь. Любое Воздействие всегда направлено на текущую цель, над которой юнит выполняет действие.
- Взрыв - по координатам текущего объекта выполняется взрыв, у которого регулируется сила и радиус ударной волны. Для взрыва также можно выбрать тип воздействия, но обычно это просто Физический удар. Логично предположить, что функцию Взрыва обычно используют не Живые юниты, а Снаряды, которые взрываются достигая цели.
- Зарождение объекта - функция для создания другого объекта. Например, Лучник с помощью этой функции должен создать Стрелу на данном кадре анимации. Дополнительно для Стрелы надо будет указать координаты, по которым она будет создана, т.е., практически, придется еще таскать спрайт Стрелы по экрану, чтобы правильно расположить его относительно Лучника. Естественно, что позиционировать создаваемый объект относительно создающего необходимо для всех направлений движения, т.е. практически, эта операция выполнялась 5 раз. В каждом случае подбирался такой спрайт Стрелы, который больше соответствовал бы текущему направлению юнита. Стрела в игре начинает своё существование именно с этого спрайта, а далее может самостоятельно разворачиваться, в зависимости от направления к цели.
На деле возможностей, конечно, было гораздо больше, но нет особого смысла в перечислении всего - главное, пожалуй, принцип.
Естественно, что юниты должны иметь характеристики, такие как сила, защита, количество жизни, скорость перемещения. Для них должно быть определено Здание, в котором они будут производиться, скорость производства, стоимость и т.д. Есть юниты, которые могут перевозить других юнитов, т.е. Транспорты. Есть вариант, когда юниты не просто перевозятся Транспортом, а еще и стреляют из него. Некоторые юниты могут летать, соответственно, для них нужно определить высоту полёта "от" и "до", в этих пределах летающий юнит может изменять свою высоту случайным образом. Некоторые Здания умеют действовать по аналогии с транспортами, т.е. юниты могут попадать внутрь, например, Лучники могут быть помещены в Защитную Башню, чтобы стрелять из неё.
Отдельного стоит кратко упомянуть об объектах Снарядах. Снаряд может поражать наземные или воздушные цели, быть автонаводящимся, т.е. следовать за целью как ракета или вести себя как катапультное ядро, т.е. двигаться по параболической траектории. Снаряд умеет выполнять только два типа анимаций Полёт и Смерть. Анимация Полёт позволяет указать Снаряду множество направлений, а не фиксированные 8 как у обычного юнита, что обеспечивает плавное изменение траектории Снаряда. В момент достижения цели Снаряд выполняет анимацию Смерть, которую можно завершить функцией Взрыв. Кроме того есть возможность дополнительно указать, что снаряд может поджигать деревья. Создаваемый Снаряд всегда получает какую-то Цель в виде координат либо игрового объекта, чтобы двигаться к ней, иначе само существования Снаряда теряет смысл.
Апгрейды, в моем случае, это такие же объекты, которые создаются с помощью редактора ресурсов. Апгрейд в игре может существовать только в одном экземпляре. Апгрейд не имеет анимаций, но имеет иконку для панели управления и список производимых действий. Также апгрейд может иметь конкретный список объектов, на которые он действует. В момент, когда апгрейд произведен, в памяти игры просматриваются все объекты и для тех, на кого действует данный Апгрейд, выполняется изменение характеристик. Помимо обычных Апгрейдов существуют и очень фундаментальные. В частности, для расы Онимодов есть возможность выбрать Путь развития, коих у него ровно 3:
- Путь Нападения
- Путь Защиты
- Путь Неизвестного
История их появления весьма необычна, и связана с тем, что одному из художников очень нравилось рисовать технику для расы Онимодов. В результате для Онимодов у меня были следующие боевые единицы тяжелой техники:
- Дрон
- Танк
- Броневик
- Камикадзе
- Когг
Выбрасывать столько готовых юнитов с моей стороны было бы непростительным расточительством, особенно учитывая, что для второй расы Ботсвана нужных юнитов, наоборот, не хватало. Поэтому для Онимодов я придумал такую штуку как Путь развития. Выбирая один из путей, игрок, практически, делает доступными одних юнитов и запрещает других. Например, первый путь, именуемый Путь Нападения позволял использовать юнитов Дрон, Танк и Камикадзе. Во втором пути, который назывался Путь Защиты, были доступны Дрон, Танк и Броневик, причем Танк умел трансформироваться в очень сильную дальнобойную пушку, а Броневик, практически, являлся ходячим дотом, так как может возить Солдат, которая находясь внутри расстреливает противника. Третий путь, именуемый Путь Неизвестного, позволял использовать юнитов Дрон, Камикадзе и Когг. Дополнительно этот путь обладал достаточно интересной возможностью - он позволял присылать воинов в Капсуле (выполняет у Онимодов роль Дома) прямо с орбитальной станции, которая по сценарию висит на орбите планеты. Визуально это выглядит так, что при посадке здания из него вываливаются юниты.
Влияние пути развития не ограничивалось только набором доступных юнитов - некоторые ключевые Апгрейды были доступны только в одном из путей. Например, Путь Нападения, позволял сделать Апгрейд, который давал возможность сажать в Вертолёт до 3-х Штурмовиков, ведущих огонь прямо из Вертолета. В результате выбор Пути развития сильно влиял на общую стратегию ведения боевых действий.
Управление объектами
Юниты и Здания должны выполнять те действия, которые им поручены игроком или AI. Для выполнения этих действий каждый объект имеет в своем составе массив команд. Чаще всего в массиве будет одна команда, которая и будет текущей, но, например, игрок может используя клавишу SHIFT добавить на выполнение сразу несколько команд. Завершив выполнение очередной команды, объект приступает к выполнению следующей. Если все команды выполнены, то объект автоматически добавляет в массив команду COM_WAIT или Ожидание, что в результате запускает анимацию с действием Ожидание.
Также юниты обладают возможностью самостоятельно добавлять к себе в массив команды. Эта ситуация рассмотрена в разделе AI где описываются Инстинкты юнитов. Простой пример: воины получили от игрока команду "Патрулирование" и курсируют между двумя точками. Если обнаруживается неприятель, то воины самостоятельно добавляют себе в начало массива команду Уничтожение или COM_DESTROY и атакуют цель. После того, как COM_DESTROY будет выполнена, она будет удалена из массива команд и управление опять вернется к команде Патрулирование, что приведет к тому, что воины продолжат патрулирование.
Для начала нужно разобраться, что же представляется из себя команда. Простейший случай команды - это COM_GO или Перемещение. Игрок выделил юнита и щелкнул мышкой в произвольное место карты. Это приведет к тому, что в массив команд выделенного юнита попадет COM_GO, которая незамедлительно начнет осуществляться. Эта команда очень проста, так как в качестве аргумента ей нужны лишь координаты точки на карте, к которому должен подойти юнит. Однако обратите внимание, что данная команда не имеет продолжения, т.е. она завершится после того, как юнит достигает цели. На деле же существуют куда более сложные команды, например, Патрулирование, Строительство здания или Добыча ресурсов. Все эти команды имеют несколько фаз, к примеру, Добыча ресурсов подразумевает после того, как текущий ресурс полностью добыт, найти новый ресурс и приступить к его добыче, т.е. команда Добычи ресурса, по сути, не заканчивается до тех пор, пока поблизости есть что добывать.
Или другой пример. Работнику дана команда Строить здание:
- Работник закладывает фундамент. Если на месте закладки фундамента находятся юниты, то они отойдут в сторону, освобождая место.
- Работник осуществляет строительство (оно же ремонт) здания.
- Работник ищет рядом другое недостроенное или поломанное здание. Если такое здание не найдено - завершаем выполнение команды, иначе нужно выполнить Ремонт найденного Здания.
Обработчик команд выполняет команды не постоянно, а только в тот момент, когда завершена очередная анимация. Т.е. шагнул юнит на одну клетку в сторону - теперь можно обрабатывать команды или стукнул Работник киркой по камушку - опять можно обрабатывать команды. Но в середине шага или на половине замаха киркой команды не обрабатываются. Практически, после выполнения очередной команды становится известна следующая анимация.
Чтобы была возможность выполнять команды любой сложности, необходимо разделить их на фазы. Значит любая команда состоит из фаз, и этих фаз может быть N штук. Сначала всегда выполняется первая фаза, но далее фазы могут выполняться в любой последовательности, так как любая фаза может передать управление на любую другую фазу. Каждая фаза может прервать команду, если нет возможности для её выполнения. Любая фаза, в моем случае, должна уметь выполнять следующие функции:
- INITIALIZAION - Инициализация перед началом выполнения фазы.
- TEST - Проверка цели на пригодность выполнения над ней действия (цель может умереть или уже не нуждаться в выполнении над ней указанной команды). Чаще всего эта функция не требуется, так как большинство команд направлены на другие игровые объекты, а проверка по умолчанию типа "живой / не живой" выполняется автоматически. Команды, не требующие цель, и вовсе не нуждаются ни в каких проверках на её пригодность. Если проверка всё же присутствует, то она вызывается постоянно по ходу выполнения фазы в момент, когда обработчик команд получает управление.
- ACTION - Основное действие фазы, которое является целевым. Выполняется, когда цель достигнута (например, оказалась в пределах зоны поражения). Выполняется один раз, как и Инициализация. Может вместо чего-то конкретного просто передать управление на другую фазу.
- FIND - Выбор новой цели для данной команды. Обычно эта функция не определена. Выполняется один раз в случае, если цель стала недоступна для выполнения над ней действия (здание было полностью отремонтировано - нужно поискать другое поломанное здание).
Большинство функций в фазе могут быть лишними. В этом случае их можно опустить.
Команда Добывать ресурс является одной из самых сложных. Поэтому хотелось бы разобрать именно её более подробно. Визуально добыча ресурсов осуществляется для Онимодов и Ботсваны принципиально иначе, но в основе всего этого лежит один и тот же алгоритм.
Добытчик Онимодов называется ИТР и визуально выглядит как достаточно крупный робот, который закрепляется рядом с ресурсом и осуществляет добычу. Добытые ресурсы он отправляет в Хранилище не двигаясь с места. Визуально это выглядит так, что он погружает в землю "трубу", а на Хранилище анимация этой трубы появляется на одной из шести технических площадок, что визуально демонстрирует, что ресурс доставлен.
Добытчик Ботсваны называется Работник и является как бы "классикой жанра". Он добывает ресурсы при помощи кирки и носит их в Хранилище своими ножками.
Специально в игру для обоих команд было добавлено условие, что добытчики у обоих рас могут одновременно добывать один и тот же ресурс только одним добытчиком.
Итак, игрок выделил мышкой несколько добытчиков и щелкнул правой кнопкой по минералу. Что происходит дальше ? Так как я сам ввел условие "каждому ресурсу по одному добытчику", то первым делом я выбираю для каждого добытчика свой персональный минерал, чтобы каждый добытчик сразу направился к своему личному ресурсу. В качестве команды каждый добытчик получит COM_EXTRACT, которая принимает координаты ресурса и его тип как аргумент.
Очень важно то, что какая бы команда не была текущей, алгоритм обработки команд всегда проверяет, присутствует ли у команды целевой объект. Например, команды типа Маскировка (включить невидимость) не имеют целевого объекта, так как они выполняются "на себе" и при этом совершенно точно никуда не нужно двигаться. Но многие команды типа Атака или Добыча ресурса выполняют действие над целью. Это означает, что цель должна быть в пределах досягаемости. Действие над целью всегда выполняется через анимацию Воздействие, которая может создавать для этой цели объект типа Снаряд, либо наносить удар самостоятельно (киркой по минералу). В первом случае достаточно приблизиться на расстояние выстрела (distance = радиус видимости). Во втором случае, очевидно, что объект должен подойти к цели вплотную (distance = 1). Действия по командам обрабатываются только после завершения очередной анимации - анимации не должны резко прерываться. Обработчик команд всегда проверяет, находится ли цель в пределах расстояния distance. Если нет, то обработчик команд будет всегда выполнять движение к цели, т.е. юнит потихоньку пошагает в нужном направлении. Вызов обработчика команд будет происходить с периодичностью в один шаг (имеется в виду переход с одной клетки карты на другую). Опять же уточняю, что движение к цели не входит в саму команду Добыча ресурса, а является частью алгоритма по обработке любой команды.
Команда COM_EXTRACT состоит из 4-х фаз:
- Фаза START_EXTRACTION Фаза начинается с INITIALIZATION. Инициализация фазы всегда вызывается до того, как обработчик команд начинает движение к цели - это позволяет подменить цель. Первым делом проверяем, есть ли уже в руках какой-то ресурс. Если есть, то переходим на фазу DELIVER_RESOURCE, т.е. ресурс сначала необходимо отнести в Хранилище. Если ресурса в руках нет, то можно добывать. Для этого нужно проверить аргументы команды. Если по указанным координатам ресурс отсутствует, то это говорит о том, что он уже добыт (возможно, что другим добытчиком). В этом случае, требуется произвести поиск другого ресурса, преимущественно того же типа, который указан в аргументах команды. Такой поиск не просто просматривает сами минералы, но и определяет факт того, что какой-то добытчик уже трудится над данным ресурсом. Т.е. выбирается именно свободный для добычи ресурс. Кроме того выполняется тестовое построение пути до выбранного ресурса с учетом того, что другие добытчики также являются препятствием на пути. Результатом выполнения инициализации фазы START_EXTRACTION является выбор ресурса, который можно добывать в настоящий момент. Обычно добытчик находится на некотором расстоянии от ресурса, поэтому нужно подойти вплотную. Обработчик команд автоматически проверяет расстояние до цели и начинает движение, если это требуется. При каждом изменении координат юнита для фазы START_EXTRACTION вызывается функция TEST, которая проверяет на то, что ресурс остается доступным для добычи. Если TEST вернула отрицательный ответ, то для фазы запускается функция FIND, которая ищет новый доступный ресурс и вся команда Добывать ресурс перезапускается с самого начала, но с новыми координатами ресурса. Если добытчик сумел добраться до ресурса, то для фазы START_EXTRACTION выполняется функция ACTION, которая, правда, ничего не делает, а лишь передает управление на следующую фазу.
- Фаза WAIT_EXTRACTION Задача этой фазы - это дожидаться, когда ресурс станет доступен для добычи. Особенно эта фаза актуальна для Работников, так как они постоянно носят ресурсы в Хранилище, позволяя другим Работникам занять их место у ресурса. Фаза имеет функцию ACTION, которая и осуществляет все эти проверки. Если ресурс пока занят для добычи, то функция включит ожидание на пару секунд, после чего будет выполнена повторная проверка на доступность ресурса к добыче. Если после ожидания добывать по прежнему нельзя, то осуществляется поиск другого доступного ресурса на максимально близком расстоянии и управление передается фазе START_EXTRACTION. Если ресурс освободился или изначально не был занят, то управление сразу передается следующей фазе, которая уже и осуществляет добычу. В случае, если ресурс за время ожидания оказался полностью добытым, то вызывается поиск нового ресурса и управление опять передается на START_EXTRACTION.
-
Фаза EXTRACT
Представляет из себя непосредственную добычу ресурса.
Сначала выполняется INITIALIZATION, которая делает для добытчика текущим Воздействие типа Добыча ресурса.
Функция ACTION осуществляет процесс добычи, которая вызывает на выполнение действие Добыча ресурса. Все типы Воздействий обрабатываются одним и тем же алгоритмом, поэтому само осуществление Воздействия не имеет никакого отношения к команде Добыча ресурса. Однако, мне кажется, что дополнительные объяснения по этому поводу позволят связать то, что я говорил уже про стадии Воздействия (Подготовка в Воздействию, Воздействие, Перезарядка оружия, Пауза между Воздействиями, Выход из Воздействия) с логикой выполнения команд.
Выполнение любого Воздействия требует разворота к цели лицом. Для Работника тут всё достаточно просто - у него Воздействие осуществляется при помощи одной анимации (машем киркой). Однако ИТР или добытчик Онимодов имеет несколько дополнительных анимаций для выполнения Воздействия.
Для начала ИТР должен закрепиться около ресурса - визуально он как бы приседает на землю для чего используется анимация типа Подготовка к воздействию. И только после этого включается анимация основного Воздействия, которая визуально выглядит как тыканье механической клешней в ресурс. Обе эти анимации (присесть и добывать) в редакторе ресурсов настраиваются так, что их прерывание игроком без выполнения переходной анимации невозможно, т.е. если игрок захочет управлять ИТР-ом, то тот сначала запустит анимацию, которая визуально выглядит как "открепление от ресурса", т.е. ИТР сначала выйдет из присевшего состояния.
Функция TEST позволяет определить 2 момента:
- Не добыт ли еще ресурс полностью ?
- Может хватит добывать и пора отнести добычу в Хранилище ?
- Фаза DELIVER_RESOURCE Задача этой фазы - доставить собранные добытчиком ресурсы в Хранилище. Как это не странно, данная фаза во многом аналогична фазе EXTRACT, так как, практически, эта фаза вместо Воздействия типа Добыча ресурса использует Воздействие под названием Доставка ресурса. Как я уже говорил в начале, типов Воздействий в редакторе ресурсов достаточно много и каждое уникальное действие над другим объектом требует собственный тип Воздействия. Сюда же, естественно, относятся и все виды магии, которые существуют в игре. Опять же сначала выполняется инициализация фазы через функцию INITIALIZATION, которая делает для добытчика текущим Воздействие типа Доставка ресурса. Далее необходимо определиться с Хранилищем, куда рациональнее всего доставить ресурс. Хранилищ на карте может быть много, поэтому надо выбрать самое ближайшее. Однако производить этот поиск каждый раз нерационально, так как Хранилища не перемещаются по карте и, стало быть, достаточно выбрать Хранилище один раз, запомнить его и далее носить ресурс именно туда. Собственно, я почти так и делаю, только если вдруг появляется еще одно Хранилище, то я сбрасываю найденное ранее Хранилище у всех добытчиков на карте, чтобы поиск был произведен повторно. Для большей интеллектуальности можно выполнять новый поиск еще и периодически, так как в теории добытчик может очень сильно удалиться от первоначального места добычи. Функция ACTION выполняет действие Доставка ресурса. Для Работника здесь опять же всё относительно просто, так как в качестве цели выступает Хранилище, а у Работника анимация Доставки ресурса не создает никаких Снарядов, то он должен подойти вплотную к Хранилищу, поэтому он топает до него ножками. У Онимодов ИТР поступает значительно более интересно. Его анимация по доставке ресурса содержит создание Снаряда, но этот снаряд не разрушает цель, а обладает Воздействием типа Доставка ресурса, т.е. ударив в Хранилище он перебросит ресурс своей команде. Это избавляет от необходимости каждый раз бегать до Хранилища и обратно. Само Воздействие имеет несколько стадий. Сначала происходит подготовка к Воздействию (ИТР погружает в землю трубу), затем выполняется анимация Воздействия, которая кидает в Хранилище невидимый снаряд, летящий с очень высокой скоростью. Снаряд же обладает интересной функцией, которую я ранее не упоминал, он прикрепляется к цели, практически, происходит что-то типа посадки в Транспорт, т.е. Снаряд как бы попадает внутрь Хранилища, как будто в дот. У Хранилища заранее определены 6 посадочных мест, куда могут прикрепляться "пассажиры", и Снаряд прикрепляется к свободному месту и тут же запускает анимацию, которая на Хранилище наблюдается в виде вылезшей из земли трубы. Визуально вся схема выглядит так, что ИТР запустил трубу под землю, а она вылезла на Хранилище. Труба на Хранилище выполнив анимацию просто умирает, так как её анимация заканчивается действием Смерть. Затем ИТР выполняет анимацию Выход из воздействия, т.е. затягивает обратно трубу. Всё, ресурс доставлен. Естественно, что все анимации ИТР на стадии доставки настроены так, что игрок не сможет мгновенно получить контроль над юнитом - ИТР сначала вытащит трубу из земли, а потом уже сможет куда-то поехать. Далее управление опять передается фазе START_EXTRACTION, но так как ИТР уже готов к добыче, т.е. находится рядом с ресурсом и уже в закопанном состоянии, то фазы START_EXTRACTION и WAIT_EXTRACTION в 99,99% случае будут пропущены и сразу начнется фаза EXTRACT. Исключение произойдет в случае, когда Хранилище находится уже слишком далеко и Снаряд просто не долетает до цели. В этом случае анимации отработают свою задачу следующим образом: Так как расстояние слишком велико, то необходимо подойти к цели поближе, а ИТР в этот момент находится визуально в "присевшем" состоянии. Обработчик команд велит ему шевелить колёсами в сторону Хранилища, но из анимации добычи нужно сначала выйти, поэтому сначала выполнится анимация, которая визуально открепляет ИТР от ресурса. Теперь можно ехать и ИТР направится к Хранилищу. Подъехав на достаточное расстояние ему нужно будет выполнить Воздействие типа Доставка ресурса, но у неё есть подготовка, поэтому он сначала выпустит трубу. После того, как ресурс будет доставлен, управление передастся на фазу START_EXTRACTION, но так как до ресурса теперь далеко, то опять придется к нему ехать.
Приведенный пример имел целью продемонстрировать механизмы лежащие в основе игровой механики буквально "в двух словах". Мне хотелось, чтобы та информация, которую я приводил насчет возможностей редактора ресурсов была как-то связана с конкретными действиями юнитов. Обратите внимание, что команда COM_EXTRACT и для Работника, и для ИТР является полностью одинаковой с точки зрения кода. Но настройки анимаций юнитов в редакторе ресурсов делают процесс добычи ресурса абсолютно разным. Именно поэтому важно иметь широкие возможности по настройке анимаций. Под каждое действие, которое можно выполнить должна существовать своя команда. При этом, например, команды COM_GO (идти в указанную точку), COM_DESTROY (уничтожить конкретную цель), COM_ATTACK (идти в указанную точку, уничтожая всех на своем пути), COM_RUN (отбежать от атакующего врага, так как нет возможности защищаться) и COM_LEAVE (отойти с места, где будет построено здание) являются, по сути, разными. В моем случае в коде я наблюдаю около 50 команд, которые и обеспечивают функционирование всей игры. Также обратите внимание, что есть даже такая милая команда как COM_CANCEL, которая просто отменяет все другие команды. Зачем она нужна ? Дело в том, что в конце статьи я кратко опишу сеть, а по сети должны уметь передаваться любые действия, которые выполняет игрок, так как эти действия должны быть повторены один в один на другом компьютере.
Поиск пути
Юниты должны уметь перемещаться по карте и находить эффективный маршрут в сложных случаях. Этому вопросу должно быть уделено достаточное внимание и время. На начальной стадии я бы рекомендовал сделать любое простейшее решение, лишь бы можно было тестировать создаваемых художниками юнитов. Реальный же алгоритм поиска пути понадобится в момент создания AI, когда компьютер должен будет иметь возможность построить путь в любой самой витиеватой ситуации. Без нормального алгоритма поиска пути AI не сможет действовать эффективно, кроме каких-то исключительных случаев, когда игровое поле представляет из себя "голую степь".
Итак, у нас более менее отлажена игровая механика, юниты умеют выполнять предписанные им действия, которые они получают при помощи мыши, при этом программа не вываливается через каждые 2 минуты. Следующая стадия - это создание эффективного алгоритма поиска пути. В настоящий момент в Интернете присутствует немало советов на эту тему, я не могу сказать, насколько хороши или плохи эти методы, так как в своё время изобрел для себя собственное и, на мой взгляд, очень эффективное решение по этому поводу. Вполне возможно, что оно примерно повторяет какой-то опубликованный впоследствии алгоритм, так как решение действительно достаточно изящное. Но, как я уже сказал, все решения, которые я показываю в данной статье на тот момент я генерировал своей головой самостоятельно.
Подробная статья по моему методу поиска пути в классических RTS имеется на моем сайте: http://astralax.ru/articles/pathway. Помимо, собственно, самого алгоритма поиска, там описан процесс управления этим алгоритмом. Этот раздел касается таких проблем, как столкновение юнитов между собой во время движения, следованию за другим юнитом (цель постоянно меняет своё положение) и прочих не таких уж и очевидных моментов. Статья писалась лет 10 назад и лично для меня имела целью "не забыть" о собственном решении. Дублировать эту информацию здесь особого смысла я не вижу, особенно учитывая, что этой информации и так достаточно много.
AI (он же Искусственный интеллект)
Давайте сначала разберемся с тем, что же такое AI. Лично я для себя определяю AI как алгоритм, который заменяет собою действия живого игрока, которые тот выполняет при помощи мыши и клавиатуры. Теперь обратите внимание вот на что... если в вашей игре юниты не совсем тупые, то они могут самостоятельно принимать некоторые решения и выполнять их без помощи игрока. К таким решениям в моем случае относятся следующие действия:
- Воины, замечая врага в пределах видимости, должны самостоятельно вступать в бой. Более того, если какой-то воин из отряда ввязался в драку, то "коллеги по оружию" не должны стоят и равнодушно "курить" чуть в сторонке от места действия - они обязаны тоже вступить в бой.
- Если юнит умеет лечить, то он должен автоматически вылечивать раненных воинов. Это особенно эффективно, если отдать приказ лекарю следовать за каким-нибудь воином из отряда - слоняясь вслед за отрядом Лекарь будет лечить каждого, кто в этом нуждается.
- Работник, стоящий без дела, будет автоматически чинить раненное здание или помогать строить новое строение. В моем случае, Работник даже анализирует "степень поломанности" зданий и может прервать текущий ремонт и начать чинить здание, которое больше в этом нуждается. Это качество может быть очень полезно, когда Работники "суетятся" между несколькими Защитными башнями, которые одновременно находятся под обстрелом. Усиленный ремонт должен производиться у той башни, которая вот-вот готова рухнуть. Дополнительно алгоритм следит за тем, чтобы распределение Работников по "строительным объектам" было разумным, а не по принципу "все силы на одно здание".
- Добытчик ресурсов должен автоматически находить рядом следующий ресурс, если текущий закончился. Если такого вида ресурса рядом нет, но есть другие виды ресурсов, то нужно посмотреть каких ресурсов у команды меньше и добывать именно этот вид ресурса.
- Юниты, которые обладают магией, должны самостоятельно следить за тем, чтобы не помереть понапрасну. Практически, когда у мага вычитается жизнь, то он проверяет количество оставшейся жизни и, если её уже критически мало, то логично рассуждает, что игрок, скорее всего, не успеет никак отреагировать. В этом случае "ничегонеделание" равносильно тому, что маг просто будет убит не нанеся противнику никакого урона, поэтому маг применяет магию самостоятельно.
- Юнит, который может включить режим маскировки (невидимость), автоматически включит его, если по нему нанесен удар, а он не находится в поле зрения Определителя (юнита, который размаскировывает невидимых врагов).
- Юниты умеют автоматически отходить в сторону, если они мешают возведению какого-то строения.
- С некоторым натягом сюда же можно отнести и умение юнита следовать за подвижной целью, т.е. если одному юниту приказать следовать за другим, то тот будет ходить за ним везде как привязанный. При этом "привязанный" юнит вполне может расстреливать врагов или осуществлять лечение дружественных юнитов, а закончив это дело снова бежать за "целью".
Как видно из приведенных примеров, все эти действия не требуют вмешательства игрока. Соответственно, моя собственная формулировка о том, что "AI - это алгоритм, который заменяет собою действия живого игрока, которые тот выполняет при помощи мыши и клавиатуры" не соответствует данным примерам. А значит, все эти примеры не имеют к AI никакого отношения. Лично для себя я в своё время дал этим действиям название "Инстинкты". Инстинкты юнитов - важнейшая часть игры, так как они снимают с игрока заботу о рутинных действия, за которыми часто просто невозможно уследить. И именно от качества имеющихся Инстинктов зависит общая комфортность игры. Инстинктом обладает конкретный юнит, AI же управляет группами юнитов и выполняет глобальные задачи типа "Основать новую базы для добычи ресурсов".
Причины проявления Инстинктов в игре человек часто видит с точностью до наоборот. Например, когда Работник бежит чинить здание, то создается впечатление, что это является инициативой самого Работника. На деле же горящее Здание "зовёт на помощь" в небольшой радиусе от себя всех юнитов, которые имеют в своем арсенале Воздействие типа Ремонт. То же самое касается и ситуации, когда к воюющему воину подбегают на помощь рядомстоящие воины - это реакция на призыв "Помоги-и-и-те! Убива-а-а-а-ют!".
Несмотря на то, что, по моему мнению, Инстинкты не имеют к AI никакого отношения, я поместил их небольшое описание в раздел с AI, так как для многих такое размещение покажется вполне ожидаемым. Но, на мой взгляд, Инстинкты относятся к общей игровой механике, т.е. по уму их надо было описывать немного выше по тексту. А игровая механика на момент написания AI должна уже полностью функционировать включая раздел об Инстинктах юнитов. Инстинкты должны работать одинаково как для команды, которой управляет живой игрок, так и для команды, которой управляет AI. В своих решениях AI не должен опускаться на уровень Инстинктов, его задачи должны звучать примерно так "сформировать отряд и отправить его в атаку по таким-то координатам", а то, что этот отряд будет уже самостоятельно крушить всех на своем пути - это уже задача Инстинктов.
Итак вернемся непосредственно к AI. В моем случае для каждой команды, которой управляет компьютер, обработчик AI вызывается примерно 1 раз в секунду. AI руководствуется в своих действиях следующими глобальными принципами:
- Есть команды врагов и есть команды союзников. Соответственно, врагов надо уничтожать, а союзникам приходить на помощь в трудную минуту.
- Какая-то из команд врагов назначается основной целью для нападения, но периодически этот враг может меняться.
- У AI есть понятие База, которое является ключевым. База является, по сути, скоплением из нескольких строений на карте. У каждой базы есть свои границы - прямоугольник, который занимает данная база. Если произошло взаимопроникновение двух баз, то AI производит их объединение. На каждой базе строится собственная оборона, а любой юнит является привязанным к какой-то базе, до момента, пока он не получил какое-то задание.
- Основные задачи, которые выполняет AI:
- Развитие
- Нападение
- Защита
- Поиск противника
- Определение невидимых воинов врага с помощью Определителя
Если быть точным, то интеллект умеет выполнять 15 видов задач. Самое интересное - это конечно задачи, которые относятся к нападению. Всего интеллект умеет атаковать 6-ю разными способами:
- Простая атака. Отряд собирается рядом с целью атаки, но там где его не видно и затем атакует.
- Осадная атака. Чем-то напоминает простую, но отряд, расположившись рядом с целью не бежит сразу атаковать, а ждет пока будет накоплено достаточно сил. Силы постепенно подтягиваются и когда их достаточно, происходит атака. Побочный эффект от осадной атаки это то, что параллельно происходит блокада иногда единственного входа в лагерь противника. Также AI может во время осады выкатить вперед Катапульту и начать расстреливать издалека Защитные башни противника, основные силы при этом стоят позади Катапульты и ждут реакции осаждаемого.
- Уничтожение конкретной цели. Иногда некоторые юниты врага (обычно танки), расположены так, что они кромсают силы атакующих без каких-либо серьезных потерь со своей стороны. Этот момент отслеживается и периодически интеллект предпринимает карательные рейды конкретно против этих юнитов. Для этого используется летающий транспорт, с которого воины высаживаются прямо на целевого юнита.
- Диверсионная атака. Карательный рейд в сердце базы противника. Для этого применяются Транспорты, которые высаживают воинов за линией обороны врага. В основном, атакуются Хранилища с большим скоплением работников. Второй вариант этой атаки – это высадка воинов на ближайший склон, откуда можно хорошо постреливать вниз и куда враг не может подняться.
- Магическая диверсия. Может исполняться только либо Шаманом (от Ботсваны), либо Научным модулем (от Онимодов). Исполнители начинают просто околачиваться рядом с лагерем противника и иногда покидывают магию в зазевавшегося вражеского воина.
- Суператака. Это простая атака, которая делается совместно. Перед ней происходит небольшое затишье – накопление сил. Далее все AI, находящиеся в одном союзе совершают нападение одновременно.
Интеллект запоминает для себя количество воинов, которым делается попытка уничтожить конкретную цель. И поэтому не будет повторно атаковать 4-мя воинами то, что не удалось уничтожить 8-ю.
Коротко о защите. Союзные AI старательно заступаются друг за друга, более того, часто один из AI (если их не менее 3-ех) вообще не атакует (только диверсиями и суператакой), зато он всегда полон сил и готов помочь своему уничтожаемому союзнику.
Кроме того, часть воинов, которая производится, не идет в атаку, а остается для защиты баз. Каждая база всегда обставляется защитными башнями.
Коротко о развитии. Существует три стадии развития. На начальной стадии строятся несколько казарм для пехоты и начинается вторая стадия. В этом режиме здания не строятся, а происходит создание воинов и попытка разнести противника сходу. Из-за этого человек должен с первой минуты заниматься обороной, иначе проиграет в течении 10 минут. На третьей стадии происходит уже полноценное развитие. Развитие сильно зависит от того, можно ли до противника добраться пешком или только по воздуху.
Есть вариант, когда стадии развития могут быть пропущены, тогда сразу происходит полноценное развитие.
Интеллект следит за тем, чтобы использовать добытые ресурсы на все сто и старательно достраивает новые казармы, чтобы ускорить производство воинов.
Некоторые воины имеют приоритет при производстве – это те, кто могут легко уничтожить башню противника (танк, вертолет, катапульта, стрекоза).
Интеллект, также умеет перебрасывать юнитов с одной базы на другую. Например, избыток работников-добытчиков с одной базы будет переброшен туда, где их не хватает.
При строительстве зданий отслеживается момент, чтобы не был заткнут какой-нибудь узкий проход.
Поиск противника тоже важная задача, так как AI атакует только те вражеские Здания, которые были найдены. Поэтому по карте постоянно слоняются юниты, которые просто заняты поиском.
Определение невидимых воинов врага с помощью Определителя – это очень нужная задача. Её суть – подвести Определитель к невидимому врагу, чтобы он стал видим и, соответственно, уязвим.
Теперь, когда даны общие пояснения, можно попробовать заняться разбором конкретных задач, которые выполняет AI.
Класс AI в моем случае называется Intellect. Объект этого класса создается для каждой команды, которой управляет компьютер. Класс базы называется Camp, и каждый Intellect содержит массив существующих баз.
Класс Camp содержит всю информацию по базе, в том числе и её размеры на карте. Также присутствует информация об объектах, которые можно произвести, и о зданиях, которые не заняты производством. Основное назначение базы - это добывать ресурсы и производить объекты.
У Intellect имеется массив задач, выполнение которых он отслеживает ежесекундно. Базовым классом для всех задач является IntellectCommand. У базового класса задачи имеются следующие поля для отслеживания состояния юнитов, которые выполняют данную задачу:
- Количество объектов.
- Общее количество воинов (объектов, которые могут сражаться).
- Количество пеших воинов.
- Количество летающих воинов.
- Количество добытчиков.
- Количество ремонтников.
- Количество Определителей.
- Количество Транспортов.
- Количество пассажиров (количество юнитов, которое находится в Транспорте).
- Общая боевая мощь.
- Прямоугольник, который занимают юниты на карте.
- Признак того, что в настоящий момент юниты сражаются.
- Признак того, что все юниты летающие.
У других задач могут быть какие-то дополнительные свойства, например, задачи INTELLECT_WAIT, INTELLECT_BUILD, INTELLECT_GUARD, INTELLECT_CREATE дополнительно формируют информацию о состоянии базы, к которой они принадлежат.
Логика установки всех этих свойств у задач достаточно проста. Имеется функция virtual void IntellectCommand::Clear(), которая обнуляет всю эту информацию для задачи. У баз для той же цели имеется функция void Camp::ClearCamp(). Перед тем как обрабатывать юнитов, происходит вызов Clear() для всех имеющихся задач и ClearCamp() для всех имеющихся баз. Когда обрабатываются юниты, то каждый из них регистрирует себя в той задаче, которую он выполняет через virtual void IntellectCommand::Register(Object* obj). Таким образом происходит постоянно обновление данных о составе участников каждой задачи. Например, если юнит находится в Транспорте, то он, соответственно, увеличивает "количество пассажиров", Добытчики увеличивают количество Добытчиков и т.д. Т.е. вся эта информация для каждой задачи всегда поддерживается в актуальном состоянии.
Всего существует 15 видов задач:
- INTELLECT_WAIT - ожидание
- INTELLECT_BUILDING - тоже ожидание, но для Зданий
- INTELLECT_GUARD - патрулирование на базе
- INTELLECT_CREATE - создание юнита или здания
- INTELLECT_ATTACH - присоедние одной задачи к другой
- INTELLECT_ATTACK - простая атака или супер-атака
- INTELLECT_SIEGE - осадная атака
- INTELLECT_SPY - поиск вражеских зданий
- INTELLECT_DETECT - определение невидимого противника с помощью Определителя
- INTELLECT_DIVERSION_TARGET - атака на конкретного юнита (высадка на него десанта с Транспорта)
- INTELLECT_DIVERSION_MAGIC - атака магом
- INTELLECT_DIVERSION_ATTACK - атака базы врага (высадка в область добычи десанта с транспорта)
- INTELLECT_PROTECT - защита своей или союзной базы
- INTELLECT_POINT - строительство новой базы по стратегическим соображениям (без добычи ресурсов)
- INTELLECT_STORE - строительство новой базы для добычи ресурсов
Первые 4 задачи не могут выполняться вне базы, кроме того, они могут выполняться только в единственном экземпляре, поэтому каждый Camp содержит массив из этих 4-х задач для INTELLECT_WAIT, INTELLECT_BUILD, INTELLECT_GUARD, INTELLECT_CREATE.
Остальные задачи могут существовать в любом количестве внутри самого Intellect. Любой юнит, которым управляет AI содержит в своем составе индекс выполняемой задачи и индекс базы, к которой он принадлежит. Эти индексы всегда позволяют получить указатель на выполняемую задачу IntellectCommand*. Если юнит не выполняет одну из первых четырех задач, то это означает, что он не принадлежит ни к одной базе, так как он выполняет задачу, которая требует покинуть базу. Такие задачи находятся в массиве задач Intellect-а.
Когда AI создает любую задачу, то сначала в ней нет ни одного участника. И первым делом необходимо собрать группу, которая эту задачу выполнит. AI знает, какие характеристики группы должны быть для выполнения поставленной задачи, например, чтобы основать новую базу нужны юниты, которые умеют строить, и небольшой отряд сопровождения. Если в настоящий момент воинов нет, то от них можно и отказаться, но строители обязательно должны быть, иначе выполнять задачу не имеет смысла. Именно для этого в задаче регистрируется состав исполнительской группы.
Задача управляет своими участниками с помощью функции virtual void IntellectCommand::GetCommand(Object* obj, Command* com); Практически, она может вернуть для любого юнита любую команду, которая вскоре будет выполнена обработчиком команд.
Также задача следит за тем, что при текущем составе участников она еще может быть выполнена. Это делается через функцию virtual void IntellectCommand::IsTaskExecuted(). Если задачу нельзя выполнить, то AI создаст для этих юнитов задачу INTELLECT_ATTACH, которая отправит их на какую-либо базу и включит в её состав.
Сложные задачи при комплектации группы исполнителей должны иметь возможность проверять на соответствие юнитов потребностям задачи. Для этого существует функция virtual void IntellectCommand::IfAttach(Object* obj). Эта функция сообщает, принят ли юнит в состав группы или нет. Также она сообщает о том, что группа исполнителей полностью укомплектована, что, практически, означает, что теперь задачу можно выполнять. Зачем всё так усложнять ? Дело в том, что некоторые задачи могут требовать конкретного юнита для своего выполнения, например, нужно выполнить диверсионную атаку на базу противника INTELLECT_DIVERSION_ATTACK, но такая атака требует Транспорт, которого сейчас может просто не быть в наличии. Тогда это означает, что задача просто не может быть выполнена мгновенно, а значит нужно подождать, пока Транспорт будет произведен.
Попробуем хоть немного разобрать конкретные задачи.
INTELLECT_WAIT
class IntellectCommandWait : public IntellectCommand
Задача звучит как Ожидание, но это не совсем так потому что Добытчики при этой задаче добывают ресурсы, а Воины следят за тем, чтобы не мешать движению и самостоятельно перемещаются, если они находятся рядом с препятствием ближе чем на 2 клетки, также Воины иногда переходят с места на место случайным образом в пределах базы. База постоянно производит юнитов и 25% от производимых воинов автоматически получает задачу INTELLECT_GUARD. Остальным присваивается задача INTELLECT_WAIT. Обе эти задачи привязаны к базе и существуют для этой базы в единственном экземпляре.
Задача INTELLECT_WAIT дополнительно выполняет очень важную функцию - она поставляет юнитов для других задач. В составе объекта IntellectCommandWait имеется массив, куда другие задачи могут добавлять запрос на передачу им юнитов из задачи INTELLECT_WAIT. При выполнении virtual void IntellectCommandWait::Register(Object* obj) каждый юнит проверяется на соответствие задачам, которые разместили свои запросы на формирование группы. Для проверки вызывается виртуальная функция задачи IfAttach(obj). Подходящий юнит перебрасывается из задачи INTELLECT_WAIT в другую задачу.
INTELLECT_GUARD
class IntellectCommandGuard : public IntellectCommand
Задача для Воинов, которые охраняют базу. Такие Воины постоянно перемещаются в пределах базы. Также охранники никогда не ходят в атаку, но всегда участвуют в защите собственной базы или базы союзника, что осуществляется с помощью задачи INTELLECT_PROTECT. Охранники также могут использоваться для супер-атаки INTELLECT_ATTACK, но никогда для обычного нападения.
INTELLECT_CREATE
class IntellectCommandCreate : public IntellectCommand
Задача выполняет все виды производства, т.е. строительство зданий, производство юнитов и исследований.
Здания Ботсваны строятся классически, т.е. Работники закладывают фундамент и стучат молотками. Здания Онимодов присылаются с орбитальной станции - визуально это выглядит как падение контейнера с неба. Но здания можно сбрасывать только на некотором расстоянии от Радара, в который умеет трансформироваться ИТР. На деле алгоритм построения Зданий у Онимодов примерно такой: выбирается место под Здание и, если оно в пределах действия Радара, то начинается "строительство". Иначе выбирается место под Радар, чтобы тот накрывал своим действием то место, куда планировалось поставить Здание. Далее опять происходит попытка выбрать место под Здание. Алгоритм выбора места под Здание предполагает, что будет более рационально располагать одинаковые Здания рядом. Это касается большинства Зданий, но не касается оборонительных башен. У них алгоритм выбора местоположения такой: делается несколько попыток выбрать место под башню случайным образом. В результате принимается тот вариант, который оказался, по возможности, на периметре базы или даже чуть дальше. Как ни странно, в моем случае этот подход часто дает очень неплохие результаты, так как Башни оказываются "на входе".
При производстве юнитов в качестве производителей выступают Здания. Здесь просто выбирается подходящее здание, которое ничем не занято, и ей дается команда на производство. Некоторые юниты имеют приоритет, так как считаются более ценными для атаки, например, это касается Вертолетов.
Апгрейды выполняются периодически по аналогии с юнитами, но также обладают приоритетами. Это связано с тем, что есть очень полезные апгрейды, а есть, скажем так, средненькие. Например, за Ботсвану очень эффективно играть Арбалетчиками, но для их производства требуется апгрейд, и логично именно этот апгрейд сделать в первую очередь.
На самом деле INTELLECT_CREATE - это очень сложная задача. Например, если просто постоянно производить дешевых воинов, то может получиться так, что на более сильных воинов ресурсов никогда не хватит, поэтому AI умеет "копить средства" на конкретный объект, т.е. если решено построить Катапульту, то всё остальное производиться не будет, а будет происходить накопление средств именно на Катапульту.
В область действия INTELLECT_CREATE также попадает и задача по построению другой базы. Правда, это частный случай производства, который, практически, приведет к созданию задачи INTELLECT_STORE, под которую будет выделена отдельная группа юнитов.
AI понимает, что для успешного ведения военных действий, надо всегда наготове иметь несколько Транспортов и несколько Определителей. Поэтому AI всегда заранее создает этих юнитов, так как они могут потребоваться в любой момент.
AI также избегает ситуации, когда у него образуется избыток средств - он начинает строить больше Зданий, чтобы быстрее производить воинов.
INTELLECT_SPY
class IntellectCommandSpy : public IntellectCommand
Задача выполняет поиск Зданий противника. Из имеющихся на базе юнитов выбирается один, который просто бродит по карте туда-сюда. Обычно этот юнит быстро помирает, но тогда AI посылает на разведку другого юнита. При выборе исполнителя учитываются его врожденные шпионские качества, например, очевидно, что если среди кандидатов имеется Витум (невидимый летающий юнит, а по совместительству Определитель), то выбрать его в разведчики куда разумнее, чем какого-нибудь Лучника.
INTELLECT_DETECT
class IntellectCommandDetect : public IntellectCommand
Задача по определению невидимого противника. Выполняется всегда одним и тем же типом юнита, который является Определителем. Для Ботсваны - это Витум, а для Онимодов - это Научный модуль. Оба они, естественно, перемещаются по воздуху, иначе навряд ли успевали бы к месту боя. Задача INTELLECT_DETECT создается когда имеется незадействованный Определитель и нашего воина атакует невидимка. Задача Определителя просто оказаться рядом. Также Определители достаточно сообразительны, чтобы не лезть на передовую - обычно они "парят" чуть позади, а если попадают в поле зрения противника, то стараются немного отступить.
Все остальные виды задач обладают одной важной особенностью - все они требуют прибыть в какую-то произвольную точку на карте. Это сильно отличается от ситуации, когда происходит выполнение задачи в пределах одной базы, так как там юниты гарантированно могут дойти до цели. В случае же, когда целевая координата может быть где угодно, ситуация резко меняется. В чем же заключается отличие ?
- Может случиться так, что до целевой точки можно добраться только по воздуху.
- Даже если можно добраться пешком, то вполне может возникнуть ситуация, что это очень нерационально, так как придется обходить всё вокруг.
- Чтобы перевезти пеших юнитов по воздуху нужен Транспорт, которого может вообще не быть в текущий момент. Или может быть так, что отряд не помещается в один Транспорт и нужно несколько Транспортов, а их не хватает.
- Может быть ситуация, когда все юниты в группе перемещаются по воздуху, тогда транспорты не нужны. А может быть так, что будут и пешие, и летающие юниты.
Все эти задачи начинаются с того, что решают вопрос, каким же образом нужно добираться до цели. Если нужны Транспорты, то их нужно включить в группу, но после того, как группа будет высажена в нужном месте, Транспорты должны быть отправлены назад на базу.
Базовым классом для всех этих задач является класс IntellectCommandComplex (class IntellectCommandComplex : public IntellectCommand). Этот класс обладает модулями, которые сменяют один другого по ходу выполнения задачи. Когда очередной модуль полностью выполнен, то управление переходит на следующий модуль.
int k_modul; // количество модулей в команде int t_modul; // текущий исполняемый модуль ModulIntellectCommand* m_modul[8]; // список модулей
Класс ModulIntellectCommand как раз является модулем. И у этого класса есть свои виртуальные функции virtual void ModulIntellectCommand::GetCommand(Object* obj, Command* com); и virtual void ModulIntellectCommand::IsTaskExecuted(). Для IntellectCommandComplex эти функции выглядят так: void IntellectCommandComplex::GetCommand(Object* obj,Command* com) { m_modul[t_modul]->GetCommand(obj, com); } bool IntellectCommandComplex::IsTaskExecuted() { return m_modul[t_modul]->IsTaskExecuted(); } Т.е. практически, принятие решений выполняет текущий модуль.
Дополнительно задача IntellectCommandComplex собирает при помощи функции Register() информацию о всех Транспортах, выполняющих данную задачу. Транспорты могут потребоваться для перевозки юнитов.
int k_transport; // количество транспортов в списке данной задачи Object* m_transport[25]; // список транспортов у данной задачи
INTELLECT_ATTACH
class IntellectCommandAttach : public IntellectCommandComplex
IntellectCommandAttach::IntellectCommandAttach(int num):IntellectCommandComplex(num) { type=INTELLECT_ATTACH; m_modul[0]=new ModulIntellectCommandBeginMove(this); m_modul[1]=new ModulIntellectCommandEndMove(this); m_modul[2]=new ModulIntellectCommandAttach(this); k_modul=3; }
Задача по присоединению юнитов к базе или другой задаче. Несмотря на неброское название несёт в себе несколько полезных целей:
- Возврат на базу юнитов, которые по каким-то причинам не могут выполнить задачу (например, пока шли строить новую базу, потеряли юнитов, которые умеют возводить Хранилище).
- Переброска юнитов между базами. Например, на одной базе добыты почти все ресурсы и добытчики сачкуют - их можно направить на новую базу, где есть что добывать. Или можно перебрасывать с базы на базу Определители и Транспорты.
- При выполнении осадной атаки INTELLECT_SIEGE происходит постоянное подтаскивание сил в "точку сбора". Прибывающие отряды объединяются с основным.
- Транспорты, после высадки юнитов в требуемом месте, возвращаются на базу.
- У Ботсваны есть такая интересная особенность - летающие юниты Стрекозы и Комары в состоянии "при смерти" выводятся из боя и спасаются бегством к ближайшей базе. Это связано с тем, что у Ботсваны есть в арсенале такой юнит как Лекарь, который запросто вылечивает раненных воинов.
Практически, задача INTELLECT_ATTACH состоит из 3-х модулей:
- ModulIntellectCommandBeginMove - если для перемещения в целевую точку не требуется Транспорт, то этот модуль просто дает юниту команду COM_WAIT, чтобы тот остановился. Иначе дается команда COM_IN, которая в качестве цели использует один из Транспортов, которые берутся из массива m_transport один за другим, чтобы юниты рассаживались по разным Транспортам. Когда все юниты будут рассажены по Транспортам модуль будет завершен.
- ModulIntellectCommandEndMove - модуль, который осуществляет доставку группы по координатам цели. Естественно, что отравлять всех юнитов в одну и ту же координату не совсем логично, поэтому для каждого выбирается своя позиция совсем рядом с целевыми координатами. Перемещение юнитов осуществляется командой COM_ATTACK, если же используются Транспорты, то они летят к цели, использую команду COM_GO. Достигнув цели, Транспорты осуществляют высадку пассажиров. По ходу движения к цели Транспорт может оказаться под обстрелом, в этом случае, если жизни остается критически мало, то Транспорт высаживает пассажиров немедленно, понимая, что иначе не выживет никто.
- ModulIntellectCommandAttach - модуль заменяет текущую задачу на задачу INTELLECT_WAIT и передает юнитов под контроль базы. Также есть вариант с присоединением к другой задаче, когда все юниты меняют текущую задачу на новую.
INTELLECT_ATTACK
class IntellectCommandAttack : public IntellectCommandComplex
IntellectCommandAttack::IntellectCommandAttack(int num):IntellectCommandComplex(num) { type=INTELLECT_ATTACK; m_modul[0]=new ModulIntellectCommandBeginMove(this); m_modul[1]=new ModulIntellectCommandEndMove(this); m_modul[2]=new ModulIntellectCommandInvisible(this); m_modul[3]=new ModulIntellectCommandAttack(this); k_modul=4; }
Обычная атака или супер-атака выполняются одной и той же задачей INTELLECT_ATTACK. Супер-атака отличается только тем, что в ней участвуют все имеющиеся силы.
Основные модули ModulIntellectCommandBeginMove и ModulIntellectCommandEndMove, естественно, делают то же самое, что и для команды INTELLECT_ATTACH, т.е. осуществляют доставку группы на место.
Модуль ModulIntellectCommandInvisible является полезным только для расы Ботсвана, так как его задача сводится к тому, чтобы сделать невидимыми всех юнитов перед началом атаки. Способностью делать юнитов невидимымми обладает Лекарь, однако для этого нужно выполнить специальный Апгрейд. В общем пока есть возможность колдовать невидимость и в группе есть видимые юниты Лекарь будет применять на них заклинание. После этого модуль завершается.
Модуль ModulIntellectCommandAttack является, собственно, самой атакой. Т.е. юниты получают команду COM_ATTACK. Целью для атаки всегда является вражеское здание, точнее не само здание, а координаты, по которым это здание находится. Если в атаке участвуют маги, то для них команда COM_ATTACK не имеет смысла, поэтому каждому магу выделяется воин, за которым тот следует. Если этот воин погибает, то выбирается другой воин. Т.е. маги следуют за воинами и всегда оказываются позади, при этом механизм Инстинктов позволяют лечить любого раненного воина из отряда. Если воинов больше не осталось, то маги пускаются наутёк к ближайшей Базе используя задачу INTELLECT_ATTACH. Если группа достигает целевых координат, то это, практически, означает, что здание уничтожено, в этом случае автоматически происходит выбор новой цели на небольшом расстоянии.
Модуль ModulIntellectCommandAttack также умеет действовать принципиально иначе. Если в группе есть дальнобойные юниты типа Катапульта, то эти юниты могут быть выдвинуты вперед, чтобы осуществлять обстрел вражеских Зданий. Основная часть воинов при этом остается прямо за Катапультами, но в случае угрозы Катапультам тут же атакуют.
INTELLECT_SIEGE
class IntellectCommandSiege : public IntellectCommandComplex
IntellectCommandSiege::IntellectCommandSiege(int num):IntellectCommandComplex(num) { type=INTELLECT_SIEGE; siege=true; m_modul[0]=new ModulIntellectCommandBeginMove(this); m_modul[1]=new ModulIntellectCommandEndMove(this); m_modul[2]=new ModulIntellectCommandSiegeWait(this); m_modul[3]=new ModulIntellectCommandInvisible(this); m_modul[4]=new ModulIntellectCommandAttack(this); k_modul=5; }
Осадная атака во многом похожа на обычную атаку, но имеет дополнительный модуль ModulIntellectCommandSiegeWait, который обеспечивает накопление сил перед лагерем противника. AI умеет выполнять задачу INTELLECT_SIEGE только в единственном экземпляре. Координаты точки накопления сил известны, поэтому периодически AI направляет туда подкрепление, используя задачу INTELLECT_ATTACH.
INTELLECT_PROTECT
class IntellectCommandProtect : public IntellectCommandComplex
IntellectCommandProtect::IntellectCommandProtect(int num):IntellectCommandComplex(num) { type=INTELLECT_PROTECT; m_modul[0]=new ModulIntellectCommandProtectWaitTarget(this); m_modul[1]=new ModulIntellectCommandBeginMove(this); m_modul[2]=new ModulIntellectCommandEndMove(this); m_modul[3]=new ModulIntellectCommandProtect(this); k_modul=4; }
Для защиты собственной Базы по ней постоянно бродят воины, выполняющие задачу INTELLECT_GUARD. При атаке на собственную Базу её защита выполняется немедленно. Если же атакуется другая база, то для начала нужно убедиться, что там дела обстоят действительно плачевно, а первый признак - это то, что враг сломал какое-то Здание. Задача INTELLECT_PROTECT набирать себе группу не только из задачи INTELLECT_WAIT, но и из задачи INTELLECT_GUARD. В общем-то INTELLECT_GUARD как раз и нужна для того, чтобы всегда имелись воины для защиты. AI также умеет защищать и Базы союзной команды, но "порог чувствительности" к таким действиям значительно ниже, чем к спасению собственной Базы - потребуется, чтобы база союзника потеряла несколько зданий.
Модуль ModulIntellectCommandProtectWaitTarget выбирает цель. В отличии от задач нападения, которые всегда направлены на атаку Зданий, задача INTELLECT_PROTECT рассчитана на уничтожение юнитов. После выбора цели происходит стандартная доставка группы по нужным координатам.
Модуль ModulIntellectCommandProtect во многом похож на модуль ModulIntellectCommandAttack. Но задача помнит вражеского юнита, который является целью и проверяет на его существование. Если он убит, то задача переходит к первому модулю, т.е. снова начинается поиск цели. Естественно, что при движении к цели, воины заодно кромсают всех, кто попадется на пути.
AI обладает то ли врожденной мстительностью, то ли некоторой прозорливостью. Если удается отбить атаку на свою базу, то он рассуждает так, что у атакующего кончились силы и надо срочно произвести контратаку.
Все решения, которые принимает AI - это, по сути, лишь вероятности. Он может сделать так, а может и наоборот. В конечном счете всё зависит от генератора случайных чисел и результата, который тот выдаст в конкретный момент.
Сеть
Принцип организации сети в классической RTS содержит в себе один парадокс. С одной стороны, сетевое взаимодействие устроено очень просто, с другой - в этой простоте содержится очень большая "мелкая пакость", которая в состоянии очень сильно попортить нервную систему.
В отличии от многих других типов сетевых игр для RTS не требуется никакой сервер, так как каждый компьютер выполняет все действия самостоятельно. Т.е. по сети между компьютерами нужно передавать только команды, которые игрок вводит с клавиатуры или мыши. Всё действия, которые попадают в игру от пользователя должны быть первым делом преобразованы в команды (COM_GO, COM_ATTACK и т.д.). Именно эти команды будут передаваться позже по сети и ничего другого передаваться по сети не должно. Справедливости ради нужно заметить, что существуют специальные команды, которые позволяют управлять самой игрой, например, это выбор политических союзов или отправка сообщения в чат. Но всё это тоже команды, которые передаются точно также как и все остальные, только они не относятся к юнитам.
Каждый компьютер создает в своей игре массивы, куда будут приходить команды от всех других компьютеров. Т.е. если у нас 3 сетевых игрока, то у каждого из них создается по 3 массива для получения сетевых команд. Также есть еще один временный массив для сбора собственных команд. Есть договоренность, что обмен командами по сети будет происходить, например, каждые 10 игровых тактов. Эта вынужденная задержка называется Латентность. Для чего это нужно ? В течение 10 тактов происходит сбор команд в собственный массив, параллельно ожидается приход команд от других компьютеров по сети. От собственного компьютера, естественно, ждать команд не надо - надо просто копировать временный массив в сетевой массив соответствующий собственному компьютеру и эти же данные нужно будет передать по сети всем другим компьютерам. После того, как от каждого компьютера будут получены данные соответствующие текущему такту можно переходить к их выполнению. В результате образуется небольшая задержка, потому что в реальности команды выполняются не в момент, когда игрок щелкнул мышкой, а каждые 10 тактов. К слову говоря, в одиночной игре используется такой же принцип, только там Латентность устанавливается в 1 такт и отсутствует рассылка по сети. Если от какого-то компьютера не пришли данные, то вся деятельность замирает и происходит ожидание. При этом "отстающему" компьютеру посылается сигнал о том, что данные от него не получены и требуется повторная пересылка. Если данные не приходят очень долго, то выводится надпись "Связь с игроком потеряна", а чуть позже потерянный компьютер отсоединяется от сетевой игры.
Чисто технически для RTS сетевая игра вообще ничем не отличается от одиночной, так как вся разница заключается в том, что вместо одного массива с командами будет несколько, каждый из которых выполняется так, будто все игроки сидят за одним компьютером.
Для игры по локальной сети в сервере вообще нет особого смысла. Однако, если требуется обеспечить игру через Интернет, то сервер должен выполнять функцию того места, где встречаются игроки и создается игра, а также выполняется присоединение к ней. Теоретически, после запуска игры сервер можно было бы вообще отсоединить, однако в последнее время активно используются разнообразные средства защиты Интернет-соединения. Например, если используется NAT, то прямое взаимодействие игроков, которые оба сидят за NAT-ом на практике малоосуществимо. А так как сервер NAT-а не имеет, то все игроки могут без проблем соединяться с ним. В этом случае сервер будет играть роль "передаточного звена", который не выполняет никаких расчетов, но тупо перебрасывает через себя все данные между игроками.
Теперь пришло время сказать пару слов о той самой мелкой пакости, которая сильно портит радость от ненужности писать полноценный сервер. Дело в том, что при таком подходе все компьютеры должны всегда делать всё одинаково, т.е. именно один в один. Никакие мелкие расхождения в происходящем недопустимы, так как если какой-то из компьютеров начнет делать что-то не так как другие, то через пару минут ситуация будет отличаться настолько, что на одном компьютере просто не будет тех юнитов, которые еще присутствуют на другом. Это очень быстро ведет к полному краху сетевой игры, вылетанию программы и нервной реакции игрока, с вспоминанием множества "тёплых" слов в адрес разработчика. Это явление называется Рассинхронизация сети, и та же Age of Empires 1 раньше вполне могла вывести на эту тему сообщение и прервать сетевую игру.
Причина такого поведения - это мелкие ошибки, которые могут приводить к этой рассинхронизации. Короче говоря, если какой-нибудь компьютер начал выполнять что-то чуть-чуть не совсем то, что остальные, то очень скоро на компьютерах будут происходить совершенно разные вещи. Ужас ситуации в том, что этот момент "расхождения" никак невозможно засечь, а вместо этого программа вдруг вылетает на одном из компьютеров. Но... это может произойти минуты через 2 после того, как ошибка произошла в реальности. Короче говоря, сложнее ошибок я в своей жизни не встречал. Поиск таких багов происходит разве что методом "внезапного озарения", но никак ни через логику. Чтобы хоть как-то воспроизводить эти ошибки в RTS появился так называемый реплэй, т.е. мультик, записанный с игры - его, естественно, превратили в фичу для игрока (можно просмотреть свою сыгранную игру), но на деле его основная задача - это хоть как-то попытаться повторить ошибку рассинхронизации сетевой игры. Обычно это помогает мало, но иногда-таки помогает.
Чтобы обнаружить рассинхронизацию сети как можно быстрее, а не через пару минут, можно вместе с командами передавать по сети, например, сумму случайных чисел, которые были выработаны на компьютере с момента прошлой передачи сетевых команд. Так как случайные числа обычно вырабатываются в подобных играх постоянно, то несоответствие в происходящем сразу же будет выявлено по этим случайным числам. Однако это опять же не показывает то место, где произошла проблема.
Приведу пример ошибки, о которой я говорил. На карте загорелось дерево и количество древесины в нем уменьшается по общему счетчику (когда древесины становится 0, то дерево исчезает с карты). Счетчик при запуске приложения сбрасывается в 0, но каждый такт он увеличивается на 1, например, до 10-и, и когда достигает 10-и снова сбрасывается в 0, а из всех горящих деревьев вычитается одна древесина. Но в программе допущена ошибка - счетчик не сбрасывается в 0 в момент запуска сетевой игры. Что мы получаем... в первый раз игроки будут играть вполне нормально, так как при старте программы у всех в счетчик записался 0, но... допустим игра закончена и создана повторная сетевая игра. И тут окажется, что этот счетчик имеет разные значения на разных компьютерах, потому что они не могут одновременно завершить игру (кто-то отсоединился раньше, а кто-то позже, а счетчик исправно отсчитывал такты). При повторном запуске сетевой игры, практически, на разных компьютерах дерево "догорит" в разное время, что приведет как минимум к тому, что юниты построят разные пути при движении к цели - на одном компьютере дерево уже отсутствует, а на другом его нужно обойти. Всё, приехали... через пару минут будет полный крах игры. Запускаем записанный реплей, чтобы понять причину, но... теперь всё работает исправно (никакого расхождения в реплэе не будет - а в нем нужно тоже контролировать выполнение через сохраненные суммы случайных чисел), так как при старте программы счетчик успешно инициализировался нулем. Дальше можно начинать молиться.
В реальности подобных ошибок может быть несколько (и в случае со сложным проектом этого не избежать), что будет приводить к тому, что сеть будет "разваливаться" через 10-15 минут игры.
Коротко об авторах и о проделанной работе
Название: Земля онимодов / Onimod land Жанр: RTS - стратегия в реальном времени Программирование: Алексей Седов (он же Odin_KG) - Технологии программирования: C++, Assembler, DirectDraw - Системные требования: Windows XP, Windows 7 (сеть в 7-ке не работает, но в остальном всё в порядке) Графика: Роман Коваленко, Константин Иванов - Стиль графики: 2D изометрия (используется примерно 12 тысяч спрайтов) Музыка: Дмитрий Голов Способ разработки: Энтузиазм Время разработки: 1998 - 2005 Страница для скачивания: Официальный сайт игры Способ распространения: бесплатно
Стартовые условия и их последствия (лирический раздел)
Страна, где я родился и вырос, называется "россия". Итак, на дворе 1998 год. Окинем беглым взглядом окружающую действительность тогдашей IT-индустрии. Intel выпустил процессор, аж, на 233 МГц, Blizzard уже известен благодаря "Diablo 1" и "Warcraft 2", а Microsoft отличилась с помощью "Age of Empires 1". Почти на всех ПК установлен "Windows 95", который некоторые уже пытаются проапгрейдить до "Windows 98". На барахолках всей необъятной родины тоннами расходятся пиратские диски, очень популярные в народе из-за низкой стоимости. Лицензионный софт может где-то и существует (например, в Москве), но, как минимум, люди даже не понимают разницу между пираткой и лицензией. Еще один важный уточняющий момент - в стране сильнейший экономический кризис, так как доллар в течение недели вырос с 6-и до 30-и рублей, соответственно, стоимость всего, что относится к "железу" выросла пропорционально доллару. Экономика лежит...
Итак...
Группа отчаянных ребят численностью 3 человека, в этот непростой для родины час, решила попробовать себя в качестве разработчиков игр. Среди них 1 человек, который надеется, что он более-менее умеет программировать (это автор статьи) и 2 человека, которые искренне любят рисовать, в том числе и на компьютере.
У каждого из нас был собственный ПК, но характеристики соответствовали моменту. Например, моя конфигурация была такая: Intel 200-MMX, ОЗУ - 32 Мб, Видеокарта - 2 Мб, HDD - 4Гб. К слову говоря, такое чудо обошлось мне до кризиса примерно в 9000 рублей, что соответствовало 1500$ (при средней зарплате по стране в районе 700 рублей). У художников ПК были куда более скромными.
В общем-то всё, что у нас было - это желание + "современная техника". Хотя я забыл, пожалуй, о самом важном - мы были хорошими друзьями, которые знали друг друга со школы и прошли вместе также и ВУЗ. И как оказалось этот фактор оказался в результате очень важным.
Пожалуй, стоит упомянуть о том, чего не было. А не было в то время достаточной информации. Именно той информации, от которой сейчас интернет просто захлебывается. Не было, кстати, и самого интернета, точнее он был, но у очень ограниченного числа людей, а скорость по Dual-Up модему максимально достигала 5 Кбайт/сек, к которым прилагались частые обрывы связи.
Всё это означает буквально следующее, что нет примеров программирования, нет алгоритмов, нет видеоуроков по обучению тому, как работать с 3D Studio Max, нет готовых движков, наконец. Не у кого спросить совета на форуме. Готовых ответов на вопросы, которые сейчас сходу "гуглятся", тем более нет. Т.е. есть только разработчик и его задача, которую необходимо решать самостоятельно и рассчитывая только на свои силы.
Вообще стартанули мы достаточно неплохо. Я потихоньку реализовывал то, о чем написано в статье, а художники предоставляли мне графику. Основная часть графики делалась в 3D Studio Max, для анимации юнитов использовался плагин Character Studio. К счастью, удалось каким-то чудом раздобыть книжку по этому плагину и более-менее разобраться. Существовала такая проблема, что 3D Max не мог толком работать на таком слабом железе, которое еще и управлялось с помощью Windows 95. При старте 3D Max возмущался, что ОЗУ должно быть минимум 48 Мб, а на компьютере его было только 16. Надо ли объяснять, с какой скоростью всё это функционировало. Исправление любой мелочи могло занять много часов. Дополнительно 3D Max очень любил схлопываться без видимых причин.
Однако художникам очень понравился мой редактор ресурсов, хотя он и выглядел как "инструмент для внутреннего пользования". Сначала они долго не понимали, что собирать готовых юнитов можно и без меня, а после произошедшего "озарения" на этот счёт у них появился новый виток энтузиазма. Константин стал периодически притаскивать мне не просто набор спрайтов, а уже вполне "живого" юнита. На тот момент сама возможность вставлять в игру объекты без участия программиста казалась чем-то фантастическим.
Ситуация резко ухудшилась, когда мы решили, что уже есть что показать издателю. Издатели были в Москве, и мы просто купили билеты на поезд и отправились показывать свою работу. На тот момент прошло уже года полтора, с начала всего этого действа. AI, конечно, не было еще и в помине, не было хорошего алгоритма поиска пути, но очень многое уже работало. Нам надо было выяснить, хоть что-то конкретное по поводу дальнейших перспектив нашего проекта. И перспективы были выяснены... и они в общем-то, практически, похоронили данный проект. Юмор ситуации был в том, что вроде бы издатели хотели издавать, но, когда нам озвучили условия, стало ясно, что это всё очень напоминает рабство. После возвращения из Москвы, художники, практически, прекратили работу над игрой - и в общем-то это было во многом правильное решение. Периодически, правда, мне что-то дорисовывали, но все уже понимали, что этот проект не может принести каких-то денег, которые не выглядели бы унизительно на фоне потраченных на него усилий.
Твёрдое мнение о том, как в россии в то время производилось "издание" у меня появилось уже позже, когда мне более-менее удалось получить в голове целостную картину, которая, собственно, всё объясняла. Не могу настаивать на её абсолютной истинности, но после того, как у меня появилось это объяснение, все вопросы были сняты. Пожалуй, я потрачу еще минутку на расстановку всего по своим местам.
Как я уже сказал - страна была просто завалена пиратскими дисками. За этими дисками "челноки" ездили в Москву, где можно было приобрести диски оптом, чтобы потом реализовать их на барахолке в родном городе. Цены на диски не сильно отличались от цен на пустые болванки, и сама цена в пределах одного рынка всегда была одинакова для любого продукта, который содержался на этом диске. Теперь вернемся к изданию лицензионных продуктов. По моему мнению, оно выглядело примерно так: у издателя в Москве было несколько своих точек сбыта (ларьков), через которые издатель реализовывал лицензионные диски. При этом выставить какую-то реальную цену на лицензионный софт было навряд ли возможно, так как: а) Процветают пиратские барахолки. б) Население страны просто не может покупать софт за нормальную цену, особенно, когда можно купить пиратский диск задёшево.
Поэтому стоимость лицензионных дисков отличалась от пиратских далеко не в десятки раз. Причем издатель должен был сначала печатать диски, тем самым производя затраты.
Чтобы не тянуть время, я озвучу стандартные условия, которые предлагались разработчику: Издатель всегда требовал эксклюзивные имущественные права. В случае, практически, законченной игры разработчику предлагался аванс 10 000 $ + роялти. Роялти (они же процент с продаж) по максимуму могли быть 25%, чаще предлагали от 15% до 20%. Проценты, в теории, должны были выплачиваться после того, как издатель погасит свои предварительные затраты, включая аванс. По поводу роялти, сразу хочется сказать, что, по имеющимся у меня данным, их никому не платили, так как для издателя в этом просто нет особого смысла. Надо понимать, что на тот момент идея о том, что российского издателя можно как-то проверить была полным мифом. Я сомневаюсь, что это легко сделать сейчас, но, по крайней мере, в настоящий момент об этом хотя бы можно всерьез мечтать. А так... в стране, где бизнес всячески укрывает налоги, рассчитывать на то, что удастся выявить какие-то записи о реальном состоянии продаж, на мой взгляд, наивно.Итак, нам как и всем предложили эти 10 000 $. Уточню, что это не в день, а ВСЕГО, а то казуальщики, которые попадают в топ на AppStore могут неправильно понять. Т.е. разработчик ставился в ситуацию, когда он должен как-то выдавать достаточно серьезную по трудозатратам игру, но предполагаемая оплата никак не соответствует этим трудозатратам. В общем-то это напрямую связано с тем, что издатель, по сути, никаким издателем не являлся, т.е. он не выполнял свою основную функцию - он не мог ничего в реальности продать, так как продажа по своим "пятнадцати ларькам" в одном городе (при наличии эксклюзивных прав) - это не издание. О каком-то продвижении вообще речи не было, хотя в общем-то это тоже обязанность издателя. Насчет продвижения в "Буке" ("Бука" в тот момент была крупным российским издателем) нам сказали, что они своим подписчикам рассылают новость на e-mail - численность подписчиков на тот момент была 11 000 человек. С "Букой" вообще было достаточно весело, так как ихняя девочка-менеджер на полном серьезе поведала мне, что они Blizzard-у за издание тоже заплатили аванс 10 000 $. В общем вся эта ахинея, на мой взгляд, была направлена на то, чтобы убедить разработчика в том, что "иначе нельзя" и надо "пока потерпеть", а зато "потом" и им тоже "что-то начнет перепадать". Точных сроков на этот "потом" не называлось, но делался намёк на то, что, возможно, следующий проект, будет рассматриваться на других условиях.
Чуть позже мне случайно стала доступна очень занимательная информация. Оказывается, некоторые очень известные пиратские компании типа того же Fargus-а, на деле являлись дочерними компаниями российских издателей. И это простое объяснение кажется мне более чем логичным, так как лично для меня оно не оставляет больше никаких вопросов.
А еще в позапрошлом году познакомился я в Интернете с одним человеком, который, как потом выяснилось, в те самые времена занимался локализацией игр (говоря по простому, они брали заказы на перевод игры с английского языка на русский). Он мне пожаловался на то, что им за локализацию платили всего-то 5000 $. Т.е. перевести игру стоило всего лишь в 2 раза дешевле, чем её сделать. И сейчас я понимаю, почему было именно так - в россии было хорошо налажено только пиратство, но не было, по сути, никакого издания. И оно так и не появилось, а вместо него появился высокоскоростной Интернет, который открыл доступ к западным торговым площадкам.
Что же случилось с игрой дальше ? Последние годы я доделывал эту работу в гордом одиночестве. Почему я не бросил эту затею ? Видимо потому что, я понимал, что второй раз я такое уже не вытяну, а вытянуть очень хотелось, тем более, что было уже очень много сделано. Да и, честно говоря, я терпеть не могу сдаваться и бросать начатое на полпути - это что-то вроде "дара" и "проклятия" в одном лице. Также у меня были серьезные просчеты в сроках, т.е. я предполагал, что удастся закончить гораздо быстрее, но на деле мелочи сжирали всё время. А чем дальше я продвигался вперед, тем обиднее было бы признаться себе, что "я не смог". Вот так я и дошел до конца. Под конец вынужден был кое-что дорисовывать самостоятельно - меню, например. Рисовать я, в общем-то, не умею и не особо стремлюсь научиться, но деваться было некуда.
Музыку к игре взял у своего приятеля, который жил в соседнем подъезде - он мне дал штук 15 мелодий на выбор. Также как и многие творческие люди в россии, он не имел никакой возможности продавать своё творчество, поэтому его мелодии были, естественно, никому неизвестны. По-моему, он их набирал нотами в какой-то музыкальном редакторе, но так как у него имелось музыкальное образование и абсолютный слух, то, на мой взгляд, произведения вполне достойны внимания. Чувствуется симпатия автора к произведениям Жан-Мишеля Жарра, а мне "космическая" тематика очень даже подходила.
Остальная озвучка была сделана, прямо скажем, не очень профессионально, так как уже не было никаких сил этим заниматься - надо было заканчивать эту эпопею и начинать жить дальше. В основном все фразы проговорили в обычный микрофон мои знакомые, а "вопли смерти" я озвучивал сам. В результате эта озвучка многим не понравилась, хотя лично меня она совершенно не раздражала.
Чем всё это закончилось... да в общем-то ничем. Я бросил делать игры, так как понял, что рассчитывать на кого-то кроме самого себя я уже не смогу, а игры, как минимум, требуют оформления. Примерно год после завершения не мог себя заставить ничего делать вообще, так как просто не видел смысла что-то начинать. Потом всё же пришёл в себя и решил сделать небольшой редактор спецэффектов, скорее, из "спортивного интереса", чем ради какой-то выгоды. Впоследствии этот редактор получил название "Magic Particles". После того, как я показал редактор игроделам, мне стали предлагать добавить в него API, чтобы спецэффекты можно было воспроизводить из собственных программ. Двигалось всё это очень сложно, но в результате у меня появились продажи. Игроделы периодически покупали мою технологию в основном для казуальных игр: http://astralax.ru/titles
В настоящий момент я продолжаю заниматься своим Magic Particles, и к моему удивлению обнаружил, что он стал уже крупнее, чем проект Земля онимодов, который, как я думал, является моим теоретическим пределом.
Благодарю за проявленное упорство в чтении данной статьи, а, также, за терпение к моему "литературному таланту". С уважением, Алексей Седов 26 марта 2016
У этой статьи существует продолжение.Обновление После того как в 2016 году эта статья вышла на крупном российском ресурсе, я впервые получил хоть какое-то ощущение того, что эта работа была проделана не зря. В результате я решил дать этой игре второй шанс. Почти год я приводил её в порядок, добился того, чтобы игра работала на Windows 8/10, полностью переделал GUI, заказал новое меню и озвучку, а также написал интернет-сервер. У игры появился официальный сайт, а также страница на Steam. Версия с моего официального сайта никак не связана с платформой Steam, но работает ничуть не хуже.