Привет хабра-сообществу.

Относительно недавно достаточно давно писал статью про создание очередной казуалки на Flash-платформе с физикой, обещал вторую статью, встречайте.
В этой статье — научу рисовать мир и расскажу о сенсорах. Остальное под катом.
Что можно сделать из этих двух уроков, можно посмотреть тут (музыку отключить нельзя, но можно убрать звук в системе).
Еще раз вспомним, что было в прошлом "уроке".
Теперь нужноукрасть нарисовать будущую графику в игре, но т.к. этот урок прежде всего про программирование игр, можете воспользоваться тем, что сделал я:
Текстура 1:

Текстура 2:

Вот две BMP текстуры, 474x474 пикселей.
Служить они будут основой для нашего мира.
Текстура 2 — это статическая текстура, она будет представлена в виде Sprite и будет всегда отрисовываться с нужным нам углом поворота.
Текстура 1 — это текстура для рисования мира, ей мы будем заливать прямоугольники нашего мира с помощью beginBitmapFill и translate-матрицы. Иначе говоря, то что не залито, то свобода передвижения и пустое пространство.
Чуть красиво оформив в соответствии с нужным нам сеттингом, получим что-то вроде:

Даем оболочку-картинку (спрайт) нашему шарику-герою, тут тоже предлагаю взять мою графику:

И чтобы уж совсем скучно не было проходить скучнейшие лабиринты, сделаем препятствия: шипы, огонь, лазеры. Но т.к. объем статьи должен быть адекватным, рассмотрим только шипы, но создадим базу для всего остального.
Текстура шипа:

Создаем необходимые линковки и наша библиотека выглядит так:

Графика готова.
Сенсоры — это обычные объекты (Static Body, Dynamic Body), за исключением того, что у них стоит флаг isSensor. Что делает этот флажок? Все очень просто, такой объект «чувствует» столкновение, однако, для объекта без флага isSensor — ничего не произойдет. Твердый объект — просто пройдет сквозь сенсор, вызвав событие коллизии в ContactListener.
ContactListener — «слушатель» столкновений в Box2D. Например, сталкивается объект A и B в мире, в ContactListener вызывается функция Add(point:b2ContactPoint), point содержит в себе: шейпы, которые участвуют в столкновении; точку столкновения в глобальной системе координат; столкнувшейся тела. Другими словами, эти события можно обработать.
— Сенсоры, слушатели, ничего не понятно, зачем это?
Затем, что при столкновении нашего героя с шипом — логично вызвать смерть героя и перезапуск уровня.
Меняем размер проекта с 500x500 на 600x580.
Меняем позицию отрисовки дебаг-спрайта, на:
Все по честному, все по центру.
Добавляем новые переменные:
В конструкторе главного класса инициализируем game и viewport:
У Flash есть специальная опция «cache as bitmap», которую можно устанавливать для отдельных клипов — это обозначает, что клип где-то в памяти сохраняется в виде растровой картинки и более не пересчитывается как векторное изображение. Мои эксперименты показали, что это дает прирост производительности, но для полного счастья его явно не достаточно.
Добавляем вращение «шестерни» в enterFrameListener, там же, где вращаем sprite:
Запускаем, видим уже что-то более менее радующее глаз, нежели просто вращающаяся зеленаяхерня картинка.
Даем шарику спрайт. Ищем функцию createHero и добавляем где-нибудь в конце:
body.SetSprite — отсутствует в оригинальном box2D. Иначе говоря, это аналог GetUserData. Но GetUserData у нас используется для типа объекта («hero», «box»), поэтому я добавил в движок и SetSprite. Если пишите на оригинальном Box2D — не расстраивайтесь, используйте динамические объекты, например: body.SetUserData({type: «hero», sprite: sprite});
Синхронизируем позицию экранного объекта с математическими расчетами каждый кадр, идем в функцию enterFrameListener, а конкретно в момент, где мы обновляем гравитацию герою, добавляем:
Убавляем Alpha у спрайта-дебага, чтобы могли различать то, что происходит:
Запускаем, крутим, видим, что все работает:

Остается теперь избавиться от DebugDraw, чтобы полностью перейти на нашу графику, но мешает это нам сделать, что стены у нас не рисуются, исправим это.
Создаем переменные для translate-матрицы и текстуры:
Инициализируем их в конструкторе:
Идем в функцию CreateStaticRect и добавляем что-то вроде:
И для наглядности добавим еще один StaticRect (в конструкторе):
На время отключаем DebugDraw комментированием строчки:
Компилируем, любуемся прорисованными стенами:

