- PVSM.RU - https://www.pvsm.ru -
Привет !
Эта статья продолжает цикл статей об игровом движке StalinGrad. В прошлой статье мы насиловали DHTML [1], а в этой — будем насиловать архитектуру и прототипы.
Кода практически не будет, поэтому, если вы не web разработчик, можете просто почитать про архитектуру и ООП. И таки да — про HTML5 тут снова ничего не будет, только DHTML-хардкор :)

Сразу короткое демо:
Пример 1 [2] — паралельные миры
Пример 2 [3] — один мир с разных камер
Пример 3 [4] — боты
Пример 4 [5] — нянкэт, облака и портальная пушка
Как-то мне захотелось написать квест, где мужик ходил бы по бару и мог разговаривать с разными людьми. А события и люди в этом баре — генерировались случайно, и каждый раз перед игроком появлялась новая цель.
Как сделать это хождение на JavaScript? В большинстве случаев это выглядит так: есть экран и персонаж, который ходит от одного конца экрана в другой.

Реализовать довольно просто. Но что если бар будет больше чем экран? Тогда можно придумать такую схему: есть экран — это DIV1 c позиционированием. Внутри этого лежит другой DIV2, который больше чем DIV1 и позиционируется относительного родителя. DIV2 — это и есть наш уровень. Все люди и объекты в баре лежат внутри DIV2 и позиционируются от его левого края.
Реализовать схему в принципе просто, но не нужно. Это абсолютно неправильный подход. Что если у нас в баре будет тысяча объектов? Мы получаем не масштабируемую основу, которая со временем может заставить нас страдать.
Я начал искать информацию о том, как делают 2D игры и перебрал кучу статей на Хабре про игры на HTML5. К сожалению, во всех найденных статьях, которые прочитал, не нашлось ни одного алгоритма или отсылки к архитектуре — только скрипты одноразовых игр. Этот факт меня опечалил, и я спросил парней на работе о том, что они думают. Мой коллега, Виталий Никитин, посоветовал прочитать книжку «Секреты разработки игр в macromedia Flash MX» (автор Jobe Makar). С этого все и началось.

Книга оказалась фантастической. Если пропускать все моменты про Flash, можно узнать очень много про алгоритмы и логику. По крайней мере, для меня, как человека ничего не знающего о движках и опыте разработчиков игр, книга была просто откровением.
С полученными знаниями, представление об играх расширилось и возникло острое желание написать игру с проработанным миром. Сначала я посмотрел на игровые движки на JavaScript, которые уже были на рынке. Обратил внимание на то, что у многих отсутствовала нормальная документация. Хардкорные варианты — тоже не нашлись. А ведь следуя логике Джоба Макара и опыту игростроя, уровень хардкора можно сделать запредельным.
Что я хотел? Марио, в котором можно будет убивать/насиловать/грабить караваны. Что нужно было сделать:
И что характерно — весь этот список уже реализован, и в данный момент возникло желание добавить ещё много фишек и реализовать их.
В какой-то момент, я осознал, что не могу написать игру. Эта штука становиться все больше и больше, но логической концовки ей — я не вижу. Тогда было решено описать все, выложить в интернет и посмотреть кому это интересно. Начав писать документацию — осознал, что многое работает не так, как надо. Да и API можно и нужно упрощать. Но давайте вернемся к сути и основным идеям.
Любая игра состоит из трех объектов.
Первый объект — это мир, в котором происходит действие. Мы можем помещать и удалять объекты из мира. Мир в свою очередь следит за всеми столкновениями, силами и событиями. Он также может оказывать дополнительное воздействие на объекты (например, задавать гравитацию).
Второй объект — это персонаж или персонажи, которые находятся в этом самом мире. И тут вовсе не обязательно, чтобы это был именно живой объект. Это может быть коробка, бочка, стенка, пуля, какой либо камень, либо что-то ещё.
В-третьих, камера. Камера нужна, чтобы смотреть на мир, т.к. предыдущие два объекта являются исключительно мат. абстракцией и никак не дают о себе знать.

Что дают нам эти три объекта? Возможность создавать огромное количество паралельных миров [2], смотреть на один мир из разных мест [3], запихивать в один мир очень много персонажей [4].

