【連載】Starlingフレームワークを用いたStage3Dによる2Dアニメーション 第7回 StarlingフレームワークとBox2Dで物理演算シミュレーションを行う [Edit]

今回は、Starlingフレームワークで物理演算エンジンとして「Box2DFlash」(以下単にBox2Dと呼びます)を使ってみます。Box2DはC++から移植された物理エンジンです。描画の速いStage3Dなら、物理シミュレーションの処理も期待できます。本稿では、Box2Dのv2.1aを用います。

  • サンプルファイル: スクリプト004と005 (Starling_07.zip/Flash CS6形式/約12KB)
    ダウンロードファイルには、Starlingフレームワークのライブラリは含まれていません。予めインストールしてください。

01 Box2Dを使うには

Box2Dを実際に使い始める前に、いくつか準備や注意があります。まずは、Box2Dをインストールしなければなりません。Box2DFlashサイト(図001)の[Download]ページから「Box2DFlash 2.1a」のFlash Player 10版を手に入れます。そして、ダウンロードした「Source」フォルダの中の「Box2D」フォルダはソースパスに置きます。Starlingフレームワークで設定したソースパスのフォルダに一緒に入れて構いません。

図001■Box2DFlashのサイト
図001

そして、初めの注意です。Box2Dのv2.1aとv2.0.2では、バージョン番号は0.1の差でありながら、仕様が大きく変わりました。どれくらい変わったかというと、2.0.2のソースを2.1aで実行するとエラーが出まくって、ひとつひとつ修正するより書直した方が早いと思えるほどです。したがって、サンプルや解説を探すときには、バージョンを必ず確かめてください。

つぎに、物理シミュレーションで落下させるオブジェクトをつくります。小さな正方形のQuadインスタンスを、ひとつステージに置きました(図002)。Starlingルートクラス(MySprite)は、以下のとおりです。Box2Dはまだ使っていません。基準点(DisplayObject.pivotXとDisplayObject.pivotYプロパティ)はインスタンスの真ん中に定めたものの、座標はデフォルトの原点(0, 0)のままです。オブジェクトの位置は、Box2Dが物理演算で決めるからです。

図002■小さな正方形のオブジェクトをひとつステージに置いた
図002左

package {
	import starling.display.Sprite;
	import starling.display.Quad;
	import starling.events.Event;
	public class MySprite extends Sprite {
		private var boxEdge:Number = 20;
		public function MySprite() {
			addEventListener(Event.ADDED_TO_STAGE, initialize);
		}
		private function initialize(eventObject:Event):void {
			var myQuad:Quad = createQuad(boxEdge, boxEdge, 0x0000FF);
			addChild(myQuad);
		}
		private function createQuad(nWidth:Number, nHeight:Number, nColor:uint):Quad {
			var myQuad:Quad = new Quad(nWidth, nHeight, nColor);
			myQuad.pivotX = nWidth / 2;
			myQuad.pivotY = nHeight / 2;
			return myQuad;
		}
	}
}

ここで、物理シミュレーションになじみのない方のために、その考え方をご説明します。物理演算は計算ですので、結果は数値です。そのままでは、アニメーションが再生されたりはしません。演算した数値を、ステージに描いたオブジェクトのプロパティに当てはめて、初めてオブジェクトの動きとして目に見えるのです。

ステージのオブジェクトは、いわば操り人形です。Box2Dが黒子のように、自分自身の姿は隠し、オブジェクトを操って物理シミュレーションを演じて見せるということです。

02 物理空間と剛体を定める

それでは、Box2Dの物理空間をつくりましょう。b2Worldクラスのコンストラクタメソッドに、ふたつの引数を渡して呼出します。第1引数には重力を、2次元のベクトルのb2Vec2オブジェクトで定めます。第2引数はブール(論理)値で、trueを渡すと動かなくなったオブジェクトはシミュレーションから外し(スリープし)て、効率を高めます。

new b2World(重力, スリープ)

第1引数の重力は通常垂直(y軸)方向の力ですので、b2Vec2()コンストラクタに渡すx座標値は0にします。垂直方向のy座標値は、プロパティ(gravityVertical)に定めておきます。

new b2Vec2(x座標, y座標)

物理で扱うかたちの変わらない物体は「剛体」(rigid body)と呼ばれます。Box2Dはb2BodyDefオブジェクトで剛体を定義します。剛体定義のメソッド(defineBody())は新たに定めます。b2BodyDef()コンストラクタでインスタンスをつくったら、剛体の位置と種類をメソッドが受取った引数で決めます。位置はb2BodyDef.positionプロパティで参照されるb2Vec2オブジェクトに、b2Vec2.Set()メソッドを呼出して座標を与えます。剛体の種類はb2BodyDef.typeプロパティに設定します。動的な剛体の値はb2Body.b2_dynamicBodyです。

