Мой быдлокод вас огорчает, часть 1
Автор: Сергей Владимирович Иванов
Статья для начинающих разработчиков о трудностях, возникающих даже в мизерных проектах из-за недостатка умения и опыта.
По просьбе нескольких трудящихся решил написать обзор на разработку своих старых продуктов.
Эта статья предназначена прежде всего для начинающих разработчиков, которые уже хотят сделать игру своей мечты, но еще не понимают, как это сделать и какими трудностями и препятствиями это обернется на практике.
Жизнь показывает, что наибольшим тиражом расходятся статьи, в которых автор указывает на свои многочисленные ошибки, так как желающие посмеяться над чужой глупостью и безграмотностью найдутся везде. Что ж, шесть лет назад этих ошибок я совершил предостаточно и был чертовски наивен и глуп.
Игра DOOM "Ы"
Для начала, определим список возможностей игры. Игра написана на Turbo Pascal, имела анимированную заставку и примитивное меню, сделанное в модуле Graph. Сам игровой процесс проходил в текстовом режиме, с использованием псевдографики. В игре было представлено 4 вида противников, два босса, 6 видов оружия, 10 уровней с разрушаемым окружением.
Теперь по порядку. Разработка игры велась в течение десяти дней командой из 4-х человек. Я был лидером команды, и на мне висела реализация всего игрового процесса, по сути - игровая механика. Второй товарищ занимался дизайном уровней и псевдографикой (переделывал шрифты под игровые объекты), а также интерфейсом. Третьему было поручено заниматься волновым алгоритмом поиска пути, которым пользовались враги игрока. Четвертый занимался созданием графической заставки и меню игры. Все замечательно, но были нюансы.
Для начала, в первые несколько дней разработки нашего шедевра мы работали, по сути, над разными проектами. Кто-то хотел чуть ли не RPG, а я, к примеру, хотел сделать что-то вроде Crimsonland, только на Pascal с использованием модуля Graph. Дальше изображения вращающегося абы как человечка в центре экрана дело у меня не дошло, ровно как и у остальных членов "команды". Отсюда - нарушение №1: отсутствие концептуальной целостности проекта. Несмотря на смехотворный масштаб работ, уже на этом мы потеряли около трех дней из десяти, что немало. Начни мы разработку "DOOM Ы" сразу же, мы бы, возможно, успели перенести ее из текстового режима на "новый движок", используя модуль Graph.
После того, как более-менее определились с нашими ролями в разработке и с тем, что мы, собственно, разрабатываем - дела пошли в гору. Неосознанно нами использовалась модель наращивания программы - на любой стадии разработки в нее можно было поиграть, хотя в начале у нас была всего лишь маленькая кракозябра в центре экрана, которая при движении оставляла за собой след из таких же кракозябр - только спустя полчаса копания в своем быдлокоде я научился "заметать следы" за игроком.
Когда код поиска пути по волновому алгоритму был закончен и внедрен в игру, встала серьезная проблема производительности. Враги искали игрока, а каждый проход алгоритма занимал немало машинного времени. Сначала грешили на неэффективность реализации алгоритма, но после вычищения немногочисленных ошибок и попытки оптимизации у нас ничего не вышло.
Проблема была решена путем смены ролей - вместо того, чтобы каждый враг пускал "волну" в поисках игрока, мы пускали эту волну непосредственно от игрока каждый игровой цикл, а противники уже находили путь по заполненной матрице. В итоге производительность возросла в несколько раз и больше нас не беспокоила.
К слову говоря, в этой реализации волнового алгоритма был один баг, который не проявился в игре, но мог произойти - два врага могли слиться в одну точку. Это не было бы критической ошибкой и не испортило бы игровой процесс, но многое говорит о нашей недальновидности и неумении определять исключительные ситуации до того, как они возникнут. О возможности этого бага я догадался только сейчас, спустя шесть лет, во время написания этой статьи.
Разработка меню и заставки вплоть до заключительной стадии проекта велась обособленно, в виде отдельной программы, а не модуля, о чем мы позже пожалели. Поскольку никаких заглушек и прочих имитаторов передачи управления между игрой и меню не было, несколько часов ушло на вылавливание багов в собранной из двух исправно работающих модулей программы. При должном подходе к проектированию этой проблемы можно было бы избежать на корню. Итак, нарушение №2: отсутствие четких спецификаций.
Корректировка баланса в игре также заняла немало времени. Основных противников было четыре: зомби (самые медленные и дохлые), пинки (кусачие и средние по скорости), импы (средние по скорости, атакуют на расстоянии) и быстрые зомби а ля Half-Life 2, на введении которых настоял я. Как только загружался новый уровень, в нем на случайные свободные точки ставились монстры (функция Generate_*имя монстра*), после чего, опять-таки, в случайную точку ставился игрок (процедура Degenerate, что символизирует). Игрок по скорости был быстрее всех, но в ограниченном игровом пространстве быстро упирался в стенку или в толпень монстров, таким образом, сильно рассчитывать на свою скорость не мог. Быстрые зомби были тем мотивирующим фактором, который заставлял игрока искать укрытие и не давать ему расстреливать остальных, более медленных монстров. К моменту вычищения всей популяции быстрых зомби остальные монстры успевали подойти поближе к игроку, что не давало ему передышки и заставляло судорожно отстреливаться.
На одном из последних уровней (игрок получает пулемет) был неприятный баг, ломающий баланс - там имелся коридор, засев в который, можно было, не убирая пальца с клавиши стрельбы, перебить всех монстров. Во время тестирования игры мы это быстро просекли, и коридор забетонировали.
В целом, это было чертовски забавно и интересно - разрабатывать такую штуку, а потом в нее рубиться, хвастаясь, у кого меньше здоровья сгрызли окаянные демоны. Разрушаемое окружение тоже радовало - можно было сделать так, что каждая стенка на уровне (не считая границ) была разрушена почти до основания, а от деревьев остались одни пеньки.
Итого - около 2500 строк кода (до жути примитивного, надо отметить), и относительно безглючная поделка.
Леталка Respond
Леталку, которая у меня в размещалась почему-то в проектной папке "respond" (я не могу вспомнить, как, зачем и почему я назвал проект именно так), я сделал примерно за год до разработки DOOM "Ы". Но, в отличие от последней, она неоднократно обрабатывалась напильников впоследствии, поэтому завершенным проект можно считать, пожалуй, только сейчас. Поэтому обзор на ее разработку размещаю вторым.
Что имеем? Вы управляете красной сплющенной фигней в центре экрана. Вокруг летают десятки точно таких же красных и синих пепелацев, которые ведут между собой безжалостную войну. Стреляют в друг друга светлыми трассерами, садятся на хвост и пытаются сбросить противника с хвоста. При смерти взрываются облаком пламени, искр и дыма, а обугленный остов летающей табуретки падает вниз. Игровое пространство представлено бескрайним небом. Но одно "но" - вылетая за определенные границы, пепелац уничтожается. После падения (точнее, взрыва на определенной высоте) вновь встает в строй, респавнувшись в случайной точке. Конфетка, а не игра™!
Как же это непотребство было создано? Началось все примерно после окончания восьмого класса, я более-менее освоил капельку навыков программирования в среде 3D Game Studio 5.12, и пытался делать первые убогие потуги на осмысленные действия противника. Для начала я пытался освоить банальное переориентирование одного объекта в сторону другого с конечной угловой скоростью, и это мне удалось.
К слову, 3D Game Studio содержала вполне удобоваримый редактор уровней и убогий редактор моделей, редактора кода не имела (он появился позже), а центральным элементом этой системы был движок Acknex, избавивший меня в свое время от соблазна написания собственного движка с блэкджеком и шлюхами. В комплект входил набор template-скриптов из разряда "сделай убийцу Unreal 3 за три дня", который позволял начинающему не обремененному интеллектом разработчику делать игры, не отвлекаясь на такую мелочь, как программирование. Поигравшись с этим ужасом около месяца, я стал учить местный скриптовый язык.
Мой код того периода был не то что отвратительным - он был чудовищным. Имена функций наподобие Function1, отсутствие каких бы то ни было комментариев, названия функций и переменных транслитом, а где-то - на английском языке (гениальное название sbil_event - лишь один из примеров), то есть код был лишен и эстетики, и стилистической целостности. За такой кошмар надо бить сразу, так как поддерживать и отлаживать такое убожество решительно невозможно. Но я это делал, и на тот момент меня никто не бил, а жаль.
Ко всему прочему, на тот момент это был первый мой опыт создания взаимодействия между объектами игрового мира. До этого подобный опыт ограничивался отскакиванием мячика от стенки (а игровой движок осуществлял это почти без моей помощи).
Объектов в игре было немного, так что матрица взаимодействий была очень проста, но реализация этого взаимодействия породила немало проблем. Взаимодействие между объектами в движках Acknex осуществляется через event-функции - каждому объекту с помощью флагов указывается, на какие события реагировать, и назначается функция-обработчик, которая срабатывает при любом из этих событий. Поясню на примере.
// функция-обработчик function Test_Object_event() { if( event_type==event_entity){do_something( );} // обработка события event_entity if( event_type==event_block){do_something_else( );} // обработка события event_block nothing_to_do( );// сделать что-то общее для всех событий } // функция-действие для объекта action Test_Object { // назначение событий, на которые будет реагировать объект my.event_block=on; // столкновение с уровнем my.event_entity=on; // столкновение с другим объектом my.event=Test_Object_event; // назначение функции-обработчика }
Тут уже начинаются сложности. Движок предоставляет реагирование на БАЗОВЫЕ события, такие, как контакт с другим объектом. Но этот другой объект сам из себя может представлять все, что угодно - он может быть пулей, ракетой, трехмерным куском дыма, другим игроком или декалью на стенке. Поэтому каждый обработчик do_something() должен проверять, с каким именно объектом произошел контакт. Также, в характеристиках и поведенческих моделях объектов вроде декалей и частиц дыма должно быть указано, что они вообще ни с кем не контактируют - для упрощения обработки событий. В принципе, если научиться грамотно пользоваться этой системой, можно создавать взаимодействия любой сложности между объектами.
Что же касается самой сложности - она растет в квадратичном порядке в зависимости от количества типов объектов. Таким образом, упрощение матрицы взаимодействий (в частности, игнорирование одних объектов другими), начиная с некоторого количества, становится жизненно-необходимой мерой. Точно такой же жизненно-важной мерой становится грамотное планирование обработчиков событий - тогда рост числа возможных взаимодействий не приведет к существенному росту объема и сложности кода.
Но шесть лет назад я об этой ереси не знал и не задумывался, и лепил свои поделки без обдумывания. Поэтому при минимальной сложности матрицы взаимодействий (всего-то пять типов объектов, а начиналось с трех) было потрачено в десятки раз большее количество сил, чем того требовала ситуация. Вот эта матрица:
Матрица взаимодействия при контакте
Уровень | Самолёт | Ракета | |
Уровень | Нет событий | ||
Самолёт | Уничтожение самолёта | Уничтожение обоих самолётов | |
Ракета | Уничтожение ракеты | Уничтожение ракеты, уменьшение здоровья самолёта | Уничтожение обеих ракет |
Собственно взрывы ракет или самолетов описаны в самой поведенческой модели ракеты и самолета соответственно, по обнулению счетчика здоровья.
По совместительству, проект "respond" являлся первым проектом, в котором я начал активно использовать эффекты частиц. Частицы - это предельно простые двумерные объекты, которые, как правило, используются в очень больших количествах для создания искр, огня, дыма и тому подобного.
Но главные трудности проекта были в создании поведенческих моделей противника, и здесь же - самые забавные глюки и наиболее кривые решения.
Модель поведения противников постепенно наращивалась, начиная с простейшей - преследование противника. При тестировании этой модели был получен закономерный результат - два самолета, которые враждовали друг с другом, летели навстречу друг другу, стреляли в друг друга на встречных курсах и в итоге сталкивались и взрывались. Хотя героизм пилотов заслуживает уважения, для игрового процесса такая модель неприменима.
Для ликвидации этой ситуации я сделал вставку в код которая заставляла самолеты разворачиваться при достижении определенного расстояния до противника, а так же снижать или увеличивать скорость в зависимости от расстояния. Бой один на один двух "асов" с таким поведением был полностью симметричным, посему - совершенно неинтересным. Поэтому я решил разыграть карту командного боя "стенку на стенку", где симметрии не было.
Как только появились команды, встал вопрос выбора цели. В случае боя один на один можно было прямо указать на противника и радоваться. В командном бою этот способ неприменим. Поиск осуществлялся инструкцией scan_entity, что породило новую матрицу взаимодействия и новые обработчики событий. В итоге, если самолет был просканирован врагом, он отправлял ему указатель на себя.
К чему это решение привело? Поскольку инструкция scan_entity шла по всему списку объектов, именно последний враг в списке оказывался "личным врагом" одной из команд, для второй команды - точно так же. В итоге, десятеро гоняются за одним, потом за вторым, третьим... и тут вступил в силу другой дефект системы. Эти самые десять самолетов все время сближались друг с другом, так как преследовали одного и того же врага. В итоге, потери от столкновения с союзниками были примерно в 2 раза больше, чем от боя с врагами. Самое смешное было, когда побеждающая команда в количестве пяти самолетов преследует одного несчастного врага, и в итоге все пятеро падают одним огненным шаром вниз, а вражеская команда, соответственно, выигрывает.