Какие проблемы решает создание разных миров? Мы можем вынести код на сервер и доделать режим игры по сети. По сути, мир — это массив и парочка методов, которые его обслуживают. Он не работает с DOM, а значит, ему безразлично, где существовать. Кроме того, генерируя миры, мы можем создавать игры с параллельными мирами, которые существуют одновременно.
Имея разные миры и карму, можно после обнуления жизней перекидывать персонажей из основного мира, в мир «Рай» или мир «Ад» и создать нелинейный сюжет. Кроме того, т.к. миры будут существовать паралельно, то возможно варианты развития событий, при которых персонажи одного мира телепортируются в другой мир и начинают там войну.
Архитектура, описанная выше — это ООП в чистом виде. Поэтому ядро движка это прототип на прототипе, повсеместная инкапсуляция и модульность. Если начать думать в таком формате, архитектура игр представляется совсем с другой стороны и возникает куча дополнительных вопросов.
Расширяемость
Идет парень, подбирает автомат и садится в машину. Другой парень стреляет в него из базуки. Кто от чего умрет? Как вообще они будут умирать?
Что такое погода?
Погода — это то, что крутится на заднем плане. Влияет ли она на игрока? Можно ли сказать что зона с ветром — это зона с гравитацией, которая тянет не вниз, а в бок? Стоит ли нам задавать свойство парусности разным объектам и домножать на них силу ветра? Как останавливать ветер препятствиями на его пути (например, стенкой), если он гравитация — то стенка его явно не остановит?
Правильный ответ на каждый из этих вопросов, может дать очень большую выгоду в перспективе. Решение должно быть максимально простым и универсальным, и тогда на его основе можно будет реализовать ещё несколько решений или добавить дополнительный функционал, о котором раньше вы даже не задумывались.
Например, оружие разбито на типы и по функционалу. Используя уже готовые методы и функции, на основе режима строительства была добавлена функция стрельбы патронами и снарядами. По сути, мы строим снаряд вместо кирпича перед персонажем и задаем снаряду начальную скорость.

На основе того, что мы можем задать размер камеры (не размер экрана, на который выводится изображение, а именно области отображения мира) очень легко была реализована функция масштабирования. А ссылка для таймера на отдельную переменную mdash; дала возможность добавить камере кнопку стоп/запустить [6] для остановки отрисовки мира.

Любой объект, который добавляется в мир, должен иметь обязательные стандартные свойства. Например: координаты, размер, ID, класс и тип, способ отрисовки. Это так называемые, стандартные свойства и они есть абсолютно у всех объектов. Далее, в зависимости от класса и типа объекта на него навешиваются дополнительные свойства и методы.
Например:
Чтобы создавать огромное число объектов с одинаковыми свойствами и прототипами — внутри движка есть фабрика объектов. Каждый объект, в зависимости от типа и имеет набор конфигов. Далее есть два массива — массив свойств и массив прототипов (на самом деле это не массивы, а тоже объекты, но это уже по части ООП в JavaScript).

Когда мы создаем объект, фабрика (согласно конфигу) наследует ему его пакет свойств и пакет прототипов. Таким образом, мы имеем что-то типа фабрики фабрик. Которая может собрать любой объект, задав ему любой комплект свойств и прототипов.
Так получилось, что оружие разделилось на три вида по типу стрельбы:
С зоной поражения.

В зависимости от дальности стрельбы, перед персонажем создается зона поражения. Далее находятся все объекты, которые попали в зону поражения. Из них выбирается объект, который ближе всего находится к персонажу. Когда жертва найдена — ей наносится ущерб, в зависимости от типа оружия.
Подобный алгоритм использует кирка. Пример можно посмотреть тут [7]
С созданием объекта снаряда.

Перед персонажем создается объект снаряда, у которого отключено воздействие гравитации. Снаряд летит и проверяет наличие столкновений с объектами. Если он столкнулся с персонажем — снаряд исчезает, а персонажу наносится урон.
Подобный алгоритм использует пушка. Пример можно посмотреть тут [8]
С проверкой свободного места.