import Box2D.Common.Math.b2Vec2;
import Box2D.Dynamics.b2World;
import Box2D.Dynamics.b2BodyDef;
import Box2D.Dynamics.b2Body;
public class MySprite extends Sprite {
	private const SCALE:Number = 1 / 30;
	private var world:b2World;
	private var gravityVertical:Number = 10;
	// ...[中略]...
	private function initialize(eventObject:Event):void {
		world = new b2World(new b2Vec2(0, gravityVertical), true);
		var bodyDef:b2BodyDef = defineBody(stage.stageWidth / 2, 0, b2Body.b2_dynamicBody);
		// ...[中略]...
	}
	private function defineBody(nX:Number , nY:Number, nType:uint):b2BodyDef {
		var bodyDef:b2BodyDef = new b2BodyDef();
		bodyDef.position.Set(nX * SCALE, nY * SCALE);
		bodyDef.type = nType;
		return bodyDef;
	}
	// ...[中略]...
}

なお、Box2Dの物理空間はメートル(m)・キログラム(kg)・秒の単位(meters-kilogram-second)にもとづいてシミュレートされます。すると、その演算結果をFlashのステージにアニメーションとして表すには、地図のように縮尺比率が要ります。そこで、ピクセルからメートルへの換算比率(メートル/ピクセル)を定数(SCALE)に定めました。したがって、ピクセルで与えられた数値をBox2Dの座標として渡すときには、この定数値を乗じています。

さて、Box2Dには剛体のb2BodyDefオブジェクトが定義されました。けれども、ステージのQuadインスタンスとはまだ何の関わりもありません。b2BodyDef.userDataプロパティにQuadインスタンスを与えると、b2BodyDefオブジェクトからQuadインスタンスが参照できるようになります。Quadインスタンスをつくるメソッド(createQuad())に、b2BodyDefオブジェクトを引数(bodyDef)として加え、プロパティに定めました。

private function initialize(eventObject:Event):void {
	// ...[中略]...
	// var myQuad:Quad = createQuad(boxEdge, boxEdge, 0x0000FF);
	var myQuad:Quad = createQuad(boxEdge, boxEdge, 0x0000FF, bodyDef);
	// ...[中略]...
}
// private function createQuad(nWidth:Number, nHeight:Number, nColor:uint):Quad {
private function createQuad(nWidth:Number, nHeight:Number, nColor:uint, bodyDef:b2BodyDef):Quad {
	// ...[中略]...
	bodyDef.userData = myQuad;
	// ...[中略]...
}

もっともここまでのStarlingルートクラス(スクリプト001)は、黒子であるBox2Dのb2BodyDefオブジェクトに、人形となるQuadインスタンスがあてがわれただけです。実際、剛体定義のメソッド(defineBody())には水平座標(nX)としてステージの真ん中の値を与えているのに、Quadインスタンスは位置が変わりません。操る処理はこの後加えていきます。

スクリプト001■物理空間に剛体をつくってステージのオブジェクトと関連づけた

// ActionScript 3.0クラスファイル: MySprite.as
package {
	import starling.display.Sprite;
	import starling.display.Quad;
	import starling.events.Event;
	import Box2D.Common.Math.b2Vec2;
	import Box2D.Dynamics.b2World;
	import Box2D.Dynamics.b2BodyDef;
	import Box2D.Dynamics.b2Body;
	public class MySprite extends Sprite {
		private const SCALE:Number = 1 / 30;
		private var world:b2World;
		private var gravityVertical:Number = 10;
		private var boxEdge:Number = 20;
		public function MySprite() {
			addEventListener(Event.ADDED_TO_STAGE, initialize);
		}
		private function initialize(eventObject:Event):void {
			world = new b2World(new b2Vec2(0, gravityVertical), true);
			var bodyDef:b2BodyDef = defineBody(stage.stageWidth / 2, 0, b2Body.b2_dynamicBody);
			var myQuad:Quad = createQuad(boxEdge, boxEdge, 0x0000FF, bodyDef);
			addChild(myQuad);
		}
		private function defineBody(nX:Number, nY:Number, nType:uint):b2BodyDef {
			var bodyDef:b2BodyDef = new b2BodyDef();
			bodyDef.position.Set(nX * SCALE, nY * SCALE);
			bodyDef.type = nType;
			return bodyDef;
		}
		private function createQuad(nWidth:Number, nHeight:Number, nColor:uint, bodyDef:b2BodyDef):Quad {
			var myQuad:Quad = new Quad(nWidth, nHeight, nColor);
			myQuad.pivotX = nWidth / 2;
			myQuad.pivotY = nHeight / 2;
			bodyDef.userData = myQuad;
			return myQuad;
		}
	}
}

03 物理空間に加えた剛体をシミュレートする

剛体を物理シミュレートするには、まず剛体定義のb2BodyDefオブジェクトをb2World.CreateBody()メソッドの引数に渡して、物理空間に剛体をつくらなければなりません。

b2Worldオブジェクト.CreateBody(b2BodyDefオブジェクト)