Мы научились рисовать наш мир, создадим препятствия.
Создаем абстрактный класс, от него наши препятствия будут наследоваться, листинг Obstacle:
И создаем дите этого класса, Spike (собственно наш шип), листинг:
Добавим три шипа в наш мир, идем в конструктор главного класса и пишем:
Компилируем, любуемся шипами, но они пока не опасны, так сделаем их опасными.

Создаем новый класс GearMazeContact, который унаследован от b2ContactListener, мы будем переопределять его методы (да-да, в AS3 очень актуальны override). Листинг:
Подключаем наш слушатель к миру, там же, где создаем мир и задаем DebugDraw:
И добавим функционал, который удалит наш шар в случае со смертью, в переборе массива с телами (enterFrameListener):
Тут можно дальше пилить геймплей и делать всякие-плюшки: лазеры, порталы, пилы,девушек.
Если вы хотите сделать из этого игрушку, то можно сделать простой алгоритм процедурной генерации лабиринтов с Obstacle.
По традиции прикладываю весь код по статье, ссылку на демо и готовую игрушку, сделанную по этим шагам.
Кстати, готовой игры не будет, увы :-(
Пока это какая-то альфа с 18-ти уровнями. Рефакторинг кода меня убивает, я творческий человек: творить, творить и еще раз творить. Деньги меня не интересуют :-)
Ссылки: исходники | демо | готовая игра
P.S. спасибо большое хорошему человеку datacompboy, когда написал первую часть статью —отсыпал подарил мне хостинг с доменом, чтобы демки, игры и исходники вы не качали с фриварных сайтов.
P.S.S. по прежнему вы можете писать мне, по поводу геймдева в as3, с радостью отвечу.

В этой статье — научу рисовать мир и расскажу о сенсорах. Остальное под катом.
Что можно сделать из этих двух уроков, можно посмотреть тут (музыку отключить нельзя, но можно убрать звук в системе).
Еще раз вспомним, что было в прошлом "уроке".
Графика
Теперь нужно
Текстура 1:

Текстура 2:

Вот две BMP текстуры, 474x474 пикселей.
Служить они будут основой для нашего мира.
Текстура 2 — это статическая текстура, она будет представлена в виде Sprite и будет всегда отрисовываться с нужным нам углом поворота.
Текстура 1 — это текстура для рисования мира, ей мы будем заливать прямоугольники нашего мира с помощью beginBitmapFill и translate-матрицы. Иначе говоря, то что не залито, то свобода передвижения и пустое пространство.
Чуть красиво оформив в соответствии с нужным нам сеттингом, получим что-то вроде:

Даем оболочку-картинку (спрайт) нашему шарику-герою, тут тоже предлагаю взять мою графику:

И чтобы уж совсем скучно не было проходить скучнейшие лабиринты, сделаем препятствия: шипы, огонь, лазеры. Но т.к. объем статьи должен быть адекватным, рассмотрим только шипы, но создадим базу для всего остального.
Текстура шипа:

Создаем необходимые линковки и наша библиотека выглядит так:

