Привет хабра-сообществу.
Относительно недавно достаточно давно писал статью про создание очередной казуалки на 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, с радостью отвечу.