つぎに、物理演算をフレームレートに合わせて進めます。物理空間の時間を進めるメソッドが、b2World.Step()です。第1引数は、シミュレーションするときに進める時間です。アニメーションさせるオブジェクトはEvent.ENTER_FRAMEイベントのリスナーで動かしますので、時間はフレームレートの逆数(1 / FPS)ずつ進めます。第2および第3引数は、物理的な制約に合わせて調整の再計算をさせる回数です。前者は速度、後者が位置についての演算を定めます[*1]。回数を増やすとシミュレーションの精度は高まるものの、処理の負荷が上がります。

b2Worldオブジェクト.Step(経過時間, 速度再計算, 位置再計算)

物理演算の時間を進めると、剛体の幾何情報が変わります。その値をステージのオブジェクトに与えることで、アニメーションが描けます。物理空間に加えられた剛体から初めのb2Bodyオブジェクトを取出すのが、b2World.GetBodyList()メソッドです。剛体の位置は、b2Body.GetPosition()メソッドによりb2Vec2オブジェクトとして得られます。また、その回転角は、b2Body.GetAngle()メソッドでラジアン角が返されます。

そして、剛体定義のb2BodyDef.userDataプロパティに与えたアニメーションさせるオブジェクトは、b2Body.GetUserData()メソッドで取出せます。この操り人形のオブジェクトに演算結果の位置や角度のプロパティを与えれば、物理シミュレーションが目に見えるように動きます。ただし前述のとおり、Box2Dの物理空間の座標の単位はメートルですから、換算比率(SCALE)で除してピクセルに直すのを忘れないようにしましょう。

private var velocityIterations:int = 8;
private var positionIterations:int = 3;
private var time:Number = 1 / 24;
// ...[中略]...
private function initialize(eventObject:Event):void {
	// ...[中略]...
	createBody(world, bodyDef);
	// ...[中略]...
	addEventListener(Event.ENTER_FRAME, update);
}
private function createBody(world:b2World, bodyDef:b2BodyDef):void {
	var body:b2Body = world.CreateBody(bodyDef);
}
private function update(eventObject:Event):void {
	world.Step(time, velocityIterations, positionIterations);
	var body:b2Body = world.GetBodyList();
	var myObject:DisplayObject = body.GetUserData() as DisplayObject;
	if (myObject) {
		var position:b2Vec2 = body.GetPosition();
		myObject.x = position.x / SCALE;
		myObject.y = position.y / SCALE;
		myObject.rotation = body.GetAngle();
	}
}

物理空間に剛体を加えるメソッド(createBody())とEvent.ENTER_FRAMEイベントのリスナーメソッド(update())は、上記のように定めました。なお、b2BodyDef.userDataプロパティにはどのようなデータも納められ、したがってその参照を得るb2Body.GetUserData()メソッドの戻り値にはデータ型が定められていません。そのため、型指定した変数(myObject)に値をとるには、as演算子でデータ型を評価し直します。また、b2World.GetBodyList()メソッドは、物理空間に剛体がないとnullを返します。そこで、戻り値(myObject)にオブジェクトがあるかどうか、ifステートメントで確かめています。

これで、ステージ中央上端に置かれたQuadインスタンスが、物理シミュレーションにしたがって落下します(図003)。ここまでのStarlingルートクラス(MySprite)の定義は、以下のスクリプト002のとおりです。

図003■物理シミュレーションにしたがってステージのオブジェクトが落下する
図003

スクリプト002■オブジェクトをBox2Dの物理シミュレーションに合わせて落下させる

// ActionScript 3.0クラス定義ファイル: MySprite.as
package {
	import starling.display.DisplayObject;
	import starling.display.Sprite;
	import starling.display.Quad;
	import starling.events.Event;
	import Box2D.Common.Math.b2Vec2;
	import Box2D.Dynamics.b2World;
	import Box2D.Dynamics.b2BodyDef;
	import Box2D.Dynamics.b2Body;
	public class MySprite extends Sprite {
		private const SCALE:Number = 1 / 30;
		private var world:b2World;
		private var gravityVertical:Number = 10;
		private var velocityIterations:int = 8;
		private var positionIterations:int = 3;
		private var time:Number = 1 / 24;
		private var boxEdge:Number = 20;
		public function MySprite() {
			addEventListener(Event.ADDED_TO_STAGE, initialize);
		}
		private function initialize(eventObject:Event):void {
			world = new b2World(new b2Vec2(0, gravityVertical), true);
			var bodyDef:b2BodyDef = defineBody(stage.stageWidth / 2, 0, b2Body.b2_dynamicBody);
			var myQuad:Quad = createQuad(boxEdge, boxEdge, 0x0000FF, bodyDef);
			createBody(world, bodyDef);
			addChild(myQuad);
			addEventListener(Event.ENTER_FRAME, update);
		}
		private function defineBody(nX:Number, nY:Number, nType:uint):b2BodyDef {
			var bodyDef:b2BodyDef = new b2BodyDef();
			bodyDef.position.Set(nX * SCALE, nY * SCALE);
			bodyDef.type = nType;
			return bodyDef;
		}
		private function createQuad(nWidth:Number, nHeight:Number, nColor:uint, bodyDef:b2BodyDef):Quad {
			var myQuad:Quad = new Quad(nWidth, nHeight, nColor);
			myQuad.pivotX = nWidth / 2;
			myQuad.pivotY = nHeight / 2;
			bodyDef.userData = myQuad;
			return myQuad;
		}
		private function createBody(world:b2World, bodyDef:b2BodyDef):b2Body {
			var body:b2Body = world.CreateBody(bodyDef);
		}
		private function update(eventObject:Event):void {
			world.Step(time, velocityIterations, positionIterations);
			var body:b2Body = world.GetBodyList();
			var myObject:DisplayObject = body.GetUserData() as DisplayObject;
			if (myObject) {
				var position:b2Vec2 = body.GetPosition();
				myObject.x = position.x / SCALE;
				myObject.y = position.y / SCALE;
				myObject.rotation = body.GetAngle();
			}
		}
	}
}

