アクションゲームの作成ログ

状況

とりあえず、左右キーによって「プレイヤー表示の反転(scaleXによる反転)」「弾の発射方向の反転」ができるようにした。キー入力用の関数内部に直接埋め込んでいるのは問題だが、まだプロトタイプだからそこはスルー。


つづいて、プレイヤーが移動できなくなる「埋まりこみ」を調査。やはり、ショットのタイミングで埋まっている模様。弾の発射位置を変えても起こったので、「カテゴリで指定してるのに当たって埋もれている」という症状ではないっぽい。
「埋もれているなら、掘り出せばいいじゃない」という思考パターンにより、「毎フレーム、少しだけpositionを上に移動させる」という処理を組み込んでみたが、状況は改善せず。むしろ、「浮いているのに動けない」という怪奇現象を発生させるだけだった。ちなみに、左右キーによってプレイヤの表示は反転しているので、キー入力を見てないわけではない。
どうも、「あるタイミングでプレイヤの位置計算が放棄されている」のではないかと思い、「そういえばSleepによってそれは起こりうるな」と思い至ったので、bWorldの初期化で「doSleep = false」にすると、今のところちゃんと移動できている。


ということで、「Box2Dで何かオブジェクトが動かせない状況があったら、Sleepを疑え」という結論で今回は終了しておく。Sleepのタイミングをどうやって見ているかがわかれば、「doSleep = false」以外の対処もあるかもしれない。
→Sleepは「別のコリジョンがぶつかるまで計算しない」というものなはずなので、そこらへんが鍵かも。


ここまで来たら、あとは本格的にアクションゲームの作成に入れる。まずはコードを整理して、きちんとクラスとして分離するところからかな。

Flash