Графика готова.
Сенсоры
Сенсоры — это обычные объекты (Static Body, Dynamic Body), за исключением того, что у них стоит флаг isSensor. Что делает этот флажок? Все очень просто, такой объект «чувствует» столкновение, однако, для объекта без флага isSensor — ничего не произойдет. Твердый объект — просто пройдет сквозь сенсор, вызвав событие коллизии в ContactListener.
ContactListener — «слушатель» столкновений в Box2D. Например, сталкивается объект A и B в мире, в ContactListener вызывается функция Add(point:b2ContactPoint), point содержит в себе: шейпы, которые участвуют в столкновении; точку столкновения в глобальной системе координат; столкнувшейся тела. Другими словами, эти события можно обработать.
— Сенсоры, слушатели, ничего не понятно, зачем это?
Затем, что при столкновении нашего героя с шипом — логично вызвать смерть героя и перезапуск уровня.
От теории к делу, приступаем к программированию
Учимся рисовать в мире
Меняем размер проекта с 500x500 на 600x580.
Меняем позицию отрисовки дебаг-спрайта, на:
sprite.x = 300; // половина 600 sprite.y = 290; // половина 580
Все по честному, все по центру.
Добавляем новые переменные:
public var game:Sprite; // контейнер-спрайт для мира public var level_sprite:Sprite = new Sprite(); // Процедурно-cгенерированная текстура мира public var viewport:Sprite; // контейнер-спрайт для мира (героя, шипов)
В конструкторе главного класса инициализируем game и viewport:
game = new sprite_game(); game.x = 300; // центрируем game.y = 290; // центрируем game.cacheAsBitmap = true; addChild(game); viewport = new Sprite(); viewport.x = 300; viewport.y = 290; addChild(viewport);
У Flash есть специальная опция «cache as bitmap», которую можно устанавливать для отдельных клипов — это обозначает, что клип где-то в памяти сохраняется в виде растровой картинки и более не пересчитывается как векторное изображение. Мои эксперименты показали, что это дает прирост производительности, но для полного счастья его явно не достаточно.
Добавляем вращение «шестерни» в enterFrameListener, там же, где вращаем sprite:
game.rotation = sprite.rotation = rotator;
Запускаем, видим уже что-то более менее радующее глаз, нежели просто вращающаяся зеленая
Даем шарику спрайт. Ищем функцию createHero и добавляем где-нибудь в конце:
var sprite:Sprite = new sprites_hero(); sprite.width = sprite.height = 16; viewport.addChild(sprite); hero.SetSprite(sprite); // о том, что такое SetSprite я расскажу позже. // задаем позицию спрайта такую же, как и у математики движка, умноженного на 30 (коф. преобразования) hero.GetSprite().x = hero.GetPosition().x * 30; hero.GetSprite().y = hero.GetPosition().y * 30; // вращение hero.GetSprite().rotation = hero.GetAngle() * 180 / Math.PI;
body.SetSprite — отсутствует в оригинальном box2D. Иначе говоря, это аналог GetUserData. Но GetUserData у нас используется для типа объекта («hero», «box»), поэтому я добавил в движок и SetSprite. Если пишите на оригинальном Box2D — не расстраивайтесь, используйте динамические объекты, например: body.SetUserData({type: «hero», sprite: sprite});
Синхронизируем позицию экранного объекта с математическими расчетами каждый кадр, идем в функцию enterFrameListener, а конкретно в момент, где мы обновляем гравитацию герою, добавляем:
body.GetSprite().x = body.GetPosition().x * 30; body.GetSprite().y = body.GetPosition().y * 30; body.GetSprite().rotation = body.GetAngle() * 180 / Math.PI;
Убавляем Alpha у спрайта-дебага, чтобы могли различать то, что происходит:
dbgDraw.m_alpha = 0.5; dbgDraw.m_fillAlpha = 0.1;
Запускаем, крутим, видим, что все работает:

Остается теперь избавиться от DebugDraw, чтобы полностью перейти на нашу графику, но мешает это нам сделать, что стены у нас не рисуются, исправим это.
Создаем переменные для translate-матрицы и текстуры:
public var matrix_texture:Matrix; public var textureal:BitmapData;
Инициализируем их в конструкторе:
textureal = new texture_gear(); matrix_texture = new Matrix(); matrix_texture.translate(237, 237); // заодно добавим наш level_sprite viewport.addChild(level_sprite);
Идем в функцию CreateStaticRect и добавляем что-то вроде:
level_sprite.graphics.beginBitmapFill(textureal, matrix_texture); level_sprite.graphics.drawRect(x - 150, y - 150, w, h); level_sprite.graphics.endFill();
И для наглядности добавим еще один StaticRect (в конструкторе):
CreateStaticRect(150, 140, 30, 30);
На время отключаем DebugDraw комментированием строчки:
// world.SetDebugDraw(dbgDraw);
Компилируем, любуемся прорисованными стенами:

Мы научились рисовать наш мир, создадим препятствия.
Создаем абстрактный класс, от него наши препятствия будут наследоваться, листинг Obstacle:
package { import Box2D.Dynamics.b2World; import flash.display.Sprite; /** * ... * @author forhaxed */ public class Obstacle extends Sprite { public var active:Boolean = true; // активна ли в текущей момент public var angle:int = 0; // угол поворота public var viewport:Sprite; // ссылка на viewport public var world:b2World; // ссылка на мир // 0 - LEFT, 1 - UP, 2 - RIGHT - 3 - DOWN public function Obstacle(_viewport:Sprite, _world:b2World, x:Number, y:Number, angle:int = 0) { viewport = _viewport; world = _world; } } }
И создаем дите этого класса, Spike (собственно наш шип), листинг:
package { import Box2D.Collision.Shapes.b2CircleDef; import Box2D.Dynamics.b2Body; import Box2D.Dynamics.b2BodyDef; import Box2D.Dynamics.b2World; import flash.display.Sprite; /** * ... * @author forhaxed */ public class Spike extends Obstacle { public function Spike(_viewport:Sprite, _world:b2World, x:Number, y:Number, angle:int = 0) { super(_viewport, _world, x, y, angle); // конструктор родителя switch(angle) // вращаем как нам надо { case 0: this.rotation = 90; break; case 1: this.rotation = 180; break; case 2: this.rotation = 270; break; case 3: this.rotation = 0; break; } var body:b2Body; var bodyDef:b2BodyDef; var circleDef:b2CircleDef; var sprite:Sprite; this.x = x; this.y = y; viewport.addChild(this); /* OBSTACLE */ x = x / 30; y = y / 30; var r:Number = 6 / 30; bodyDef = new b2BodyDef(); bodyDef.position.Set(x, y); circleDef = new b2CircleDef(); circleDef.radius = r; circleDef.density = 1; circleDef.friction = 1; circleDef.isSensor = true; // устанавливаем isSensor в true, чтобы этот объект только следил за миром circleDef.restitution = 0.2; body = world.CreateBody(bodyDef); body.SetUserData("spike"); // тип ставим как spike body.SetSprite(this); // ставим спрайт body.CreateShape(circleDef); body.SetMassFromShapes(); addChild(new saw()); } } }
Добавим три шипа в наш мир, идем в конструктор главного класса и пишем:
new Spike(viewport, world, 37, 5, 0); new Spike(viewport, world, 15, 27, 1); new Spike(viewport, world, 15, -17, 3);
Компилируем, любуемся шипами, но они пока не опасны, так сделаем их опасными.

Создаем новый класс GearMazeContact, который унаследован от b2ContactListener, мы будем переопределять его методы (да-да, в AS3 очень актуальны override). Листинг:
package { import Box2D.Dynamics.*; import Box2D.Collision.*; import Box2D.Collision.Shapes.*; import Box2D.Dynamics.Joints.*; import Box2D.Dynamics.Contacts.*; import Box2D.Common.*; import Box2D.Common.Math.*; import flash.display.MovieClip; public class GearMazeContact extends b2ContactListener { public override function Add(point:b2ContactPoint):void { var p1:b2Body = point.shape1.GetBody(); var p2:b2Body = point.shape2.GetBody(); if(p1.GetUserData()=="hero" && p2.GetUserData()=="spike") { if ((p2.GetSprite() as Obstacle).active) { trace("Ha-ha!"); p1.SetUserData("hero_dead"); // меняем тип объекта на hero_dead, для его удаления /* Внимание, в процессинге просчета физики ничего с объектами не сделать, мир заблокирован от нас, поэтому мы просто удалим его, когда мир разблокируется */ } } } } }
Подключаем наш слушатель к миру, там же, где создаем мир и задаем DebugDraw:
var GearListener:GearMazeContact = new GearMazeContact(); world.SetContactListener(GearListener);
И добавим функционал, который удалит наш шар в случае со смертью, в переборе массива с телами (enterFrameListener):
if (body.GetUserData() == "hero_dead") { if (body.GetSprite() is Sprite) viewport.removeChild(body.GetSprite()); world.DestroyBody(body); }
Тут можно дальше пилить геймплей и делать всякие-плюшки: лазеры, порталы, пилы,
Если вы хотите сделать из этого игрушку, то можно сделать простой алгоритм процедурной генерации лабиринтов с Obstacle.
По традиции прикладываю весь код по статье, ссылку на демо и готовую игрушку, сделанную по этим шагам.
Кстати, готовой игры не будет, увы :-(
Пока это какая-то альфа с 18-ти уровнями. Рефакторинг кода меня убивает, я творческий человек: творить, творить и еще раз творить. Деньги меня не интересуют :-)
Ссылки: исходники | демо | готовая игра
P.S. спасибо большое хорошему человеку datacompboy, когда написал первую часть статью —
P.S.S. по прежнему вы можете писать мне, по поводу геймдева в as3, с радостью отвечу.