[*1] b2World.Step()メソッドの第2引数が定めるのは、剛体の移動についての演算です。そして剛体同士が重なると、第3引数の位置についての再計算が行われます。すると、速度から求めた移動先からずれますので、精度を高めるには改めて演算し直さなければならないでしょう。ふたつの引数は、このような調整の計算を行う回数の定めなのです。

再計算の引数ふたつの説明は、C++用の「Box2D v2.2.0 User Manual」2.4「Simulating the World (of Box2D)」に記載されています。再計算の回数は、速度について8回、位置は3回が目安とされています。

04 物理空間に静的な剛体を加える

矩形がただ落ちるだけでは、物理シミュレーションとしてもの足りません。ステージの下端に床を置きましょう。動かない床は、静的な剛体として定義します。静的な剛体はb2BodyDef.typeプロパティに、b2Body.b2_staticBodyを定めます。床の静的な剛体と落下させる動的な剛体は、それぞれ別のメソッド(createStaticFloor()とcreateDynamicBox())を定めてつくることにします。ステージに置く操り人形にはやはりQuadインスタンスを使いますので、今のところ剛体の種類以外は同じ処理の流れです。

private function initialize(eventObject:Event):void {
	// ...[中略]...
	var floorQuad:Quad = createStaticFloor(nCenterX, nStageHeight - nFloorHeight, nFloorWidth, nFloorHeight, 0xCCCCCC);
	addChild(floorQuad);
	var myQuad:Quad = createDynamicBox(nCenterX, 0, boxEdge, boxEdge, 0x0000FF);
	addChild(myQuad);
	// ...[中略]...
}
private function createStaticFloor(nX:Number, nY:Number, nWidth:Number, nHeight:Number, nColor:uint):Quad {
	var bodyDef:b2BodyDef = defineBody(nX, nY, b2Body.b2_staticBody);
	var myQuad:Quad = createQuad(nWidth, nHeight, nColor, bodyDef);
	createBody(world, bodyDef);
	return myQuad;
}
private function createDynamicBox(nX:Number, nY:Number, nWidth:Number, nHeight:Number, nColor:uint):Quad {
	var bodyDef:b2BodyDef = defineBody(nX, nY, b2Body.b2_dynamicBody);
	var myQuad:Quad = createQuad(nWidth, nHeight, nColor, bodyDef);
	createBody(world, bodyDef);
	return myQuad;
}

ここで慌ててアニメーションを確かめては早すぎます。Event.ENTER_FRAMEイベントのリスナーメソッド(update())では、物理空間に初めに加えた落下する剛体しかシミュレートしていません。ですから、床はデフォルト位置に取り残されてしまうのです(図004)。

図004■床が物理シミュレートに加わっていない
図004

リスナーメソッド(update())でつぎの剛体を取出します。用いるメソッドは、b2Body.GetNext()です。初めのb2Bodyオブジェクトは、b2World.GetBodyList()メソッドでb2Worldオブジェクトからもらいました。そこから順につぎの剛体を得るには、b2Body.GetNext()メソッドでb2Bodyオブジェクトに尋ねることにご注意ください[*2]。剛体がさらに加わる場合に備えて、whileループでオブジェクトがなくなる(nullになる)まで処理は繰返しています。

private function update(eventObject:Event):void {
	world.Step(time, velocityIterations, positionIterations);
	var body:b2Body = world.GetBodyList();
	while (body) {
		var myObject:DisplayObject = body.GetUserData() as DisplayObject;
		if (myObject) {
			var position:b2Vec2 = body.GetPosition();
			myObject.x = position.x / SCALE;
			myObject.y = position.y / SCALE;
			myObject.rotation = body.GetAngle();
		}
		body = body.GetNext();
	}
}

できあがったStarlingルートクラスは、以下のスクリプト003のとおりです。これで、床は正しい位置に置かれます。けれど、まだおしまいではありません。試してみると、落下した矩形が床を抜けるイリュージョンになります(図005)。

図005■落下する矩形が床をすり抜ける
図005

スクリプト003■落下する剛体と静的な剛体の物理シミュレーション