Это особый вид стрельбы, необходим для режима строительства. При стрельбе создается блок материала и ищется свободное место около персонажа, куда можно его поставить.
Подобный алгоритм используется при строительстве. Пример можно посмотреть тут [9]
Как сделать оружие ближнего боя
Тут все просто — любые мечи, ножи, кирки работают по первому принципу (с зоной поражения)
Как сделать скорострельное и медленное оружие
Скорострельное оружие работает по первому принципу и наносит урон сразу. Медленное оружие (ракеты и т.п.) — по второму, и ждет столкновение снаряда.
Как сделать гравитационную пушку
Стрельба делается по первому типу, только вместо урона на все объекты накладывается дополнительное ускорение. Его направление зависит от режима стрельбы (либо к себе, либо от себя).
Пример гравитационной пушки можно посмотреть тут [10]

Как сделать портальную пушку
Точно так же, как и строительство блоков — по третьему принципу. Отличие только одно — пушка сохраняет ссылку на созданный портал, чтобы привязать его ко второму созданному порталу.
Пример портальной пушки можно посмотреть тут [5]
Как прокачать портальную пушку
Т.к. у нас возможно прокидывать порталы между двумя разными мирами, можно создать пушку, которая бы прокидывала свою жертву в другой мир, или на другой конец карты. Получится уже не совсем портальная пушка, а скорее пушка телепорт
Какие проблемы решает объект камеры? Т.к. Canvas работает не везде (перспектива прокачать играми электронные книги меня все ещё манит), я решил делать камеру, которая сможет отрисовать мир с помощью верстки. Наркомания скажете вы? Возможно, зато работает везде. В данный момент камера может рисовать картинками и div`ами с background`ом. Перевести все на canvas если заставит нужна — не проблема, т.к. для этого нужно всего лишь переписать несколько функций рендеринга (на самом деле тут, шутки ради, можно вообще сделать отрисовку текстом). Мой друг постоянно говорил мне о том, что без canvas все будет виснуть, т.к. шейдеры… бла… бла… бла… HTML5… и т.д. — но оказалось, что нет. В начале камера конечно висла, но проблема каждый раз была не в ней, а в алгоритмах обсчета мира.

