Создание очередной казуалки на Flash-платформе с физикой. Часть II

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

Относительно недавно достаточно давно писал статью про создание очередной казуалки на 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.
Меняем позицию отрисовки дебаг-спрайта, на:

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

Подробнее
Реклама

Комментарии 19

    +5
    Cпасибо! :)
      +3
      Ух ты, продолжение :D
        0
        Хоть бы привет написали демосценерам у которых трек стырили
          0
          Который, при этом, раздражающий и не отключается. Или в игру не играй или музыку любимую отключай.
            +1
            А мне очень понравилась музыка, на вкус и цвет значит!
            +4
            За что минусы? Всё правильно. В игре обязана быть кнопка выключения музыки.
              0
              Так же как в бюллетени графа «Против всех»
            +4
            Heaven seven называется демка, очень клевая, кстати…
              +1
              Как-то лет 5 назад на 1м рашн канале после рекламы был анонс фильма с Сигалом. В фоне их аудирежиссёр канала наложил куски из «The Cookie Thing» (Aardbei) :)
              –1
              А вы значит полиция нравов да? Человек потратил тонну времени чтобы рассказать заинтересованным людям процесс создания игры(!), а вы тут свои пять копеек про «трек стырили»… это, наверное, только у нас такие люди находятся.
              –2
              Если не секрет, каким образом Вы нарисовали шестеренку? Я, вот, сколько не пытался — мусор какой-то получался.
                +1
                Ну вот, вы убили два часа моей жизни :(. Зашел прокомментировать и заигрался.
                  0
                  то же самое, и главное столько :D
                  0
                  ох, я думаю при длительной игре головокружения не избежать)
                    0
                    ИМХО, это хардкор, а не казуалка)
                    За статью спасибо. Но мне всё равно больше нравится nape)
                      0
                      Ну вы бы хоть упомянули про Spin The Black Circle, убившую многие часы моей жизни :) все-таки у вас — полный ее аналог.
                        0
                        Круто! Благодарю, искренне!
                          0
                          спасибо за ваш труд… заигрался)

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                          Самое читаемое