// ActionScript 3.0クラス定義ファイル: MySprite.as
package {
	import starling.display.DisplayObject;
	import starling.display.Sprite;
	import starling.display.Quad;
	import starling.events.Event;
	import Box2D.Common.Math.b2Vec2;
	import Box2D.Dynamics.b2World;
	import Box2D.Dynamics.b2BodyDef;
	import Box2D.Dynamics.b2Body;
	public class MySprite extends Sprite {
		private const SCALE:Number = 1 / 30;
		private var world:b2World;
		private var gravityVertical:Number = 10;
		private var velocityIterations:int = 8;
		private var positionIterations:int = 3;
		private var time:Number = 1 / 24;
		private var boxEdge:Number = 20;
		public function MySprite() {
			addEventListener(Event.ADDED_TO_STAGE, initialize);
		}
		private function initialize(eventObject:Event):void {
			var nStageWidth:Number = stage.stageWidth;
			var nStageHeight:Number = stage.stageHeight;
			var nCenterX:Number = nStageWidth / 2;
			var nFloorWidth:Number = nStageWidth * 0.8;
			var nFloorHeight:Number = 20;
			world = new b2World(new b2Vec2(0, gravityVertical), true);
			var floorQuad:Quad = createStaticFloor(nCenterX, nStageHeight - nFloorHeight, nFloorWidth, nFloorHeight, 0xCCCCCC);
			addChild(floorQuad);
			var myQuad:Quad = createDynamicBox(nCenterX, 0, boxEdge, boxEdge, 0x0000FF);
			addChild(myQuad);
			addEventListener(Event.ENTER_FRAME, update);
		}
		private function createStaticFloor(nX:Number, nY:Number, nWidth:Number, nHeight:Number, nColor:uint):Quad {
			var bodyDef:b2BodyDef = defineBody(nX, nY, b2Body.b2_staticBody);
			var myQuad:Quad = createQuad(nWidth, nHeight, nColor, bodyDef);
			createBody(world, bodyDef);
			return myQuad;
		}
		private function createDynamicBox(nX:Number, nY:Number, nWidth:Number, nHeight:Number, nColor:uint):Quad {
			var bodyDef:b2BodyDef = defineBody(nX, nY, b2Body.b2_dynamicBody);
			var myQuad:Quad = createQuad(nWidth, nHeight, nColor, bodyDef);
			createBody(world, bodyDef);
			return myQuad;
		}
		private function defineBody(nX:Number , nY:Number, nType:uint):b2BodyDef {
			var bodyDef:b2BodyDef = new b2BodyDef();
			bodyDef.position.Set(nX * SCALE, nY * SCALE);
			bodyDef.type = nType;
			return bodyDef;
		}
		private function createQuad(nWidth:Number, nHeight:Number, nColor:uint, bodyDef:b2BodyDef):Quad {
			var myQuad:Quad = new Quad(nWidth, nHeight, nColor);
			myQuad.pivotX = nWidth / 2;
			myQuad.pivotY = nHeight / 2;
			bodyDef.userData = myQuad;
			return myQuad;
		}
		private function createBody(world:b2World, bodyDef:b2BodyDef):b2Body {
			var body:b2Body = world.CreateBody(bodyDef);
		}
		private function update(eventObject:Event):void {
			world.Step(time, velocityIterations, positionIterations);
			var body:b2Body = world.GetBodyList();
			while (body) {
				var myObject:DisplayObject = body.GetUserData() as DisplayObject;
				if (myObject) {
					var position:b2Vec2 = body.GetPosition();
					myObject.x = position.x / SCALE;
					myObject.y = position.y / SCALE;
					myObject.rotation = body.GetAngle();
				}
				body = body.GetNext();
			}
		}
	}
}

[*2] ひとつにまとめたオブジェクトのそれぞれに、つぎのオブジェクトの参照をもたせて順序づける仕組みは「連結リスト(linked list)」と呼ばれます。

05 物理シミュレーションにフィクスチャを加える

前掲スクリプト003では、落下する矩形が床をすり抜けました。けれど、私たちが「矩形」とか「床」とわかるのは、Quadインスタンスのかたちを見ているからです。黒子のBox2Dには、剛体定義(defineBody())で座標と動くか動かないかしか伝えていません。剛体のかたちも大きさも、Box2Dのあずかり知らぬことなのです。

剛体のかたちや質感は、フィクスチャというもので定めます。フィクスチャの定義をb2FixtureDefオブジェクトでつくり、b2FixtureDef.shapeプロパティにかたちを与えます。かたちを定めるのはBox2Dのシェイブ(b2Shape)オブジェクトです。矩形はb2PolygonShapeオブジェクトにb2PolygonShape.SetAsBox()メソッドで幅と高さを引数に渡してつくります。

フィクスチャの定義と矩形シェイプをつくるのは、それぞれ新たなメソッド(defineFixture()とcreateBoxShape())として定めました。そして、剛体をつくるメソッド(createBody())の引数にb2FixtureDefオブジェクトが加わります。剛体にフィクスチャを与えるのはb2Body.CreateFixture()メソッドで、引数にb2FixtureDefオブジェクトを渡します。

