サイトトップ

Director Flash 書籍 業務内容 プロフィール

Adobe Flash非公式テクニカルノート

Starlingフレームワークで物理演算エンジンBox2Dを使う

ID: FN1202006 Product: Flash CS5 and above Platform: All Version: 11 and above/ActionScript 3.0

Starlingフレームワークで使える物理演算エンジンとして「Box2DFlash」(以下単にBox2Dと呼びます)を採上げます。バージョンによって、細かい仕様が変わることもありますのでご注意ください。本稿ではバージョン2.1aを使います。

なお、Starlingフレームワークを使うには、Flash Player 11のSWFを書出すことができ、Starlingフレームワークをインストールしなければなりません。まだ済ませていない方は、つぎのふたつのノートをお読みください。また、Starlingフレームワークのごく基本的な使い方は「Starlingフレームワークを使う」に解説しました。

  1. Flash CS5/CS5.5でFlash Player 11のSWFを書出す
  2. Starlingフレームワークをインストールする

01 Box2Dをインストールする
Box2DはC++から移植された物理エンジンです。サイト(図001)のDownloadページから「Box2DFlash 2.1a」のFlash Player 10版を手に入れます。そして、ダウンロードしたファイルの中の「Box2D」フォルダはソースパスに置きます。Starlingフレームワークで設定したソースパスのフォルダに一緒に入れて構いません(前出「Starlingフレームワークをインストールする」03「[パブリッシュ設定]によるActionScriptファイルのパスの指定」の項参照)。

図001■Box2DFlashのサイト
図001


02 物理空間と剛体を定める
本稿でこれから定義するのは、Starlingフレームワークの表示リスト最上位(ルート)に置く表示オブジェクトをつくるクラス(MySprite)です。FLAファイル(ASファイルと同階層)には、そのクラスのオブジェクトをtarlingフレームワークに加えるつぎのフレームアクションが書かれているものとします(前出「Starlingフレームワークを使う」03「Starlingフレームワークの初期化」スクリプト002参照)。

// フレームアクション: メインタイムライン
import starling.core.Starling;
var myStarling:Starling = new Starling(MySprite, stage);
myStarling.start();

物理演算エンジンが行うのは、基本的に物理空間に置かれた物体がどのように動くかという計算です。つまり、そのままでは目に見えません。演算結果をActionScriptの表示オブジェクトに反映して、初めてアニメーションとして見ることができるのです。

その演算結果を出すまでに、踏まなければいけない手続きがあります。まず、シミュレーションの3つの準備をしましょう。

    【物理演算シミュレーションの準備】
  1. 物理ワールドをつくる
  2. 剛体を定義する
  3. 剛体定義に表示オブジェクトを関連づける

第1に、重力の与えられた物理空間をつくります。クラスはb2Worldですので、「物理ワールド」と呼んでおきます。このb2Worldインスタンスが、物理演算シミュレーションのおおもとです。

第2に、物理エンジンで扱う物体となる「剛体(Rigid body)」を定義します。Box2Dでは剛体そのものの前に、その定義をb2BodyDefオブジェクトとしてつくります。

第3に、物理演算の結果を反映する表示オブジェクトは、剛体の定義と関連づけなければなりません。今回は、StarlingフレームワークのQuadオブジェクトを用います。

物理演算シミュレーションの準備まで終えたクラス定義は、スクリプト001として後に掲げます。その中から3つの準備に当たる処理をそれぞれ抜出してご説明します(行番号はスクリプト001にもとづきます)。

物理ワールドをつくるb2Worldクラスのコンストラクタメソッドは、ふたつの引数を取ります[*1]。第1引数の重力は、2次元のベクトルを定めるb2Vec2オブジェクトで定めます。b2Vec2()コンストラクタにはxy座標をふたつの引数として渡します。b2World()コンストラクタの第2引数のスリープはブール(論理)値です。trueを渡すと、動かなくなった剛体はシミュレーションから外し(スリープし)て、効率を高めます。

new b2World(重力, スリープ)

重力は通常垂直(y軸)方向の力ですので、b2Vec2()コンストラクタに渡す第1引数のx座標値は0にしました(第18行目)。また、垂直方向のy座標値は、変数(gravityVertical)に定めています(第12行目)。生成したb2Worldインスタンスは、privateのプロパティ(world)に納めます(第11および第18行目)。

  1. private var world:b2World;
  2. private var gravityVertical:Number = 10;
  1.   world = new b2World(new b2Vec2(0, gravityVertical), true);

後で落下させる予定の剛体は、1辺20ピクセルの正方形とし、その長さを変数(boxEdge)に置きます(第13行目)。そして、剛体をつくる処理はメソッド(createBodyDef())に定めました(第23〜28行目)。メソッドの第1および第2引数(nXとnY)は、剛体のxy座標です。第3引数(nType)は剛体の種類で、b2Bodyの静的プロパティが定める整数とします[*2]

b2BodyDefクラスのコンストラクタメソッドでインスタンスをつくったら、剛体の位置と種類を決めます(第24〜26行目)。位置はb2BodyDef.positionプロパティでb2Vec2オブジェクトを参照し、b2Vec2.Set()メソッドにより座標を定めます。剛体の種類はb2BodyDef.typeプロパティに設定します。動的な剛体の値はb2Body.b2_dynamicBodyです。

  1. private const SCALE:Number = 1 / 30;
  1. private var boxEdge:Number = 20;
  1.   var bodyDef:b2BodyDef = createBodyDef(stage.stageWidth / 2, boxEdge, b2Body.b2_dynamicBody);
  1. private function createBodyDef(nX:Number, nY:Number, nType:uint = 0):b2BodyDef {
  2.   var bodyDef:b2BodyDef = new b2BodyDef();
  3.   bodyDef.position.Set(nX * SCALE, nY * SCALE);
  4.   bodyDef.type = nType;
  5.   return bodyDef;
  6. }

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

