Поделиться
Поделиться

В этот раз FlashGamm превзошёл мои ожидания по размаху. Более 400 человек! Можно было покатать на MaxRacer от Alternativa3D, сыграть в реальную русскую рулетку c Arvara, пообщаться с кем хочешь и вообще, просто познавательно провести время. Отличное мероприятие, спасибо тем кто его устроил, кто пришёл туда с докладами и тем, кто пришёл их послушать.

Выкладываю свой доклад с FlashGamm Kyiv 2010. Тема доклада предполагает, что вы уже знакомы с физическим движком Box2D и уже что-то пробовали на нём сделать. И, надеюсь, что у вас это получилось. Если же вы ещё с ним никогда не работали, то вот полезный набор для ликбеза:

Box2D 2.1 Manual (или предыдущая версия движка, зато на русском )

Box2D в картинках

Todd`s Box2D Tutorials

Этого хватит с головой! Ну и сам доклад с комментариями:

[kml_flashembed movie=»http://xitri.com/wp-content/uploads/2010/12/flashgamm2010_secure.swf» height=»413″ width=»550″ /]

Персонаж в виде Box2D

Первое с чем вы столкнётесь, решив создать игру на Box2D -как описать своего красиво нарисованного персонажа средствами Box2D. Ведь у персонажа есть руки и ноги, которые торчат в разные стороны и ещё целая куча выступающих деталей. А Box2D умеет работать только с телами 2-х типов: круг и выпуклый многоугольник (прямоугольник — как частный случай). Поэтому представлять персонажа мы будем простыми геометрическими фигурами и в большинстве случаев хватит одного из 3-х вариантов, представленных на слайде 2. Если всё же нужно передать сложные формы с изгибами и впадинами, то это можно сделать комбинируя фигуры (Shapes) — слайд 3. Чаще всего это приходится делать для самого уровня — платформы и стены. Само собой у этой системы есть недостатки. На 4-м слайде видна разница между реальной формой персонажа и той которая участвует в физических расчётах. Но на самом деле для мультяшных игр большего и не нужно, а подобные «заступы» добавят жизни вашей игре.

Первый вариант — персонаж «колобок» (слайд 5).

Или шар, или круг, или колесо. Можно по разному его называть, но для двухмерного движка разницы нет. Здесь шейпу задаётся сила трения и смещается он изменением угловой скорости. Проще говоря, мы его программно вращаем, а он за счёт сил трения цепляется за поверхность и смещается. Чем выше сила трения, тем меньше проскальзывание при старте и тем резче персонаж меняет направление. Такой колобок отлично накатывается на все виды поверхностей — идеальный вариант для персонажа, если по форме он похож на колобка.

Вариант номер 2 — персонаж прямоугольник (слайд 6).

Такого уже не покрутишь, поэтому смещается он изменением линейной скорости. В большинстве платформеров ось персонажа всегда смотрит вертикально вверх и не наклоняется (fixedRotation=true) — я рассматриваю именно такой вариант. Тут высокая сила трения препятствует разгону и даёт быстрое торможение, а слабая — даёт скольжение при торможении. Возможно, подбирая параметры, вам придётся дополнительно тормозить персонажа скриптом. Плюс прямоугольника — он хорошо подходит для большинства человечкоподобных персонажей (которые есть в 90% платформерах). Минусы конечно есть и их немало: некорректно ходит по наклонной поверхности, цепляется за малейшие выступы (попробуйте походить по мостику), благодаря трению одинаково цепляется как за пол, так и за стены (прилипает если держать клавишу движения). Если для стен можно задать своё трение, то для платформ это сделать уже сложнее. В общем-то неплохой вариант для горизонтальных платформ и вертикальных стен.

Вариант номер 2.1 — персонаж пятиугольник (слайд 7).

В принципе это то же самое что и прямоугольник, только лучше бегает по наклонным поверхностям. Зато появляется дополнительный глюк если попасть на угол платформы наклонной гранью — персонаж зависает и уже не может на неё забраться.

Вариант номер 3 — персонаж как комбинация тел (слайд 8).

Это наш вариант! Создаём 2 тела: круг и прямоугольник и связываем их через Revolute Joint. В итоге получаем одни плюсы от каждой формы: круг используем для смещения персонажа и накатывания на любой вид поверхности, а прямоугольник, который привязан к кругу и всё время тягается за ним — правильное столкновение с платформами.

Теперь, когда мы определились с программной формой персонажа, нужно привязать к нему свою графику. У объекта b2Body есть замечательное свойство userData в которое можно записать всё что угодно. Обычно туда загоняют ссылки на соответствующие клипы, а потом в основном цикле, перебирая все тела, считывают с них координаты и присваивают их графическим клипам. Мы пойдём этим же путём (честно говоря, другого я и не встречал). Единственная разница в том, что сам клип с персонажем я загоню в userData прямоугольнику, а класс, который отвечает за логику (а у меня персонаж состоит из абстрактного объекта с логикой и собственно самого клипа-визуала, который только переводится в нужное состояние) — в userData колобка. В итоге из колобка я буду получать координаты персонажа и всего его контакты с поверхностью (для добавления эффектов и т.д.), а клип-визуал автоматом привяжется к прямоугольнику (слайд 9).

Состояния персонажа

Я знаю 2 способа, как организовать состояния персонажа во флеше.

Первый — классический для флеша — расставить клипы с состояниями по кадрам и переключаться по ним через gotoAndStop(stateFrame). Но этот способ не всегда хорошо работает с AS3, так как после перехода в нужный кадр вы не можете сразу получить доступ к клипам которые в нём находятся и нужно либо ждать 1 кадр (пока флеш их инициализирует) либо вручную вызывать событие RENDER.

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

Со смещением более-менее понятно, но я обещал ещё добавить управление мышкой! У меня персонаж старается навести прицел на координаты мыши (слайд 17). Это можно делать программно, но тогда оружие должно находиться на оси X, что не всегда удобно. Зато такой поворот идеально точный. А можно ещё сделать наведение на мышку анимацией. В этом случае делается анимация полного поворота на 360 градусов (для удобства длиной в 90, 180 или 360 кадров) и затем вычислив угол, переводим анимацию в нужный кадр. Тут есть один нюанс: так как оружие вряд ли расположено строго на оси X и в процессе анимации смещается с неё, угол будет не всегда верным и нужно будет рассчитывать корректирующий угол либо, как вариант, вращать сам клип с пулемётом (слайд 17).

На 18-м слайде показаны клипы, которые участвуют в анимационном повороте. Интерес представляет клип самого тела, внутри которого сделана анимация движения (стоит, бежит, прыгает, сидит). Фактически персонаж состоит из 2-х уровней: главный клип, внутри которого сделана анимация поворота на 360 градусов и анимация движения внутри клипа с телом. То есть анимация ходьбы вся внутри клипа и я могу масштабировать и смещать сам клип, что я и делаю в анимации поворота. Запутанно, но смысл в этом есть 🙂

А вот с оружием я поступил дедовским способом, расположив разные винтовки в разных кадрах. Дело в том, что мне просто не нужны ссылки на эти объекты, а графику флеш сам прекрасно отрисует.

Прыжок

Очень важный момент в игре. Нельзя разрешать прыгать, если под ногами нет опоры. Для этого я решил считать контакты. Коснулся поверхности, +1 к контактам, упал с платформы или прыгнул, соответственно -1. Но все контакты мне не интересны, а то я так и от стенок смогу отпрыгивать, поэтому считаем контакты только с теми поверхностями, угол наклона которых лежит в диапазоне от -135° до -45°. На практике эти углы определяются через нормаль, которую мы получаем при контакте (кто не знает, нормаль — это единичный вектор, направленный наружу перпендикулярно поверхности). Например для этих углов условие будет таким:

if (normal.x > -.72 && normal.x < .72 && normal.y < 0) {...}

Но и это ещё не всё, если вы набросаете динамических объектов на уровень, то при большом количестве контактов с ними счётчик сбивается. Я думаю это из-за того, что за время контакта тело поворачивается и уже выходит за рамки условия. В общем, при потере контакта стоит дополнительно проверять, был ли этот шейп в списке допустимых поверхностей. И если да, то делать -1 к контактам (слайды 19, 20).
Один из участников конференции предложил делать проверку на контакты в момент прыжка методом getContactlist(). Я попробовал, но сразу не заиграло. Может быть фильтрация не настроена правильно или сенсоры реагируют, но я получал ответ что контакт есть, даже когда тело ни с кем не касалось. Если у кого есть опыт такого подхода, отпишитесь пожалуйста в комментариях.

Анимация прыжка, как и ходьба, реализована в клипе с телом. Тело на самом деле висит на месте, меняется только положение ног. Во время прыжка персонажа считываем его скорость по оси Y и по пропорции переводим анимацию в соответствующий кадр (слайд 21).

Приседание

Движок Box2D работает с телами типа Rigid Body — то есть твёрдые как алмаз. Это значит что в процессе игры нельзя изменить геометрию шейпов, подвигав точки. Зато можно создать тело-прямоугольник, состоящий из нескольких шейпов и включать/выключать верхний в момент вставания/приседания. Реализовать это можно, сделав шейп сенсором. Тела теперь не будут сталкиваться, но события всё же будут генериться, так что нужно будет, например, для пуль делать проверку, что они законтачили не с сенсором. Так я рассказывал на FlashGamme, но буквально вчера, работая над другой своей игрушкой, нашёл более интересный способ. Это задание шейпу categoryBits = 0 (фильтрация в Box2D). При этом другие шеймы вообще перестают с ним сталкиваться и получать контакты. Не знаю, хак это или так было задумано, но это работает.

Выстрелы и эффекты

С точки зрения Pure AS3, это самое слабое место в моём примере, так как код, в моём случае, пишется в кадре. Дело в том, что снаряд должен ложиться на сцену не сразу по клику мышки (в этот момент запускается сама анимация выстрела), а через какое-то время (2-7 кадров), так как оружие должно «раскачаться» и только потом выстрелить. К тому же у каждого оружия свой интервал и становится сложно заложить все паузы программно. Так что я решил сделать так: когда анимация выстрела доходит до нужного момента, вызывается такой код:

import flash.events.Event;
dispatchEvent(new Event("shoot", true))

Вторым параметром я включаю режим распространения события по цепочке клипов наверх, до stage. Таким образом я могу один раз подписаться к основному клипу на получение этого события и буду получать его не зависимо от того на какой глубине иерархии находится нужный клип. Доступ же к нему я всегда имею через event.target. В общем, перехватываем это событие и помещаем снаряд на сцену. Снаряд летит и при контакте с другими телами генерит событие, прося удалить себя со сцены и добавить вместо неё эффект взрыва. Всё легко и просто.

Снаряды я делаю динамическими телами — так как это единственный тип тела в Box2D который может смещаться под действием физики и контактировать со статическими телами. А на динамические тела, как известно, действует гравитация. Но для снарядов и пуль она нам не нужна. То есть берём и применяем к пулям силу, прямо противоположную гравитации. Чтобы было легче, зашьём это дело в основной цикл и создадим интерфейс IAvoidGravity. Теперь если какой-то класс захочет избавиться от гравитации, ему нужно просто применить этот интерфейс (слайд 25).

Эффекты

Все эффекты — это обычные мувиклипы, имеющие разную длину в зависимости от анимации (слайд 26). В последнем кадре ставится такой код:

stop();
parent.removeChild(this);

Остановка нужна чтобы не вылетали ошибки. Дело в том, что вы успешно удалите клип со сцены, но сборщик мусора может не успеть удалить клип из памяти до того, как он снова захочет удалить себя со сцены — а его уже там нет.

Когда два тела касаются друг друга, вы получаете объект контакта, где есть точка касания и нормаль поверхности (читай, угол поворота). Поэтому расположить эффект и повернуть его на нужный угол не составляет труда.

Это легко, НО! Нужно знать что contact.getManifold() возвращает точки касания и нормаль в локальных координатах. Чтобы получить их в мировых координатах, нужно преобразовать его в b2WorldManifold. Ещё почему-то круг и прямоугольник (при контакте с другим прямоугольником), дают прямо противоположные нормали — это тоже нужно учитывать.

Ну а сам результат можно посмотреть на слайде 28. Оружие переключается клавишами 1-7, работает двойной прыжок. Игра ещё в разработке, так что возможны баги, строго не судите 🙂

Об авторе

Денис Романко (Stormit)Занимаюсь дизайном, программированием, спецэффектами и разработкой игр.

НазадНазад