private function createDynamicBox(nX:Number, nY:Number, nWidth:Number, nHeight:Number, nColor:uint):Quad {
	// ...[中略]...
	var boxShape:b2PolygonShape = createBoxShape(boxEdge / 2, boxEdge / 2);
	var fixtureDef:b2FixtureDef = defineFixture(boxShape);
	// createBody(world, bodyDef);
	createBody(world, bodyDef, fixtureDef);
}
private function defineFixture(myShape:b2PolygonShape):b2FixtureDef {
	var fixtureDef:b2FixtureDef = new b2FixtureDef();
	fixtureDef.shape = myShape;
	return fixtureDef;
}
private function createBoxShape(nX:Number, nY:Number):b2PolygonShape {
	var myShape:b2PolygonShape = new b2PolygonShape();
	myShape.SetAsBox(nX * SCALE, nY * SCALE);
	return myShape;
}
// private function createBody(world:b2World, bodyDef:b2BodyDef):b2Body {
private function createBody(world:b2World, bodyDef:b2BodyDef, fixtureDef:b2FixtureDef):void {
	var body:b2Body = world.CreateBody(bodyDef);
	body.CreateFixture(fixtureDef);
}

なお、b2PolygonShape.SetAsBox()メソッドでつくる矩形は原点となる中心から幅と高さを測るので、矩形そのものの幅と高さの1/2を引数に渡します。

これでBox2Dにふたつの剛体のかたちが伝わりました。つまり、互いの衝突がわかるということです。一応、物理シミュレーションとしては成立ちます。けれど、不満を感じるでしょう。矩形は床に落ちても弾みもせず、吸いつくように止まります(図006)。鉄棒や体操の着地ならみごとです。

図006■落下した矩形は床の上に吸いつくように着地する
図006

Box2Dの話しもここまでくると、そろそろ理由の見当はついてくるでしょう。黒子が操り人形のキャラ設定を知らない。つまり、落とした剛体が弾むような材質であることを、Box2Dに教えていないからです。

フィクスチャの定義には、密度と摩擦および弾性の3つの性質がプロパティで定められます。密度がb2FixtureDef.densityプロパティです。3次元空間なら体積当たりの重さとなるところ、Box2Dは2次元ですのでkg/m2が単位となります。摩擦と弾性はそれぞれb2FixtureDef.frictionb2FixtureDef.restitutionプロパティで、ともに0から1までの範囲の値を基本として定めます。

private function createDynamicBox(nX:Number, nY:Number, nWidth:Number, nHeight:Number, nColor:uint):Quad {
	// ...[中略]...
	var fixtureDef:b2FixtureDef = defineFixture(boxShape);
	setFixtureDef(fixtureDef, 1, 0.5, 0.5);
	// ...[中略]...
}
private function setFixtureDef(fixtureDef:b2FixtureDef, density:Number, friction:Number, restitution:Number = 0):void {
	fixtureDef.density = density;
	fixtureDef.friction = friction;
	fixtureDef.restitution = restitution;
}

密度と摩擦および弾性の3つを新たなメソッド(setFixtureDef())で定め、落下する剛体をつくるメソッド(createDynamicBox())から呼出します。これでようやく、落とした矩形が床で弾みます(図007)。できあがったStarlingルートクラスは、スクリプト004のとおりです。

図007■落下した矩形のオブジェクトが床で弾む
図007

スクリプト004■動的な剛体を静的な剛体の上に落下させる物理シミュレーション