そして、Quadインスタンスの生成とb2BodyDefオブジェクトへの関連づけです。この処理もメソッド(createQuadForBodyDef())として定めました(第29〜35行目)。つくったQuadオブジェクトは、b2BodyDef.userDataプロパティに設定して剛体の定義と関連づけます(第33行目)。剛体を表す表示オブジェクトの基準点は、基本的には中心(重心)に置きます(第31〜32行目)。

  1.   var myQuad:Quad = createQuadForBodyDef(bodyDef, boxEdge, boxEdge, 0x0000FF);
  2.   addChild(myQuad);
  1. private function createQuadForBodyDef(bodyDef:b2BodyDef, nWidth:Number, nHeight:Number,nColor:uint=0):Quad {
  2.   var myQuad:Quad = new Quad(nWidth, nHeight, nColor);
  3.   myQuad.pivotX = myQuad.width / 2;
  4.   myQuad.pivotY = myQuad.height / 2;
  5.   bodyDef.userData = myQuad;
  6.   return myQuad;
  7. }

これら3つの準備をまとめてクラス(MySprite)として定めたのが、つぎのスクリプト001です。まだ剛体の定義までしかしていませんので、Quadインスタンスはステージに置かれるだけで動きません。

スクリプト001■物理演算シミュレーションの準備までのクラス定義
    // ActionScript 3.0クラス定義ファイル: MySprite.as
  1. package {
  2.   import starling.display.Sprite;
  3.   import starling.display.Quad;
  4.   import starling.events.Event;
  5.   import Box2D.Common.Math.b2Vec2;
  6.   import Box2D.Dynamics.b2World;
  7.   import Box2D.Dynamics.b2BodyDef;
  8.   import Box2D.Dynamics.b2Body;
  9.   public class MySprite extends Sprite {
  10.     private const SCALE:Number = 1 / 30;
  11.     private var world:b2World;
  12.     private var gravityVertical:Number = 10;
  13.     private var boxEdge:Number = 20;
  14.     public function MySprite() {
  15.       addEventListener(Event.ADDED_TO_STAGE, initialize);
  16.     }
  17.     private function initialize(eventObject:Event):void {
  18.       world = new b2World(new b2Vec2(0, gravityVertical), true);
  19.       var bodyDef:b2BodyDef = createBodyDef(stage.stageWidth / 2, boxEdge, b2Body.b2_dynamicBody)
  20.       var myQuad:Quad = createQuadForBodyDef(bodyDef, boxEdge, boxEdge, 0x0000FF);
  21.       addChild(myQuad);
  22.     }
  23.     private function createBodyDef(nX:Number , nY:Number, nType:uint = 0):b2BodyDef {
  24.       var bodyDef:b2BodyDef = new b2BodyDef();
  25.       bodyDef.position.Set(nX * SCALE, nY * SCALE);
  26.       bodyDef.type = nType;
  27.       return bodyDef;
  28.     }
  29.     private function createQuadForBodyDef(bodyDef:b2BodyDef, nWidth:Number, nHeight:Number, nColor:uint = 0):Quad {
  30.       var myQuad:Quad = new Quad(nWidth, nHeight, nColor);
  31.       myQuad.pivotX = myQuad.width / 2;
  32.       myQuad.pivotY = myQuad.height / 2;
  33.       bodyDef.userData = myQuad;
  34.       return myQuad;
  35.     }
  36.   }
  37. }

[パブリッシュプレビュー]で確かめると、Quadインスタンスはステージ左上角つまり座標(0, 0)の位置に表れます(図002)。インスタンスは剛体定義のb2BodyDefオブジェクトに関連づけただけで、座標も設定していないからです。そこでつぎに定義から剛体をつくり、そのうえでシミュレーションした演算結果をQuadインスタンスに反映させます。

図002■Quadインスタンスはデフォルト位置の左上角に表れる
図002

[*1] 前のバージョン2.0.2のb2World()コンストラクタは引数を3つ取り、第1引数がb2AABBオブジェクトでした。バージョン2.1aではコンストラクタからこの引数が除かれました。

[*2] ActionScript 3.0では、このような種類を指定する値は定数とするのが通常です。しかし、これらの値はb2Bodyクラスに静的プロパティとして定められており、値を変えることもできるようです。


03 剛体とフィクスチャの定義から剛体をつくる
剛体となるb2BodyDefオブジェクトをつくるには、剛体の定義だけでは足りません。フィクスチャという(b2Fixtureクラスの)オブジェクトを剛体に定める必要があります[*3]。フィクスチャも、b2FixtureDefクラスで予め定義します。b2FixtureDefには、重さ(密度)や滑りやすさ(摩擦)、跳ね返りの度合い(弾性)などを定めます。また、かたちを決めるオブジェクトも加えます。そして、剛体とフィクスチャのふたつの定義から剛体をつくるのです。

    【剛体をつくる】
  1. フィクスチャを定義する
  2. 剛体とフィクスチャの定義から剛体をつくる

前掲スクリプト001に剛体をつくる処理を加えたクラス定義は、スクリプト002として後に掲げます。その中から上記ふたつの処理に関わる部分を、それぞれ抜出してご説明します(行番号は後継スクリプト002にもとづきます)。