前回と同じく、

  • 左右キーで移動
  • 上キーでジャンプもどき(むしろ舞空術
  • Sキーでショット

コード

//mxmlc Main.as
//author Show=O=Healer

package {
	import Box2D.Dynamics.*;
	import Box2D.Collision.*;
	import Box2D.Collision.Shapes.*;
	import Box2D.Common.Math.*;
	import flash.events.Event;
	import flash.display.*;
	import flash.text.TextField;
	//Input
	import flash.ui.Keyboard;
	import flash.events.KeyboardEvent;
	//AA
	import flash.geom.*;

	[SWF(backgroundColor="#ffffff", width="350", height="200")]
	public class Main extends Sprite {
		private var m_world:b2World;
		private var m_physScale:Number = 10;

		static public const GRAVITY:Number = 10.0;

		//Player
		private var m_Player:b2Body;
		private var m_PlayerGraphic:Sprite;
		private var m_PlayerDir:Number = 1.0;//右向きなら1、左向きなら-1

		//弾
		private var m_BulletBd:b2BodyDef;
		private var m_Bullet:Array = new Array;

		//Input
		private var m_InputL:int = 0;
		private var m_InputR:int = 0;
		private var m_InputU:int = 0;
		private var m_InputD:int = 0;
		private var m_InputSpace:int = 0;

		static public const KEY_S:int = 83;

		//Param
		public static const FLOOR_W:Number = 400;
		public static const FLOOR_H:Number = 10;
		public static const FLOOR_Y:Number = 250;

		public static const BLOCK_W:Number = 40;
		public static const BLOCK_H:Number = 20;
		public static const BLOCK_X:Number = 300;

		public static const PLAYER_X:Number = 50;
		public static const PLAYER_Y:Number = 220;
		public static const PLAYER_R:Number = 10;
		public static const PLAYER_V:Number = 10;

		public static const BULLET_X:Number = 50;
		public static const BULLET_Y:Number = 220;
		public static const BULLET_R:Number = 5;
		public static const BULLET_V:Number = 200;

		//Category
		public static const CATEGORY_PLAYER:int 		= 0x0001;
		public static const CATEGORY_PLAYER_BULLET:int 	= 0x0002;
		public static const CATEGORY_TERRAIN:int 		= 0x0004;
		public static const CATEGORY_BLOCK:int 			= 0x0008;

		public function Main() {
			stage.scaleMode = "noScale";
			stage.align = "TL";

			var text:TextField = new TextField();
			text.text = "CLICK TO START!!!";
			text.x = text.y = 100;
			addChild(text);

			stage.addEventListener("click", function(event:Event):void {
				if(text.visible){
					Init();
				}
				text.visible = false;
			});
		}

		//どうも、クリックでフォーカスを当てないとBox2Dがめりこむので、コンストラクタから分離
		private function Init():void {
			{//Init Input
				stage.addEventListener(KeyboardEvent.KEY_DOWN, OnKeyDown);
				stage.addEventListener(KeyboardEvent.KEY_UP, OnKeyUp);
			}

			{//Init Box2D
				{//Common
					//AABB
					var worldAABB:b2AABB = new b2AABB();
					worldAABB.minVertex.Set(-100.0, -100.0);
					worldAABB.maxVertex.Set(100.0, 100.0);
					//Gravity
					var g:b2Vec2 = new b2Vec2(0.0, GRAVITY);
					//World
					m_world = new b2World(worldAABB, g, false);
				}

				{//Floor
					//Shape
					var wallSd:b2BoxDef = new b2BoxDef();
					{
						wallSd.extents.Set(FLOOR_W/2 / m_physScale, FLOOR_H/2 / m_physScale);
//						wallSd.localRotation = Math.random() * Math.PI / 8;
						wallSd.restitution = 0.9;
						wallSd.categoryBits = CATEGORY_TERRAIN;
					}
					//Body
					var wallBd:b2BodyDef = new b2BodyDef();
					{
						wallBd.position.Set(FLOOR_W/2 / m_physScale, (FLOOR_Y + FLOOR_H/2) / m_physScale);
						wallBd.AddShape(wallSd);
					}
					//Create
					m_world.CreateBody(wallBd);
				}

				{//Player
					//collision
					{
						//Shape
						var playerSd:b2CircleDef = new b2CircleDef();
						{
							playerSd.radius = PLAYER_R / m_physScale;
	//						playerSd.localRotation = Math.random() * Math.PI / 8;
							playerSd.density = 2;
							playerSd.friction = 0.0;
	//						playerSd.restitution = 0.0;
							playerSd.categoryBits = CATEGORY_PLAYER;
							playerSd.maskBits = CATEGORY_TERRAIN | CATEGORY_BLOCK;
						}
						//Body
						var playerBd:b2BodyDef = new b2BodyDef();
						{
							playerBd.AddShape(playerSd);
						}
						//Graphic
						{
							m_PlayerGraphic = CreateAASprite("┏(^o^)┛\n     ┛┓");
							playerBd.userData = m_PlayerGraphic;
						}

						//Create
						playerBd.position.Set(PLAYER_X / m_physScale, PLAYER_Y / m_physScale);
						m_Player = m_world.CreateBody(playerBd);
					}
				}

				{//Bullet Init
					//Shape
					var ballSd:b2CircleDef = new b2CircleDef();
					{
						ballSd.radius = BULLET_R / m_physScale;
//						ballSd.localRotation = Math.random() * Math.PI / 8;
						ballSd.density = 10;
						ballSd.friction = 0.2;
						ballSd.restitution = 0.9;
						ballSd.categoryBits = CATEGORY_PLAYER_BULLET;
						ballSd.maskBits = CATEGORY_TERRAIN | CATEGORY_BLOCK;
					}
					//Body
					m_BulletBd = new b2BodyDef();
					{
						m_BulletBd.AddShape(ballSd);
					}
					//Graphic
//					{
//						m_BulletBd.userData = CreateAASprite("@");
//					}
					//Create
//					for(var t:int = 0; t < 1; ++t){
//						ballBd.position.Set((300 + t*0.1) / m_physScale / 2, (100 - t*20) / m_physScale);
//						m_world.CreateBody(ballBd);
//					}
				}

				{//Blocks Init
					var sd:b2BoxDef = new b2BoxDef();
					sd.density = 1;
					sd.friction = 0.2;
					sd.extents.Set(BLOCK_W/2 / m_physScale, BLOCK_H/2 / m_physScale);
					sd.categoryBits = CATEGORY_BLOCK;
					var bd:b2BodyDef = new b2BodyDef();
					bd.AddShape(sd);
					for (var i:int = 0; i < 10; i++) {
						bd.position.Set(BLOCK_X / m_physScale, (FLOOR_Y - BLOCK_H/2 - i*BLOCK_H) / m_physScale);
						m_world.CreateBody(bd);
					}
				}
			}

			//毎フレームUpdateを呼ぶ
			addEventListener("enterFrame", function(event:Event):void {
				Update();
			});
		}

		//in_textを(Bitmapによって)表示するSpriteを作成する
		private function CreateAASprite(in_text:String):Sprite{
			//まずは文字列を普通に表示するものを作成
			var text_field:TextField = new TextField();
			{
				text_field.text = in_text;

				text_field.border = false;
				text_field.x = 0;
				text_field.y = 0;
				text_field.width = 100;
				text_field.height = 100;
			}

			//その内容をビットマップデータとして取り込む
			var bmp_data : BitmapData = new BitmapData( 100 , 100 , true , 0x00000000);
			{
				var matrix : Matrix = new Matrix(1,0,0,1,0,0);
				var color : ColorTransform = new ColorTransform(1,1,1,1,0,0,0,0);
				var rect : Rectangle = new Rectangle(0,0,400,300);
				bmp_data.draw(text_field, matrix, color, BlendMode.NORMAL, rect, true);
			}

			//取り込んだビットマップデータを表示
			var bmp_obj:Bitmap = new Bitmap( bmp_data , PixelSnapping.AUTO , true);
			{
				bmp_obj.x = -text_field.textWidth*0.5;
				bmp_obj.y = -text_field.textHeight*0.5;
			}

			//さらにそれを表示するスプライトを作成して返す
			var sprite:Sprite = new Sprite();
			{
				stage.addChild(sprite);

				sprite.addChild(bmp_obj);
			}

			return sprite;
		}

		//=Input=
		private function OnKeyDown(event:KeyboardEvent):void{
			if(event.keyCode == Keyboard.LEFT){
				m_InputL = 1;
				m_PlayerDir = -1.0;
				m_PlayerGraphic.scaleX = -1;
			}
			if(event.keyCode == Keyboard.RIGHT){
				m_InputR = 1;
				m_PlayerDir = 1.0;
				m_PlayerGraphic.scaleX = 1;
			}
			if(event.keyCode == Keyboard.UP){
				m_InputU = 1;
			}
			if(event.keyCode == Keyboard.DOWN){
				m_InputD = 1;
			}
			if(event.keyCode == Keyboard.SPACE){
				m_InputSpace = 1;
			}
			if(event.keyCode == KEY_S){
				//ここで生成してしまう
				Shot();
			}
		}
		private function OnKeyUp(event:KeyboardEvent):void{
			if(event.keyCode == Keyboard.LEFT){
				m_InputL = 0;
			}
			if(event.keyCode == Keyboard.RIGHT){
				m_InputR = 0;
			}
			if(event.keyCode == Keyboard.UP){
				m_InputU = 0;
			}
			if(event.keyCode == Keyboard.DOWN){
				m_InputD = 0;
			}
			if(event.keyCode == Keyboard.SPACE){
				m_InputSpace = 0;
			}
		}


		//=Shot=

		//弾の生成
		private function Shot():void{
			//Graphic
			{//毎回別のものを設定しないと、同じSpriteを動かしてしまうはず
				m_BulletBd.userData = CreateAASprite("o");
			}

			//右なら1、左なら-1
			var dir:Number = m_PlayerDir;

			//プレイヤ位置から発射
			m_BulletBd.position.Set(
				m_Player.m_position.x + dir*20.0/m_physScale,
				m_Player.m_position.y-(PLAYER_R/m_physScale)
			);
			//今のところ、右方向に一定速度で打ち出すのみ
			m_BulletBd.linearVelocity.Set(dir*BULLET_V / m_physScale, 0);
			//重力相殺を毎回行うので、記憶しておく(消失処理が追加で必要)
			m_Bullet.push(m_world.CreateBody(m_BulletBd));
		}


		//=Update=

		public function Update():void {
			if(!m_world) {
				return;
			}

			{//player update
				//入力にしたがって動かす
				m_Player.m_linearVelocity.x = PLAYER_V * (m_InputR - m_InputL);
				if(m_InputU > 0){
					m_Player.m_linearVelocity.y = -PLAYER_V;
				}
			}

			{//bullet update
				//重力を相殺するように力を加える
				m_Bullet.forEach(
					function(bullet:b2Body, index:int, arr:Array):void{
						bullet.m_force.Add(b2Math.MulFV(bullet.GetMass(), new b2Vec2(0.0, -GRAVITY)));
					}
				);
			}

			//Box2D Update
			m_world.Step(1.0 / 30.0, 10);

			// Render
			graphics.clear();
			for (var bb:b2Body = m_world.m_bodyList; bb; bb = bb.m_next) {
				//primitive描画っぽいもの
				for (var s:b2Shape = bb.GetShapeList(); s != null; s = s.GetNext()) {
					DrawShape(s);
				}

				//Spriteへの位置の反映
				if(bb.m_userData != null){
					bb.m_userData.x = bb.m_position.x * m_physScale;
					bb.m_userData.y = bb.m_position.y * m_physScale;
				}
			}
		}


		//=Render=

		public function DrawShape(shape:b2Shape):void {
			//box
			if(shape.m_type == b2Shape.e_polyShape) {
				var drawBox:Function = function():void{
					var poly:b2PolyShape = shape as b2PolyShape;
					var tV:b2Vec2 = b2Math.AddVV(poly.m_position, b2Math.b2MulMV(poly.m_R, poly.m_vertices[i]));

					graphics.beginFill(0x999999, 1);
					graphics.lineStyle(1,0xffffff,1);
					graphics.moveTo(tV.x * m_physScale, tV.y * m_physScale);

					for (var i:int = 0; i < poly.m_vertexCount; ++i) {
						var v:b2Vec2 = b2Math.AddVV(poly.m_position, b2Math.b2MulMV(poly.m_R, poly.m_vertices[i]));
						graphics.lineTo(v.x * m_physScale, v.y * m_physScale);
					}
					graphics.lineTo(tV.x * m_physScale, tV.y * m_physScale);

					graphics.endFill();
				};

				drawBox();
			}
/*
			//circle
			if(shape.m_type == b2Shape.e_circleShape) {
				var drawCircle:Function = function():void{
					var circle:b2CircleShape = shape as b2CircleShape;
					var pos:b2Vec2 = circle.m_position;
					var r:Number = circle.m_radius;

					graphics.beginFill(0x999999, 1);
					graphics.lineStyle(1,0xaaaaaa,1);

					graphics.drawCircle(pos.x * m_physScale, pos.y * m_physScale, r * m_physScale);

					graphics.endFill();
				};

				drawCircle();
			}
//*/
		}
	}
}