// ActionScript 3.0クラス定義ファイル: MySprite.as
package {
	import starling.display.DisplayObject;
	import starling.display.Sprite;
	import starling.display.Quad;
	import starling.events.Event;
	import Box2D.Common.Math.b2Vec2;
	import Box2D.Dynamics.b2World;
	import Box2D.Dynamics.b2BodyDef;
	import Box2D.Dynamics.b2Body;
	import Box2D.Dynamics.b2FixtureDef;
	import Box2D.Collision.Shapes.b2PolygonShape;
	public class MySprite extends Sprite {
		private const SCALE:Number = 1 / 30;
		private var world:b2World;
		private var gravityVertical:Number = 10;
		private var velocityIterations:int = 8;
		private var positionIterations:int = 3;
		private var time:Number = 1 / 24;
		private var boxEdge:Number = 20;
		public function MySprite() {
			addEventListener(Event.ADDED_TO_STAGE, initialize);
		}
		private function initialize(eventObject:Event):void {
			var nStageWidth:Number = stage.stageWidth;
			var nStageHeight:Number = stage.stageHeight;
			var nCenterX:Number = nStageWidth / 2;
			var nFloorWidth:Number = nStageWidth * 0.8;
			var nFloorHeight:Number = 20;
			world = new b2World(new b2Vec2(0, gravityVertical), true);
			var floorQuad:Quad = createStaticFloor(nCenterX, nStageHeight - nFloorHeight, nFloorWidth, nFloorHeight, 0xCCCCCC);
			addChild(floorQuad);
			var myQuad:Quad = createDynamicBox(nCenterX, 0, boxEdge, boxEdge, 0x0000FF);
			addChild(myQuad);
			addEventListener(Event.ENTER_FRAME, update);
		}
		private function createStaticFloor(nX:Number, nY:Number, nWidth:Number, nHeight:Number, nColor:uint):Quad {
			var bodyDef:b2BodyDef = defineBody(nX, nY, b2Body.b2_staticBody);
			var myQuad:Quad = createQuad(nWidth, nHeight, nColor, bodyDef);
			var boxShape:b2PolygonShape = createBoxShape(nWidth / 2, nHeight / 2);
			var fixtureDef:b2FixtureDef = defineFixture(boxShape);
			createBody(world, bodyDef, fixtureDef);
			return myQuad;
		}
		private function createDynamicBox(nX:Number, nY:Number, nWidth:Number, nHeight:Number, nColor:uint):Quad {
			var bodyDef:b2BodyDef = defineBody(nX, nY, b2Body.b2_dynamicBody);
			var myQuad:Quad = createQuad(nWidth, nHeight, nColor, bodyDef);
			var boxShape:b2PolygonShape = createBoxShape(nWidth / 2, nHeight / 2);
			var fixtureDef:b2FixtureDef = defineFixture(boxShape);
			setFixtureDef(fixtureDef, 1, 0.5, 0.5);
			createBody(world, bodyDef, fixtureDef);
			return myQuad;
		}
		private function defineBody(nX:Number , nY:Number, nType:uint):b2BodyDef {
			var bodyDef:b2BodyDef = new b2BodyDef();
			bodyDef.position.Set(nX * SCALE, nY * SCALE);
			bodyDef.type = nType;
			return bodyDef;
		}
		private function createQuad(nWidth:Number, nHeight:Number, nColor:uint, bodyDef:b2BodyDef):Quad {
			var myQuad:Quad = new Quad(nWidth, nHeight, nColor);
			myQuad.pivotX = nWidth / 2;
			myQuad.pivotY = nHeight / 2;
			bodyDef.userData = myQuad;
			return myQuad;
		}
		private function defineFixture(myShape:b2PolygonShape):b2FixtureDef {
			var fixtureDef:b2FixtureDef = new b2FixtureDef();
			fixtureDef.shape = myShape;
			return fixtureDef;
		}
		private function createBoxShape(nX:Number, nY:Number):b2PolygonShape {
			var myShape:b2PolygonShape = new b2PolygonShape();
			myShape.SetAsBox(nX * SCALE, nY * SCALE);
			return myShape;
		}
		private function setFixtureDef(fixtureDef:b2FixtureDef, density:Number, friction:Number, restitution:Number = 0):void {
			fixtureDef.density = density;
			fixtureDef.friction = friction;
			fixtureDef.restitution = restitution;
		}
		private function createBody(world:b2World, bodyDef:b2BodyDef, fixtureDef:b2FixtureDef):void {
			var body:b2Body = world.CreateBody(bodyDef);
			body.CreateFixture(fixtureDef);
		}
		private function update(eventObject:Event):void {
			world.Step(time, velocityIterations, positionIterations);
			var body:b2Body = world.GetBodyList();
			while (body) {
				var myObject:DisplayObject = body.GetUserData() as DisplayObject;
				if (myObject) {
					var position:b2Vec2 = body.GetPosition();
					myObject.x = position.x / SCALE;
					myObject.y = position.y / SCALE;
					myObject.rotation = body.GetAngle();
				}
				body = body.GetNext();
			}
		}
	}
}

物理シミュレーションで手間がかかるのは、目の前で動かす人形とは別に、物理空間の黒子に立ち位置やふるまいを予め教えておかなければならないからです。けれど、ひとたび設定が決まれば、位置を変えたり、剛体の個数を増やしても、アニメーションはすべて物理演算エンジンに任せてしまえます。

おまけとして、落下する剛体の数を50個に増やしてみました(図008)。動的な剛体をループ処理によりつくっただけで、ルートクラスに新たなメソッドは加えていませんし、Box2Dについてとくに補う説明もありません。興味のある方は、以下のスクリプト005とサンプルファイルでご覧ください。

図008■50個の矩形を床に落とす
図008左   図008右

スクリプト005■動的な剛体50個を静的な剛体の上に落下させる物理シミュレーション

// ActionScript 3.0クラス定義ファイル: MySprite.as
package {
	import starling.display.DisplayObject;
	import starling.display.Sprite;
	import starling.display.Quad;
	import starling.events.Event;
	import Box2D.Common.Math.b2Vec2;
	import Box2D.Dynamics.b2World;
	import Box2D.Dynamics.b2BodyDef;
	import Box2D.Dynamics.b2Body;
	import Box2D.Dynamics.b2FixtureDef;
	import Box2D.Collision.Shapes.b2PolygonShape;
	public class MySprite extends Sprite {
		private const SCALE:Number = 1 / 30;
		private var world:b2World;
		private var gravityVertical:Number = 10;
		private var velocityIterations:int = 8;
		private var positionIterations:int = 3;
		private var time:Number = 1 / 24;
		private var boxEdge:Number = 20;
		public function MySprite() {
			addEventListener(Event.ADDED_TO_STAGE, initialize);
		}
		private function initialize(eventObject:Event):void {
			var nStageWidth:Number = stage.stageWidth;
			var nStageHeight:Number = stage.stageHeight;
			var nCenterX:Number = nStageWidth / 2;
			var nFloorWidth:Number = nStageWidth * 0.8;
			var nFloorHeight:Number = 20;
			world = new b2World(new b2Vec2(0, gravityVertical), true);
			var floorQuad:Quad = createStaticFloor(nCenterX, nStageHeight - nFloorHeight, nFloorWidth, nFloorHeight, 0xCCCCCC);
			addChild(floorQuad);
			var i:int = 50 + 1;
			while (--i) {
				var nX:Number = Math.random() * nStageWidth;
				var nY:Number = -Math.random() * nStageHeight;
				var nColor:uint = Math.floor(Math.random() * 0xFFFFFF);
				var myQuad:Quad = createDynamicBox(nX, nY, boxEdge, boxEdge, nColor);
				addChild(myQuad);
			}
			addEventListener(Event.ENTER_FRAME, update);
		}
		private function createStaticFloor(nX:Number, nY:Number, nWidth:Number, nHeight:Number, nColor:uint):Quad {
			var bodyDef:b2BodyDef = defineBody(nX, nY, b2Body.b2_staticBody);
			var myQuad:Quad = createQuad(nWidth, nHeight, nColor, bodyDef);
			var boxShape:b2PolygonShape = createBoxShape(nWidth / 2, nHeight / 2);
			var fixtureDef:b2FixtureDef = defineFixture(boxShape);
			createBody(world, bodyDef, fixtureDef);
			return myQuad;
		}
		private function createDynamicBox(nX:Number, nY:Number, nWidth:Number, nHeight:Number, nColor:uint):Quad {
			var bodyDef:b2BodyDef = defineBody(nX, nY, b2Body.b2_dynamicBody);
			var myQuad:Quad = createQuad(nWidth, nHeight, nColor, bodyDef);
			var boxShape:b2PolygonShape = createBoxShape(nWidth / 2, nHeight / 2);
			var fixtureDef:b2FixtureDef = defineFixture(boxShape);
			setFixtureDef(fixtureDef, 1, 0.5, 0.5);
			createBody(world, bodyDef, fixtureDef);
			return myQuad;
		}
		private function defineBody(nX:Number , nY:Number, nType:uint):b2BodyDef {
			var bodyDef:b2BodyDef = new b2BodyDef();
			bodyDef.position.Set(nX * SCALE, nY * SCALE);
			bodyDef.type = nType;
			return bodyDef;
		}
		private function createQuad(nWidth:Number, nHeight:Number, nColor:uint, bodyDef:b2BodyDef):Quad {
			var myQuad:Quad = new Quad(nWidth, nHeight, nColor);
			myQuad.pivotX = nWidth / 2;
			myQuad.pivotY = nHeight / 2;
			bodyDef.userData = myQuad;
			return myQuad;
		}
		private function defineFixture(myShape:b2PolygonShape):b2FixtureDef {
			var fixtureDef:b2FixtureDef = new b2FixtureDef();
			fixtureDef.shape = myShape;
			return fixtureDef;
		}
		private function createBoxShape(nX:Number, nY:Number):b2PolygonShape {
			var myShape:b2PolygonShape = new b2PolygonShape();
			myShape.SetAsBox(nX * SCALE, nY * SCALE);
			return myShape;
		}
		private function setFixtureDef(fixtureDef:b2FixtureDef, density:Number, friction:Number, restitution:Number = 0):void {
			fixtureDef.density = density;
			fixtureDef.friction = friction;
			fixtureDef.restitution = restitution;
		}
		private function createBody(world:b2World, bodyDef:b2BodyDef, fixtureDef:b2FixtureDef):void {
			var body:b2Body = world.CreateBody(bodyDef);
			body.CreateFixture(fixtureDef);
		}
		private function update(eventObject:Event):void {
			world.Step(time, velocityIterations, positionIterations);
			var body:b2Body = world.GetBodyList();
			while (body) {
				var myObject:DisplayObject = body.GetUserData() as DisplayObject;
				if (myObject) {
					var position:b2Vec2 = body.GetPosition();
					myObject.x = position.x / SCALE;
					myObject.y = position.y / SCALE;
					myObject.rotation = body.GetAngle();
				}
				body = body.GetNext();
			}
		}
	}
}

 

コメント

この記事にコメントを書く

記事に対するテクニカルな質問はご遠慮ください(利用規約)。

その他の記事