第1に、フィクスチャを定義するb2FixtureDefオブジェクトの作成と設定は、3つのメソッドで行いました。まず、b2FixtureDefオブジェクトをつくって返すメソッド(createFixtureDefWithShape())です(第46〜50行目)。オブジェクトにかたちを与えるメソッド(setShapeToFixtureDef())は、別に定めて呼出しています(第51〜55行目)。

メソッドの引数は、b2FixtureDefオブジェクトの参照と、矩形の幅および高さです。剛体のかたちはb2FixtureDef.shapeプロパティにb2Shapeオブジェクトで設定します(第54行目)。矩形はb2PolygonShapeオブジェクトにb2PolygonShape.AsBox()メソッドで幅と高さを決めます(第52〜53行目)。剛体同士の当たり判定は、このかたちにもとづいて計算されます。

そして、3つ目のメソッド(setFixtureDef())で、フィクスチャ定義に密度と摩擦と弾性の値を与えます(第56〜60行目)。引数に受取ったb2FixtureDefオブジェクトの参照に、引数の3つの数値をそれぞれプロパティb2FixtureDef.densityb2FixtureDef.friction、およびb2FixtureDef.restitutionに設定します。

  1.   var fixtureDef:b2FixtureDef = createFixtureDefWithShape(boxEdge, boxEdge);
  2.   setFixtureDef(fixtureDef, 1, 0.5, 0.5);
  1. private function createFixtureDefWithShape(nWidth:Number, nHeight:Number):b2FixtureDef {
  2.   var fixtureDef:b2FixtureDef = new b2FixtureDef();
  3.   setShapeToFixtureDef(fixtureDef, nWidth / 2, nHeight / 2);
  4.   return fixtureDef;
  5. }
  6. private function setShapeToFixtureDef(fixtureDef:b2FixtureDef, nX:Number, nY:Number):void {
  7.   var myShape:b2PolygonShape = new b2PolygonShape();
  8.   myShape.SetAsBox(nX * SCALE, nY * SCALE);
  9.   fixtureDef.shape = myShape;
  10. }
  11. private function setFixtureDef(fixtureDef:b2FixtureDef, density:Number, friction:Number, restitution:Number = 0):void {
  12.   fixtureDef.density = density;
  13.   fixtureDef.friction = friction;
  14.   fixtureDef.restitution = restitution;
  15. }

第2に、剛体とフィクスチャのふたつの定義から、剛体のb2Bodyオブジェクトをつくります。そのために定めたメソッド(createBodyWithFixture())はb2Worldとb2BodyDefおよびfixtureDefの3オブジェクトを引数として受取り、それらからつくられたb2Bodyインスタンスを返します(第61〜65行目)。

まず、b2Bodyオブジェクトは、b2World.CreateBody()メソッドでb2BodyDefオブジェクトを引数としてつくり、物理ワールドに加えます。そしてつぎに、b2BodyDef.CreateFixture()メソッドにより、引数のb2FixtureDefオブジェクトにもとづいて、フィクスチャ(b2Fixtureオブジェクト)をb2BodyDefオブジェクトに定めます。

  1.   var body:b2Body = createBodyWithFixture(world, bodyDef, fixtureDef);
  1. private function createBodyWithFixture(world:b2World, bodyDef:b2BodyDef, fixtureDef:b2FixtureDef):b2Body {
  2.   var body:b2Body = world.CreateBody(bodyDef);
  3.   body.CreateFixture(fixtureDef);
  4.   return body;
  5. }

これで物理演算シミュレーションの仕込みは済みました。けれど、まだステージ上のインスタンスは動きません。残るは、シミュレーションの演算を行って、インスタンスにその結果を反映することです。

[*3]「Class b2Fixture」は、フィクスチャをつぎのように説明しています(筆者訳)。「幾何学情報に含まれない」(non-geometric data)というのは、b2BodyDefオブジェクトに定められていないということを指すものと考えられます。

フィクスチャは剛体にかたちを与え、当たり判定に用いられます。...[中略]... フィクスチャには、幾何学情報に含まれない摩擦や衝突フィルタなどが加わっています。

フィクスチャの仕組みは、バージョン2.1aから加えられました(「Box2DFlash 2.1a Update Notes」参照)。


04 剛体を落下させる − 物理演算シミュレーションの実行
物理演算は目に見えません。つまり、表示オブジェクトと異なり、画面の再描画で処理が進むという決まりもありません。そのため、一定の時間を進めては演算するという繰返しになります。そして、演算結果を表示オブジェクトの座標や回転角に反映させます。 時間を進めて物理演算を行うのが、b2World.Step()メソッドです。

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

第1引数は、シミュレーションを行うに当たって進める時間です。計算結果を反映する表示オブジェクトはEvent.ENTER_FRAMEイベントでアニメーションさせますので、時間はフレームレートの逆数(1 / FPS)ずつ進めればよいでしょう。第2および第3引数は、物理的な制約に合わせて調整の再計算をさせる回数です。前者は速度、後者が位置についての演算を定めます[*4]。回数を増やすとシミュレーションの精度は高まるものの、処理の負荷が上がります。

後掲スクリプト002のクラス定義から抜出した、物理演算シミュレーションを進めて表示オブジェクトに反映する処理が、以下のスクリプトです。Event.ENTER_FRAMEイベントに、リスナーメソッド(update())を加えました(第66〜76行目)。

リスナーメソッドは、まずb2World.Step()メソッドで物理演算シミュレーションを進めます(第67行目)。b2World.Step()メソッドに渡す3つの引数値は、privateのプロパティに置いてあります(第16〜18行目)。

