Игра Pengu5 больше всего мне нравится тем, что я там могу делать всё что хочу, не слушая никого.
Она пишется на чистом, ничем не замутнённом JavaScript, без каких-либо движков или внешних зависимостей.
Основной идеей игры стала льдина, плавающая по морю. Этот элемент я решил сделать смысловым центром и заставить работать не только по прямому назначению.
Так, уровень воды служит индикатором прогресса загрузки ресурсов. А логотип игры не просто находится на экране, но и сам является льдиной и плавает на воде.
Кроме того, он управляется мышью точно так же, как льдина управляется в игре. Что не играет никакой роли, но такое решение мне очень нравится, так как использует саму механику игры для заставки.
Первоначально игра была простой. Вот море, по морю плавает льдина. Льдину можно двигать влево и вправо. На льдине стоит пингвин, который может прыгать.
Всё это были отдельные сущности. Льдина привязана к морю, пингвин может стоять на льдине или находиться в воздухе. Когда он в воздухе, на него действует гравитация и он падает.
Потом я начал думать над дальнейшим развитием игры и поставил такие вопросы: а что, если льдина взлетит в воздух? Тогда ей надо тоже падать. А что, если пингвин упадёт в воду? Тогда ему надо тоже плавать. Раньше, кстати, он просто тонул, и игра заканчивалась, поэтому проблем не было.
Это была такая фишка: пингвин, который боится воды.
Управление было привязано к льдине. А что, если льдин будет две, и пингвин будет перепрыгивать с одной на другую? Значит, нужно уметь управлять обеими, но в разные моменты времени. А что, если игроков будет двое и каждый будет управлять своей льдиной?
Чтобы подвести под все возможные комбинации единую базу, я постепенно разработал систему. Она не обязательно является оптимальной. Просто я решил делать так и поэтому делаю.
Обёртки
Я сделал достаточно абстрактные объекты, которые начинаются с графического примитива. Скажем, при подгрузке данных на экран выводится индикатор, вроде курсора с часиками:
Это простой спрайт, который не имеет веса, объёма, скорости и т.д. Он просто есть.
Если же нужно сделать движущийся объект, то я добавляю к спрайту обёртку (wrapper). Эта обёртка имеет необходимые параметры, такие как координаты и скорость. Когда я манипулирую параметрами обёрток, они в свою очередь манипулируют прикреплёнными к ним спрайтами.
Я могу сделать много разных обёрток. Например, чтобы заставить индикатор загрузки вращаться и пульсировать, я добавляю к нему обёртку PulseWhirl:
Чисто теоретически я мог бы сделать две отдельных обёртки, одну для пульсации, другую для вращения, и применить их обе. Для экономии места покажу код обёртки Rotate, которая делает только вращение:
Название mc это сокращение от MovieClip – так было принято писать в Flash, где была сделана первая версия игры.
Таким образом, я могу сочетать любой графический примитив с любой обёрткой, и делать это только тогда, когда необходимо. То есть не перегружать базовые объекты ничем лишним.
Модбоксы
Модбоксы являются расширенными версиями обёрток. Это целые контейнеры, которые могут содержать в себе список объектов.
Каждый контейнер имеет свою функцию и применяет эту функцию ко всем объектам, которые содержит.
Есть базовый контейнер BuoiBox. Это аналог физического пространства. Ко всем объектам, помещённым в него, он применяет ускорение свободного падения. Также он связан с компонентом, эмулирующим воду, и если объект находится ниже уровня воды, на него начинает действовать выталкивающая сила, которая компенсирует гравитацию, и объект начинает плавать. Да, всё честно по Архимеду.
Есть контейнер HorizontalControlBox. Объекты, находящиеся в нём, будут перемещаться влево-вправо вслед за курсором мыши.
Есть контейнер ColliderBox. Объекты, находящиеся в нём, будут проверяться на столкновения друг с другом.
Так как разные контейнеры могут менять один и тот же параметр у объекта несколько раз, эти изменения не применяются сразу к объекту.
Контейнер MotionBox выполняет финализацию движения. Его задача – взять изменения параметров, накопленные в предыдущих контейнерах, и применить эти изменения к экранным объектам. Двигаться по экрану будут все объекты, добавленные в MotionBox.
Если же я хочу просто рассчитывать параметры объекта, не отображая изменений на экране, то я не буду добавлять его в MotionBox.
Вот что я делаю в заставке игры:
- Создаю модбоксы HorizontalControlBox, MotionBox, BuoiBox.
- Создаю спрайт льдины с логотипом (iceLogo) и обёртку MotionWrapper для него.
- Добавляю спрайт льдины и его обёртку в эти модбоксы.
Дальше всё работает само собой. Льдина, установленная сверху экрана, начинает падать вниз, потому что на неё действует гравитация в BuoiBox. Упав ниже уровня воды, льдина начинает плавать и раскачиваться на волнах, опять же благодаря BuoiBox. Когда двигается мышь, HorizontalControlBox двигает льдину. Наконец, все изменения положения льдины финализируются в MotionBox.
Аналогично можно задать поведение пингвина. Льдина, на которой он стоит – тоже модбокс PlatformBox. Его задача – удерживать на себе все добавленные объекты. То есть я могу добавить хоть целую толпу пингвинов на льдину, и все они будут там стоять и никуда не денутся.
Сама льдина находится в модбоксе HorizontalControlBox (да, это модбокс внутри модбокса!) и поэтому управляется мышью и плавает вправо-влево, а вместе с ней плавают все, кто на ней стоит. Это происходит совершенно автоматически.
Когда пингвин подпрыгивает, я убираю его из PlatformBox и помещаю в BuoiBox. Теперь он находится в свободном падении. Если он падает в воду, то начинает плавать в воде так же, как и льдина.
Но пингвин и льдина также добавлены в ColliderBox, который отслеживает столкновения, и как только они столкнутся, пингвин снова прикрепится к PlatformBox и покинет BuoiBox и ColliderBox:
Данное поведение я, кстати, не программировал. Оно получилось само собой. Когда пингвин подпрыгивает, ему надо приземлиться на льдину. Это отслеживает ColliderBox. Но если пингвин пересёкся с льдиной, будучи в воде, то ColliderBox также возвращает его на место.
В итоге, с помощью одних и тех же инструментов я сделал и заставку игры, и сам игровой процесс, не переписывая ничего конкретно под пингвина. Прелесть в том, что эти процессы работают независимо и математически надёжно, то есть можно быть уверенным, что добавив объект туда-то и туда-то, я получу соответствующее поведение.
Я также могу в любой момент выключить любой из модбоксов, и он перестанет оказывать влияние. И также в любой момент его включить.
Но сейчас есть небольшая, исторически сложившаяся проблема с иерархией объектов. Например, я должен добавлять в модбокс графический объект и его обёртку как два отдельных параметра, что приводит к излишнему загромождению кода. Зачем так было сделано, пока не вспомню, но следующим этапом надо будет разобраться с этим и заодно всё вычитать и освежить.
Вся подборка: