Box2DでActionScript物理プログラミング

第6回画像を使って表現力アップ

前回作った車のサンプルをはじめ、ここまで説明してきたサンプルでは、描画にDebugDrawを使いました。DebugDrawを使えば簡単に物体の形などを表示することができましたが、線だけなので表現力に欠けます。今回はこれを書き換え、画像を使って車体などを描くようにします。

説明に際しては、前回のラストに完成したDraggableCar.asをベースに修正を加えていく形で進めていきます。

画像をFlashに埋め込む

Embedで画像をFlashに埋め込む

Flashで画像を使うにはいろいろな方法がありますが、今回はEmbedを使った簡単な方法を使います(HTMLのembedタグとは関係ありません⁠⁠。この方法を使うと、Flashに画像を埋め込むことができます。Embedは次のように書きます。

[Embed(source='image.png')]
private static const SomeImage:Class;

Embedは、クラスのプロパティとして書くのが一般的です。これによりimage.pngという画像がFlash内に埋め込まれます。埋め込まれた画像はSomeImageというクラスとしてActionScriptの中で使うことができます。

埋め込んだ画像を使う

Embedを使って埋め込んだ画像を使うには、次のように書きます。

private bmp:Bitmap = new SomeImage();

先ほど定義したSomeImageクラスを使って、bmpという変数を作っています。ポイントは、bmpをBitmapクラスの変数として作っているところです。SomeImageは画像を埋め込んで作ったクラスなので、実際に使うときにはBitmapクラスの変数として使います。

車を描くための画像を埋め込む

前回のサンプルを描画するには、床、車体、車輪の3つの画像が必要です。以下の画像をダウンロードして、ActionScriptのプログラムと同じディレクトリに置いてください。

これらの画像をFlashに埋め込むために、前回のDraggableCarクラスのプロパティに3つのクラスを追加します。mouseJointプロパティを定義しているところに以下の定義を書き加えてください。

[Embed(source='floor.png')]
private static const FloorImage:Class;
[Embed(source='body.png')]
private static const BodyImage:Class;
[Embed(source='wheel.png')]
private static const WheelImage:Class;

FloorImage、BodyImage、WheelImageの3つのクラスとして画像を埋め込みました。これらを使って車を描きます。

床の画像を読み込んで表示する

床の画像を読み込み、大きさと位置を調整する

では画像を表示する部分のプログラムの説明に移ります。最初は床を表示するところからです。

先ほどEmbedを使って定義したFloorImageクラスを使って、床の画像を読み込みます。

// 床の画像を作る
var floorImage:Bitmap = new FloorImage();

このプログラムは、床を作った直後、つまりb2Bodyクラスのfloorを作ってCreateShapeを呼んだ直後に書きます。次に、画像の大きさと位置を調整します。

// 画像のサイズを合わせる
floorImage.width = 4 * DRAW_SCALE;
floorImage.height = 0.2 * DRAW_SCALE;
// 床の中心が(0, 0)に来るように、左上にずらす
floorImage.x = -floorImage.width / 2;
floorImage.y = -floorImage.height / 2;

大きさは床のサイズ(幅4m、厚さ20cm)に合わせます。場所は、画像の幅と高さの半分だけ左上にずらしておきます。floorImageはこの後SpriteにaddChildするのですが、ここであらかじめx、y座標をずらしておくことで回転などの処理を簡単にすることができます。

Spriteを作って画像を表示させる

次に、b2Bodyクラスのプロパティm_userDataにSpriteクラスの変数を設定します。

// b2BodyのユーザデータとしてSpriteを作る
floor.m_userData = new Sprite();

m_userData(ユーザデータ)は、b2Bodyに対して何かデータを関連付けるときに使うプロパティです。これに、床の画像を表示するためのSpriteを設定しておきます。後で書きますが、m_userDataに表示用のSpriteを設定しておくととても便利です。

Spriteを作ったら、その場所を物理エンジン内の床の場所に一致させます。作成したユーザデータはGetUserDataメソッドで参照できます。

// Spriteの場所を物理エンジン内の場所と一致させる
floor.GetUserData().x = floor.GetWorldCenter().x * DRAW_SCALE;
floor.GetUserData().y = floor.GetWorldCenter().y * DRAW_SCALE;

GetWorldCenterで得られる座標はメートル単位なので、DRAW_SCALEをかけて補正します。最後に、読み込んだ画像floorImageをSpriteに追加し、Sprite自体を画面に追加します。

floor.GetUserData().addChild(floorImage);
addChild(floor.GetUserData());

ここまでの結果のプログラムを、クラス名を変えて置いておきます。

まだDebugDrawの機能は切っていないので、床や車の枠線は表示されたままです。関係する部分のプログラムをコメントアウトすればDebugDrawの機能を切ることができますが、そうすると車がまったく表示されなくなるので、まだ表示は残しておきます。

その他の物体を表示する

車体の画像を読み込んで表示する

車のそれぞれのパーツについての描画方法は、床のときとほとんど同じです。まず車体を描画するためのプログラムについて説明します。

// 車体の画像を読み込んで表示する
var bodyImage:Bitmap = new BodyImage()
bodyImage.width = 0.8 * DRAW_SCALE;
bodyImage.height = 0.2 * DRAW_SCALE;
bodyImage.x = -bodyImage.width / 2;
bodyImage.y = -bodyImage.height / 2;
body.m_userData = new Sprite();
body.GetUserData().x = body.GetWorldCenter().x * DRAW_SCALE;
body.GetUserData().y = body.GetWorldCenter().y * DRAW_SCALE;
body.GetUserData().addChild(bodyImage);
addChild(body.GetUserData());

このプログラムは、車体を作り終わった直後に書きます。先ほどの床の画像を表示するためのプログラムと比較すると分かると思いますが、変化しているのは変数名と数値だけです。

変数名については、床のときにfloor~という名前になっていたものがbody~となっています。数値については、車体を作るときに設定した幅と高さ(80cmと20cm)を反映した値になっています。

車輪の画像を読み込んで表示する

この規則に従えば、前輪と後輪についても同じように書くことができます。全て書くと長くなってしまうので一部省略しますが、最後に完成形のソースコードを添付しておくので、分からないことがあったらそちらを参照してください。

// ↓前輪を作り終わった後に書く
var frontWheelImage:Bitmap = new CircleImage();
frontWheelImage.width = 0.3 * DRAW_SCALE;
frontWheelImage.height = 0.3 * DRAW_SCALE
// (略)

// ↓後輪を作り終わった後に書く
var rearWheelImage:Bitmap = new CircleImage();
rearWheelImage.width = 0.3 * DRAW_SCALE;
rearWheelImage.height = 0.3 * DRAW_SCALE;
// (略)

ここまでのソースをコンパイルして実行すると、妙なことに気づくと思います。床も車も表示はされているのですが、車の画像が宙に浮いたままです。

物理エンジンが動作していないのかというと、そういうわけではありません。DebugDrawによる描画をみれば分かることですが、きちんと動作しています。車の絵がきちんと走るようにするには、物理エンジンの情報を反映させる必要があります。最後にその方法について説明します。

物理エンジンの動作に合わせて画像を動かす

物理エンジン内の時間を進めているのは、enterFrameHandler内のworld.Stepです。これにより物理エンジン内の時間が1/24秒進みます。車の位置を物理エンジンから画面に反映するには、これと同じくenterFrameHandler内に書くとよいでしょう。

ワールド内のb2Bodyに対するfor文

物理エンジンの動作を反映させるには、enterFrameHandlerの最初に以下のようなプログラムを書きます。

for (var b:b2Body = world.GetBodyList(); b; b = b.GetNext()) {
  // 物体の位置を更新する処理。中身は後述
}

for文の書き方が少し特殊なので説明します。world.GetBodyListは、ワールド内の最初のb2Bodyクラスの変数を返します。これがfor文における変数bの初期値となります。今回のサンプルでは床や車体などがそれに当たります。もしワールド内にb2Bodyが無ければnullが返されます。

for文の終了条件は単にbとしているので、bがnullでなければfor文の中の処理が実行されます。処理が終わると、b2BodyクラスのGetNextメソッドにより次のb2Bodyが返されます。これがnullでなければ、引き続きfor文の中の処理が実行されます。

全てのb2Bodyに対する処理が終わるとGetNextメソッドがnullを返すので、そこでfor文から抜けます。全体的なイメージとしては以下のようになります。

b = world.GetBodyList(); // for文の最初で、bは床を指す
  // 床の位置を更新する
b = b.GetNext(); // bは車体を指す
  // 車体の位置を更新する
b = b.GetNext(); // bは前輪を指す
  // 前輪の位置を更新する
b = b.GetNext(); // bは後輪を指す
  // 後輪の位置を更新する
b = b.GetNext(); // bにnullが設定され、for文を抜ける

b2Bodyの内容をSpriteに反映する

for文の中身には、以下のような処理を書きます。

// for文の中身
if (b.GetUserData() is Sprite) {
  b.GetUserData().x = b.GetWorldCenter().x * DRAW_SCALE;
  b.GetUserData().y = b.GetWorldCenter().y * DRAW_SCALE;
  b.GetUserData().rotation = b.GetAngle() * 180 / Math.PI;
}

b2BodyのユーザデータにSpriteが設定されているとき、物理エンジン内の情報をそのSpriteに反映させます。

物体の場所は、今までも何度か出てきたGetWorldCenterメソッドで得られます。これにDRAW_SCALEをかけてSpriteの座標とします。回転角度はGetAngleメソッドで取得できます。これは単位がラジアンなので、Spriteのrotationプロパティに設定するときには度に直す必要があります。

このように、b2BodyのユーザデータとしてSpriteを設定しておくと、for文を回すだけでSpriteに対する処理を一括して行えます。

DebugDrawの表示を切る

最後の仕上げに、DebugDrawによる表示を切ります。debugDraw変数を作り、それをワールドに登録している部分までを/* */でコメントアウトするだけで構いません。これでDebugDrawによる描画が行われなくなります。

完成したソースファイルとFlashがこちらです。ソースファイルは例によってクラス名を変えてあります。

 ページを開くとFlashの再生が始まり、車が動いて落ちてしまいます。車を確認するには、ページを更新してすぐにこのFlashを閲覧ください。

まとめ

画像を読み込んで表示する方法は、最初は少し面倒ですが、パターン化してしまうと簡単なものです。BitmapやSpriteを作ってb2Bodyに割り当てる部分は、メソッド化してしまうこともできます。そうすれば今回のプログラムも少しシンプルになると思います。

いったんSpriteをb2Bodyに割り当ててしまえば、表示の部分でfor文を回すだけです。それぞれのb2Bodyについて個別に処理を書く必要はありません。

次回はコンタクトリスナというものについて説明します。コンタクトとは、日本語では接触という意味です。物と物がぶつかったことを検知できる仕組みを使ったサンプルで説明しようと思います。

おすすめ記事

記事・ニュース一覧