つぎに、b2World.GetBodyList()メソッドで、物理ワールド(b2Worldオブジェクト)に加えた最初の剛体となるb2Bodyインスタンスを取出します(第68行目)。そして、b2Body.GetUserData()メソッドにより、その剛体の定義に関連づけた表示オブジェクトの参照を得ています(第69行目)。

後は、剛体からシミュレーションの結果を調べて、表示オブジェクトの該当するプロパティに設定します(第71〜74行目)。剛体から位置のb2Vec2オブジェクトを得るのが、b2Body.GetPosition()メソッドです(第72行目)。Box2Dのxy座標値は、前述のとおり単位がメートルですので、換算比率(SCALE)で除してピクセルに直します(第72〜73行目)。

また、剛体の回転角を求めるのが、b2Body.GetAngle()メソッドです(第74行目)。なお、剛体の定義に関連づけられた表示オブジェクト(DisplayObject)があるかどうかを、念のためif条件で調べました(第70行目)。

  1. private var velocityIterations:int = 10;
  2. private var positionIterations:int = 10;
  3. private var time:Number = 1 / 24;
  1.       addEventListener(Event.ENTER_FRAME, update);
  1. private function update(eventObject:Event):void {
  2.   world.Step(time, velocityIterations, positionIterations);
  3.   var body:b2Body = world.GetBodyList();
  4.   var myObject:DisplayObject = body.GetUserData() as DisplayObject;
  5.   if (myObject) {
  6.     var position:b2Vec2 = body.GetPosition();
  7.     myObject.x = position.x / SCALE;
  8.     myObject.y = position.y / SCALE;
  9.     myObject.rotation = body.GetAngle();
  10.   }
  11. }

これでBox2Dの物理演算シミュレーションにもとづいて、インスタンスが自然落下します(図003)。ここまでの書替えをすべて加えたクラス(MySprite)の定義全体は、つぎのスクリプト002のとおりです。

図003■矩形の剛体が落下する
図003

スクリプト002■剛体に定めたQuadインスタンスが自然落下するアニメーション
    // ActionScript 3.0クラス定義ファイル: MySprite.as
  1. package {
  2.   import starling.display.DisplayObject;
  3.   import starling.display.Sprite;
  4.   import starling.display.Quad;
  5.   import starling.events.Event;
  6.   import Box2D.Common.Math.b2Vec2;
  7.   import Box2D.Dynamics.b2World;
  8.   import Box2D.Dynamics.b2BodyDef;
  9.   import Box2D.Dynamics.b2Body;
  10.   import Box2D.Dynamics.b2FixtureDef;
  11.   import Box2D.Collision.Shapes.b2PolygonShape;
  12.   public class MySprite extends Sprite {
  13.     private const SCALE:Number = 1 / 30;
  14.     private var world:b2World;
  15.     private var gravityVertical:Number = 10;
  16.     private var velocityIterations:int = 10;
  17.     private var positionIterations:int = 10;
  18.     private var time:Number = 1 / 24;
  19.     private var boxEdge:Number = 20;
  20.     public function MySprite() {
  21.       addEventListener(Event.ADDED_TO_STAGE, initialize);
  22.     }
  23.     private function initialize(eventObject:Event):void {
  24.       world = new b2World(new b2Vec2(0, gravityVertical), true);
  25.       var bodyDef:b2BodyDef = createBodyDef(stage.stageWidth / 2, boxEdge, b2Body.b2_dynamicBody);
  26.       var myQuad:Quad = createQuadForBodyDef(bodyDef, boxEdge, boxEdge, 0x0000FF);
  27.       var fixtureDef:b2FixtureDef = createFixtureDefWithShape(boxEdge, boxEdge);
  28.       setFixtureDef(fixtureDef, 1, 0.5, 0.5);
  29.       var body:b2Body = createBodyWithFixture(world, bodyDef, fixtureDef);
  30.       addChild(myQuad);
  31.       addEventListener(Event.ENTER_FRAME, update);
  32.     }
  33.     private function createBodyDef(nX:Number, nY:Number, nType:uint= 0):b2BodyDef {
  34.       var bodyDef:b2BodyDef = new b2BodyDef();
  35.       bodyDef.position.Set((nX * SCALE), nY * SCALE);
  36.       bodyDef.type = nType;
  37.       return bodyDef;
  38.     }
  39.     private function createQuadForBodyDef(bodyDef:b2BodyDef, nWidth:Number, nHeight:Number, nColor:uint = 0):Quad {
  40.       var myQuad:Quad = new Quad(nWidth, nHeight, nColor);
  41.       myQuad.pivotX = myQuad.width / 2;
  42.       myQuad.pivotY = myQuad.height / 2;
  43.       bodyDef.userData = myQuad;
  44.       return myQuad;
  45.     }
  46.     private function createFixtureDefWithShape(nWidth:Number, nHeight:Number):b2FixtureDef {
  47.       var fixtureDef:b2FixtureDef = new b2FixtureDef();
  48.       setShapeToFixtureDef(fixtureDef, nWidth / 2, nHeight / 2);
  49.       return fixtureDef;
  50.     }
  51.     private function setShapeToFixtureDef(fixtureDef:b2FixtureDef, nX:Number, nY:Number):void {
  52.       var myShape:b2PolygonShape = new b2PolygonShape();
  53.       myShape.SetAsBox(nX * SCALE, nY * SCALE);
  54.       fixtureDef.shape = myShape;
  55.     }
  56.     private function setFixtureDef(fixtureDef:b2FixtureDef, density:Number, friction:Number, restitution:Number = 0):void {
  57.       fixtureDef.density = density;
  58.       fixtureDef.friction = friction;
  59.       fixtureDef.restitution = restitution;
  60.     }
  61.     private function createBodyWithFixture(world:b2World, bodyDef:b2BodyDef, fixtureDef:b2FixtureDef):b2Body {
  62.       var body:b2Body = world.CreateBody(bodyDef);
  63.       body.CreateFixture(fixtureDef);
  64.       return body;
  65.     }
  66.     private function update(eventObject:Event):void {
  67.       world.Step(time, velocityIterations, positionIterations);
  68.       var body:b2Body = world.GetBodyList();
  69.       var myObject:DisplayObject = body.GetUserData() as DisplayObject;
  70.       if (myObject) {
  71.         var position:b2Vec2 = body.GetPosition();
  72.         myObject.x = position.x / SCALE;
  73.         myObject.y = position.y / SCALE;
  74.         myObject.rotation = body.GetAngle();
  75.       }
  76.     }
  77.   }
  78. }

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

バージョン2.0.2のb2World.Step()メソッドは引数がふたつしかなく、再計算の回数は第2引数ひとつで定められていました(Box2DFlash「Simulating the World (of Box2D)」参照)。再計算の引数ふたつの説明は、C++用の「Box2D v2.2.0 User Manual」2.4「Simulating the World (of Box2D)」に記載されています。


05 静的な剛体を加える − 衝突のシミュレーション
前掲スクリプト002で剛体を落とすことはできたものの、ステージの下端に消えてしまいます。また、テクスチャに与えたかたちを始め、密度や摩擦、弾性といった値は、他の剛体にぶつけないと見て取れません。そこで、床を加えます。

その前に、落下する矩形の剛体をつくる処理(前掲スクリプト第25〜29行目)は、メソッドとして分けることにします。床を加えたクラス定義(MySprite)は、後にスクリプト003として掲げます。その中からメソッド(createDynamicBox())に関わる部分をつぎに抜出しました。この機会に、設定で使われる値を、プロパティや変数に置いています(第20〜22行目および第27〜29行目)。

  1. private var density:Number = 1;
  2. private var friction:Number = 0.5;
  3. private var restitution:Number = 0.5;
  1.   var nStageWidth:Number = stage.stageWidth;
  2.   var nStageHeight:Number = stage.stageHeight;
  3.   var nCenterX:Number = nStageWidth / 2;
  1.   var myQuad:Quad = createDynamicBox(nCenterX, boxEdge, boxEdge, boxEdge, 0x0000FF);
  1. private function createDynamicBox(nX:Number, nY:Number, nWidth:Number, nHeight:Number, nColor:uint):Quad {
  2.   var bodyDef:b2BodyDef = createBodyDef(nX, nY, b2Body.b2_dynamicBody);
  3.   var myQuad:Quad = createQuadForBodyDef(bodyDef, nWidth, nHeight, nColor);
  4.   var fixtureDef:b2FixtureDef = createFixtureDefWithShape(nWidth, nHeight);
  5.   setFixtureDef(fixtureDef, density, friction, restitution);
  6.   var body:b2Body = createBodyWithFixture(world, bodyDef, fixtureDef);
  7.   return myQuad;
  8. }

それでは、床を加える処理もメソッド(createStaticFloor())として定め(第39〜45行目)、初期化のときにそのメソッドを呼出すことにします(第33行目)。メソッドの中身は、基本的には先に定めた落下する矩形の剛体をつくるメソッド(createDynamicBox())と同じです。

  1.   var nStageWidth:Number = stage.stageWidth;
  2.   var nStageHeight:Number = stage.stageHeight;
  3.   var nCenterX:Number = nStageWidth / 2;
  4.   var nFloorWidth:Number = nStageWidth * 0.8;
  5.   var nFloorHeight:Number = 20;
  1.   var floorQuad:Quad = createStaticFloor(nCenterX, nStageHeight - nFloorHeight, nFloorWidth, nFloorHeight, 0xCCCCCC);
  2.   addChild(floorQuad);
  1. private function createStaticFloor(nX:Number, nY:Number, nWidth:Number, nHeight:Number, nColor:uint):Quad {
  2.   var bodyDef:b2BodyDef = createBodyDef(nX, nY);
  3.   var myQuad:Quad = createQuadForBodyDef(bodyDef, nWidth, nHeight, nColor);
  4.   var fixtureDef:b2FixtureDef = createFixtureDefWithShape(nWidth, nHeight);
  5.   var body:b2Body = createBodyWithFixture(world, bodyDef, fixtureDef);
  6.   return myQuad;
  7. }

床を加えるメソッド(createStaticFloor())と落下する剛体をつくるメソッド(createDynamicBox())との違いは大きくふたつです。ひとつは、床は動きませんので、剛体定義(b2BodyDefオブジェクト)のb2BodyDef.typeプロパティにb2Body.b2_staticBodyを指定することです。もっとも、床を加えるメソッド(createStaticFloor())の中に、そのような定めは見当たりません。