Когда я показал схему работы одному коллеге на работе — он сказал, что камера должна сразу отрисовать весь мир, а не создавать и удалять постоянно DOM элементы. Это абсолютно неправильное понимание. Предположим, что в мире 40 объектов, а значит, у нас есть 40 DOM элементов для работы. Ну и ладно, это не много и браузер сможет их вывести без тормозов. А если у нас в мире тысяча объектов? Следуя такой логике, мы должны создать тысячу DOM элементов и страдать от тормозов (хотя на экране, в данный момент, все равно будет лишь штук 30, т.к. больше не влезет).
Камера — это объект мира. Она имеет размеры и координаты, и постоянно ищет столкновение других объектов с собой. Все кто с ней сталкиваются — попадают в массив видимых объектов и создаются на дисплее. Если объект выходит за рамки камеры — его представление уничтожается.
Рендеринг мира осуществляется в данный момент двумя способами — картинками и div`ами. Для обоих методов подойдет один дисплей в виде родительского div`а, относительно которого позиционируется вся верстка. В будущем можно добавить ещё рендеринг через canvas, но в данный момент особой необходимости в этом не вижу, т.к. есть более интересные задачи.
Ещё один интересный момент. Поскольку вся отрисовка игры mdash; это верстка, то любой верстальщик (даже с малым опытом работы) сможет легко исправить CSS файл и заменить картинки, тем самым придать объектам совершенно иной внешний вид. Например можно создать вязанный мир [11] (для сравнения обычный [8]).
Как выбрать между картинкой и div`ом?
Ну тут зависит от ситуации.
Если у вас большой экран и какой-нибудь очень длинный объект — то следует отрисовать его div`ом с background`ом (например кирпичную стенку). Она будет хорошо выглядеть и это будет всего лишь один DOM элемент (чем меньше DOM элементов на дисплее, тем меньше тормозит отрисовка).
Если у вас маленький экран или объект с анимацией — то следует отрисовать его через img, т.к. через смену адреса картинки можно воссоздать анимацию, а кроме того картинки при сжатии выглядят адекватно.
Все элементы на экране задаются строго в процентах, поэтому экран резиновый и натягивает куда угодно, в каком угодно виде. Соотношение отображаемой области, пропорций экрана и т.д. можно задать при создании объекта камеры (см. документацию).
Надо также понять, что камера это отдельный объект сам по себе, и он никак не связан ни с одним персонажем в мире. Но у каждой камеры, есть метод bind, который привязывает её к другому объекту в мире (не обязательно к персонажу, можно и к кирпичу её привязать). Такой интерфейс объекта позволяет легко реализовать переключение камеры между разными игроками (аналог вы уже видели в Counter-Strike, когда после смерти можно посмотреть бой от лица ещё живых игроков).
Какие выгоды нам дает объект камеры сам по себе?

Очевидно, что мы можем просто получить div в полный экран с фиксированным соотношением сторон. Вовсе не обязательно использовать камеру по основному назначению, этот объект может помочь нам в восстановление старых игр (например, получать идеально квадратную доску для нард, при любом соотношение сторон экрана)
Данный игровой движок можно использовать двумя способами:
Для создания новой игры вам понадобятся четыре основных компонента:

Т.к. все описанные выше компоненты мы получаем на так называемых «Фабриках», то мы можем создавать их в неограниченном количестве. Например, можно создать три мира, которые не будут никак пересекаться между собой. Или пять камер нацеленных на один мир, чтобы смотреть глазами разных персонажей.
Но есть и некоторые ограничения — например, объект может быть помещен только в один мир, иначе его поведение будет неадекватным (он будет реагировать на события в разных мирах).

Пояснение к схеме
Также есть модуль загрузки и сохранения. При сохранении он преобразует в JSON массив объектов в мире, а при загрузке — восстанавливает все объекты на фабрике и обновляет реестр.
Т.к. статью писал в течение нескольких дней — могут быть логические нестыковки, так что извините, если что не так. Чуть больше можно узнать тут [13]. В следующей статье будет больше практики, разбор API, а также ресурсы, необходимые для разработки игр.
Также рекомендую ознакомиться с трудами этих товарищей:
БЭМ: «Организация файловой системы» [14] и «История создания» [15]
Выступление [16] Вадима Макеева о CSS фреймворках
Канал [17] Андрея Короткова о том, как нормальные мужики пилят архитектуру
Выступление [18] Миши Давыдова в Челябинске о том как надо брать и масштабировать
Книгу Джоба Макара «Секреты разработки игр в macromedia Flash MX»
Серию статей [19] Миши Кадикова «Дизайн уровней. Теория и практика»
Автор: bakhirev
Источник [20]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/45816
Ссылки в тексте:
[1] насиловали DHTML: http://habrahabr.ru/post/196530/
[2] Пример 1: http://bakhirev.biz/StalinGrad/demo/12.html
[3] Пример 2: http://bakhirev.biz/StalinGrad/demo/13.html
[4] Пример 3: http://bakhirev.biz/StalinGrad/demo/14.html
[5] Пример 4: http://bakhirev.biz/StalinGrad/demo/18.html
[6] стоп/запустить: http://bakhirev.biz/StalinGrad/demo/
[7] тут: http://bakhirev.biz/StalinGrad/demo/15.html
[8] тут: http://bakhirev.biz/StalinGrad/demo/16.html
[9] тут: http://bakhirev.biz/StalinGrad/demo/17.html
[10] тут: http://bakhirev.biz/StalinGrad/demo/19.html
[11] вязанный мир: http://bakhirev.biz/StalinGrad/demo/20.html
[12] Мозг: http://www.braintools.ru
[13] тут: http://bakhirev.biz/StalinGrad/
[14] «Организация файловой системы»: http://ru.bem.info/method/history/
[15] «История создания»: http://ru.bem.info/method/filesystem/
[16] Выступление: http://pepelsbey.net/2008/04/css-frameworks/
[17] Канал: http://www.youtube.com/user/megadrone86/videos
[18] Выступление: http://tech.yandex.ru/events/yasubbotnik/chlb-feb-2012/talks/154/
[19] Серию статей: http://pro.level-design.ru/
[20] Источник: http://habrahabr.ru/post/197796/
Нажмите здесь для печати.