それは、剛体定義のメソッド(createBodyDef())で、プロパティ値に充てる引数にデフォルト値0が与えてあるからです(第54行目)。整数0はb2Body.b2_staticBodyの値です。

  1. private function createBodyDef(nX:Number, nY:Number, nType:uint = 0):b2BodyDef {
  2.   var bodyDef:b2BodyDef = new b2BodyDef();
  1.   bodyDef.type = nType;

床を加えるメソッドが落下する剛体のメソッドと異なるふたつ目は、フィクスチャに密度・摩擦・弾性を定めるメソッド(setFixtureDef())が呼出されていないことです。これは、b2FixtureDefオブジェクトのデフォルト設定をとくに変えなくてよいからです。

ここまで手を加えて[ブラウザプレビュー]を確かめると、落下する矩形の剛体は確かに床とおぼしきあたりで弾むものの、床のQuadインスタンスがステージ左上角に取り残されています(図004)。これは、物理演算シミュレーションの結果が床のQuadインスタンスに反映されていないためです。つまり、Event.ENTER_FRAMEイベントのリスナーメソッド(update())に手を加えなければなりません。

図004■剛体は画面下で弾むものの床がステージ左上角にある
図004

前掲スクリプト002のリスナーメソッド(update())では、物理ワールド(b2Worldオブジェクト)に加えた最初のb2Bodyインスタンスを、b2World.GetBodyList()メソッドで取出し、物理演算の結果をQuadインスタンスに適用しました(第68〜75行目)。ですから、矩形のインスタンスが落下したのです。けれど、新たに加えた床のインスタンスには、演算結果が反映されていません。

物理ワールドに加えられたつぎの剛体は、すでに取出したb2Bodyインスタンスに対してb2Body.GetNext()メソッドを呼出すことにより得られます。このメソッドでb2Bodyインスタンスを順に取出し、もうつぎはなくなるとnullが返されます。したがって、戻り値がnullになるまで、繰返し処理をすればよいということになります(第90〜99行目)。

  1. private function update(eventObject:Event):void {
  2.   world.Step(time, velocityIterations, positionIterations);
  3.   var body:b2Body = world.GetBodyList();
  4.   while (body) {
  5.     var myObject:DisplayObject = body.GetUserData() as DisplayObject;
  6.     if (myObject) {
  7.       var position:b2Vec2 = body.GetPosition();
  8.       myObject.x = position.x / SCALE;
  9.       myObject.y = position.y / SCALE;
  10.       myObject.rotation = body.GetAngle();
  11.     }
  12.     body = body.GetNext();
  13.   }
  14. }

[パブリッシュプレビュー]を確かめると、床がステージ下側に置かれ、落下した矩形が軽く弾みながら、やがて床の上で止まります。ここまで書き加えたクラス定義(MySprite)が、つぎのスクリプト003です。

図005■落下した矩形の剛体が床で弾む
図005

スクリプト003■動的な剛体が落下して静的な剛体のうえで弾むアニメーション
    // ActionScript 3.0クラス定義ファイル: MySprite.as
  1. package {
  2.   import starling.display.DisplayObject;
  3.   import starling.display.Sprite;
  4.   import starling.display.Quad;
  5.   import starling.events.Event;
  6.   import Box2D.Common.Math.b2Vec2;
  7.   import Box2D.Dynamics.b2World;
  8.   import Box2D.Dynamics.b2BodyDef;
  9.   import Box2D.Dynamics.b2Body;
  10.   import Box2D.Dynamics.b2FixtureDef;
  11.   import Box2D.Collision.Shapes.b2PolygonShape;
  12.   public class MySprite extends Sprite {
  13.     private const SCALE:Number = 1 / 30;
  14.     private var world:b2World;
  15.     private var gravityVertical:Number = 10;
  16.     private var velocityIterations:int = 10;
  17.     private var positionIterations:int = 10;
  18.     private var time:Number = 1 / 24;
  19.     private var boxEdge:Number = 20;
  20.     private var density:Number = 1;
  21.     private var friction:Number = 0.5;
  22.     private var restitution:Number = 0.5;
  23.     public function MySprite() {
  24.       addEventListener(Event.ADDED_TO_STAGE, initialize);
  25.     }
  26.     private function initialize(eventObject:Event):void {
  27.       var nStageWidth:Number = stage.stageWidth;
  28.       var nStageHeight:Number = stage.stageHeight;
  29.       var nCenterX:Number = nStageWidth / 2;
  30.       var nFloorWidth:Number = nStageWidth * 0.8;
  31.       var nFloorHeight:Number = 20;
  32.       world = new b2World(new b2Vec2(0, gravityVertical), true);
  33.       var floorQuad:Quad = createStaticFloor(nCenterX, nStageHeight - nFloorHeight, nFloorWidth, nFloorHeight, 0xCCCCCC);
  34.       addChild(floorQuad);
  35.       var myQuad:Quad = createDynamicBox(nCenterX, boxEdge, boxEdge, boxEdge, 0x0000FF);
  36.       addChild(myQuad);
  37.       addEventListener(Event.ENTER_FRAME, update);
  38.     }
  39.     private function createStaticFloor(nX:Number, nY:Number, nWidth:Number, nHeight:Number, nColor:uint):Quad {
  40.       var bodyDef:b2BodyDef = createBodyDef(nX, nY);
  41.       var myQuad:Quad = createQuadForBodyDef(bodyDef, nWidth, nHeight, nColor);
  42.       var fixtureDef:b2FixtureDef = createFixtureDefWithShape(nWidth, nHeight);
  43.       var body:b2Body = createBodyWithFixture(world, bodyDef, fixtureDef);
  44.       return myQuad;
  45.     }
  46.     private function createDynamicBox(nX:Number, nY:Number, nWidth:Number, nHeight:Number, nColor:uint):Quad {
  47.       var bodyDef:b2BodyDef = createBodyDef(nX, nY, b2Body.b2_dynamicBody);
  48.       var myQuad:Quad = createQuadForBodyDef(bodyDef, nWidth, nHeight, nColor);
  49.       var fixtureDef:b2FixtureDef = createFixtureDefWithShape(nWidth, nHeight);
  50.       setFixtureDef(fixtureDef, density, friction, restitution);
  51.       var body:b2Body = createBodyWithFixture(world, bodyDef, fixtureDef);
  52.       return myQuad;
  53.     }
  54.     private function createBodyDef(nX:Number, nY:Number, nType:uint= 0):b2BodyDef {
  55.       var bodyDef:b2BodyDef = new b2BodyDef();
  56.       bodyDef.position.Set((nX * SCALE), nY * SCALE);
  57.       bodyDef.type = nType;
  58.       return bodyDef;
  59.     }
  60.     private function createQuadForBodyDef(bodyDef:b2BodyDef, nWidth:Number, nHeight:Number, nColor:uint=0):Quad {
  61.       var myQuad:Quad = new Quad(nWidth, nHeight, nColor);
  62.       myQuad.pivotX = myQuad.width / 2;
  63.       myQuad.pivotY = myQuad.height / 2;
  64.       bodyDef.userData = myQuad;
  65.       return myQuad;
  66.     }
  67.     private function createFixtureDefWithShape(nWidth:Number, nHeight:Number):b2FixtureDef {
  68.       var fixtureDef:b2FixtureDef = new b2FixtureDef();
  69.       setShapeToFixtureDef(fixtureDef, nWidth / 2, nHeight / 2);
  70.       return fixtureDef;
  71.     }
  72.     private function setShapeToFixtureDef(fixtureDef:b2FixtureDef, nX:Number, nY:Number):void {
  73.       var myShape:b2PolygonShape = new b2PolygonShape();
  74.       myShape.SetAsBox(nX * SCALE, nY * SCALE);
  75.       fixtureDef.shape = myShape;
  76.     }
  77.     private function setFixtureDef(fixtureDef:b2FixtureDef, density:Number, friction:Number, restitution:Number = 0):void {
  78.       fixtureDef.density = density;
  79.       fixtureDef.friction = friction;
  80.       fixtureDef.restitution = restitution;
  81.     }
  82.     private function createBodyWithFixture(world:b2World, bodyDef:b2BodyDef, fixtureDef:b2FixtureDef):b2Body {
  83.       var body:b2Body = world.CreateBody(bodyDef);
  84.       body.CreateFixture(fixtureDef);
  85.       return body;
  86.     }
  87.     private function update(eventObject:Event):void {
  88.       world.Step(time, velocityIterations, positionIterations);
  89.       var body:b2Body = world.GetBodyList();
  90.       while (body) {
  91.         var myObject:DisplayObject = body.GetUserData() as DisplayObject;
  92.         if (myObject) {
  93.           var position:b2Vec2 = body.GetPosition();
  94.           myObject.x = position.x / SCALE;
  95.           myObject.y = position.y / SCALE;
  96.           myObject.rotation = body.GetAngle();
  97.         }
  98.         body = body.GetNext();
  99.       }
  100.     }
  101.   }
  102. }

06 落下する剛体を増やす
では仕上げに、落下させる剛体の数を50個ほどに増やしてみましょう。といっても、Box2Dについて新たに説明することはありません。初期化のメソッド(initialize())でforループを使って、落下する剛体作成のメソッド(createDynamicBox())を繰返し呼出すだけです。後の計算は、すべてBox2Dが行ってくれます。剛体の位置や色は、ランダムに変えてみました。

  1. private var count:uint = 50;
  1.   for (var i:uint = 0; i < count; i++) {
  2.     var nX:Number = Math.random() * nFloorWidth + nFloorLeft;
  3.     var nY:Number = (Math.random() - 0.5) * nStageHeight;
  4.     var nColor:uint = uint(Math.random() * 0xFFFFFF)
  5.     var boxQuad:Quad = createDynamicBox(nX, nY, boxEdge, boxEdge, nColor);
  6.     addChild(boxQuad);
  7.   }

クラス定義(MySprite)の全体は、以下のスクリプト004のとおりです。50個の矩形の剛体がランダムな色と場所に置かれ、すべてが床に落下します(図006)。

図006■50個の矩形の剛体が床に落ちる
図006左図 図006右図

スクリプト004■50個のランダムな色と位置の剛体を床に落下させる
    // ActionScript 3.0クラス定義ファイル: MySprite.as
  1. package {
  2.   import starling.display.DisplayObject;
  3.   import starling.display.Sprite;
  4.   import starling.display.Quad;
  5.   import starling.events.Event;
  6.   import Box2D.Common.Math.b2Vec2;
  7.   import Box2D.Dynamics.b2World;
  8.   import Box2D.Dynamics.b2BodyDef;
  9.   import Box2D.Dynamics.b2Body;
  10.   import Box2D.Dynamics.b2FixtureDef;
  11.   import Box2D.Collision.Shapes.b2PolygonShape;
  12.   public class MySprite extends Sprite {
  13.     private const SCALE:Number = 1 / 30;
  14.     private var world:b2World;
  15.     private var gravityVertical:Number = 10;
  16.     private var velocityIterations:int = 10;
  17.     private var positionIterations:int = 10;
  18.     private var density:Number = 0.5;
  19.     private var friction:Number = 0.5;
  20.     private var restitution:Number = 0.5;
  21.     private var time:Number = 1 / 24;
  22.     private var count:uint = 50;
  23.     private var boxEdge:Number = 20;
  24.     public function MySprite() {
  25.       addEventListener(Event.ADDED_TO_STAGE, initialize);
  26.     }
  27.     private function initialize(eventObject:Event):void {
  28.       var nStageWidth:Number = stage.stageWidth;
  29.       var nStageHeight:Number = stage.stageHeight;
  30.       var nCenterX:Number = nStageWidth / 2;
  31.       var nFloorWidth:Number = nStageWidth * 0.8;
  32.       var nFloorHeight:Number = 20;
  33.       var nFloorLeft:Number = (nStageWidth - nFloorWidth) / 2;
  34.       world = new b2World(new b2Vec2(0, gravityVertical), true);
  35.       var floorQuad:Quad = createStaticFloor(nCenterX, nStageHeight - nFloorHeight, nFloorWidth, nFloorHeight, 0xCCCCCC);
  36.       addChild(floorQuad);
  37.       for (var i:uint = 0; i < count; i++) {
  38.         var nX:Number = Math.random() * nFloorWidth + nFloorLeft;
  39.         var nY:Number = (Math.random() - 0.8) * nStageHeight;
  40.         var nColor:uint = uint(Math.random() * 0xFFFFFF)
  41.         var boxQuad:Quad = createDynamicBox(nX, nY, boxEdge, boxEdge, nColor);
  42.         addChild(boxQuad);
  43.       }
  44.       addEventListener(Event.ENTER_FRAME, update);
  45.     }
  46.     private function createStaticFloor(nX:Number, nY:Number, nWidth:Number, nHeight:Number, nColor:uint):Quad {
  47.       var bodyDef:b2BodyDef = createBodyDef(nX, nY);
  48.       var myQuad:Quad = createQuadForBodyDef(bodyDef, nWidth, nHeight, nColor);
  49.       var fixtureDef:b2FixtureDef = createFixtureWithShape(nWidth, nHeight);
  50.       var body:b2Body = createBodyWithFixture(world, bodyDef, fixtureDef);
  51.       return myQuad;
  52.     }
  53.     private function createDynamicBox(nX:Number, nY:Number, nWidth:Number, nHeight:Number, nColor:uint):Quad {
  54.       var bodyDef:b2BodyDef = createBodyDef(nX, nY, b2Body.b2_dynamicBody);
  55.       var myQuad:Quad = createQuadForBodyDef(bodyDef, nWidth, nHeight, nColor);
  56.       var fixtureDef:b2FixtureDef = createFixtureWithShape(nWidth, nHeight);
  57.       setFixture(fixtureDef, density, friction, restitution);
  58.       var body:b2Body = createBodyWithFixture(world, bodyDef, fixtureDef);
  59.       return myQuad;
  60.     }
  61.     private function createBodyDef(nX:Number , nY:Number, nType:uint = 0):b2BodyDef {
  62.       var bodyDef:b2BodyDef = new b2BodyDef();
  63.       bodyDef.position.Set(nX * SCALE, nY * SCALE);
  64.       bodyDef.type = nType;
  65.       return bodyDef;
  66.     }
  67.     private function createQuadForBodyDef(bodyDef:b2BodyDef, nWidth:Number, nHeight:Number, nColor:uint = 0):Quad {
  68.       var myQuad:Quad = new Quad(nWidth, nHeight, nColor);
  69.       myQuad.pivotX = myQuad.width / 2;
  70.       myQuad.pivotY = myQuad.height / 2;
  71.       bodyDef.userData = myQuad;
  72.       return myQuad;
  73.     }
  74.     private function createFixtureWithShape(nWidth:Number, nHeight:Number):b2FixtureDef {
  75.       var fixtureDef:b2FixtureDef = new b2FixtureDef();
  76.       setShapeToFixtureDef(fixtureDef, nWidth / 2, nHeight / 2);
  77.       return fixtureDef;
  78.     }
  79.     private function setShapeToFixtureDef(fixtureDef:b2FixtureDef, nX:Number, nY:Number):void {
  80.       var myShape:b2PolygonShape = new b2PolygonShape();
  81.       myShape.SetAsBox(nX * SCALE, nY * SCALE);
  82.       fixtureDef.shape = myShape;
  83.     }
  84.     private function setFixture(fixtureDef:b2FixtureDef, density:Number, friction:Number, restitution:Number = 0):void {
  85.       fixtureDef.density = density;
  86.       fixtureDef.friction = friction;
  87.       fixtureDef.restitution = restitution;
  88.     }
  89.     private function createBodyWithFixture(world:b2World, bodyDef:b2BodyDef, fixtureDef:b2FixtureDef):b2Body {
  90.       var body:b2Body = world.CreateBody(bodyDef);
  91.       body.CreateFixture(fixtureDef);
  92.       return body;
  93.     }
  94.     private function update(eventObject:Event):void {
  95.       world.Step(time, velocityIterations, positionIterations);
  96.       var body:b2Body = world.GetBodyList();
  97.       while (body) {
  98.       var myObject:DisplayObject = body.GetUserData() as DisplayObject;
  99.         if (myObject) {
  100.           var position:b2Vec2 = body.GetPosition();
  101.           myObject.x = position.x / SCALE;
  102.           myObject.y = position.y / SCALE;
  103.           myObject.rotation = body.GetAngle();
  104.         }
  105.         body = body.GetNext();
  106.       }
  107.     }
  108.   }
  109. }

作成者: 野中文雄
作成日: 2012年2月21日


Copyright © 2001-2012 Fumio Nonaka.  All rights reserved.