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

第7回円を落として星を飛び散らせる

Box2Dの様々な機能について説明してきましたが、そろそろBox2Dのマニュアルに書かれている主な機能の説明が一通り終わるので、今回で最終回とさせていただきます。最後のテーマはコンタクトリスナです。

コンタクトリスナを使うと、物と物がぶつかったことを検出できます。ピンボールではボールが様々なオブジェクトに当たったときに得点が入りますが、そういった部分の実装にコンタクトリスナを使うことができます。

サンプルについて

まずはサンプルのFlashをご覧ください。円を作るときのFlashに似ていますが、円が落下して床にぶつかると星が飛び散ります。前回DebugDrawを使わない方法を説明したばかりですが、今回は説明を簡単にするためにDebugDrawを使っています。

動作についての説明

このFlashでは、コンタクトリスナを使って画面内の全ての物体同士の衝突を検出しています。衝突を検出すると、その場所からくるくる回る星の絵を複数飛ばします。

コンタクトリスナでは衝突の強さを検出できるので、それに応じて飛ばす星の数を調整しています。激しく衝突するほど星の数を増やし、最大で10個としています。

全ての物体同士の衝突を見ているので、円同士の衝突でも星が飛び散ります。しかし、実際にそうしようとしても、飛ぶときと飛ばないときがあります。これはBox2D側の不具合ではないかと思われます。

サンプルファイルの構成

このFlashを作るためのプログラムと画像は以下のとおりです。本当はこれらのファイルだけではコンパイルできず、別のライブラリ(tweener)のソースが必要なのですが、これについては後述します。

今回のサンプルコードは、今までと少し違います。まず1つ目は、ActionScriptファイルが複数あることです。

メインのクラスが書かれているのはKiraKira.asです。mxmlcでコンパイルするときには、このファイルを指定すればFlashが生成されます。もう1つのContactListener.asは、コンタクトリスナを使うためのクラスです。

コンタクトリスナを使うときには、b2ContactListenerクラスを継承したクラスを作る必要があります。そのためのContactListenerクラスがContactListener.asに書かれています。

star.pngは、円が床に当たったときに飛び散る星の画像です。この画像は、前回説明したEmbed文を使ってFlashに埋め込まれます。

tweenerを使うには

もう1点、今までのサンプルと違うところは、tweenerを使っていることです。tweenerについては、プログラマのためのFlash遊び方の第6回、動きのあるFlashを作るにて取り上げられています。利用方法についてはそちらの説明をご覧ください。caurinaディレクトリをKiraKira.asなどと同じ階層に置けばコンパイルできるようになります。

コンタクトリスナの仕組み

サンプルプログラムの説明をする前に、まずは少し複雑なコンタクトリスナの仕組みについて説明します。

衝突を検出……というと、ActionScriptでのプログラミングに慣れている人なら、衝突検出用のイベントリスナを登録するのでは?と考えると思います。しかし、Box2Dは元々C++で書かれていたプログラムを移植したものなので、そこの考え方がかなり違います。

メソッドをオーバーライドして衝突を検出する

Box2Dで衝突を検出するには、以下のようにb2ContactListenerクラスの派生クラスを作り、Addメソッドをオーバーライドします。Addメソッドは、ワールド内の物体同士が衝突したときに呼び出されるメソッドです。

public class ContactListener extends b2ContactListener {
  public override function Add(point:b2ContactPoint):void {
    // 衝突検出時の処理
  }
}

このクラスの変数を作り、b2WorldクラスのSetListenerメソッドを使ってワールドに登録すると、実際にAddメソッドが呼び出されるようになります。

var contactListener:ContactListener = new ContactListener();
world.SetListener(contactListener);

その他のオーバーライドできるメソッド

b2ContactListenerクラスには、Addメソッドの他にもオーバーライドできるメソッドがあります。Addとまとめて整理すると以下のようになります。

Add
物体が衝突したときに呼び出されるメソッド
Persist
物体が衝突してから離れるまで繰り返し呼び出されるメソッド
Remove
物体が離れたときに呼び出されるメソッド
呼び出されるメソッド
呼び出されるメソッド

b2ContactPointについて

上で説明した3つのメソッドは、b2ContactPoint型の変数を引数に取ります。この変数は衝突に関するプロパティを持っており、何らかのアクションをするときにはこれらのプロパティの参照が欠かせません。

プロパティは数が多いので、よく使うものだけ説明します。詳しくはb2ContactPointクラスのソースを見てみてください。

position
物体が接触している点の座標。サンプルではこの場所から星を飛ばしています
normalForce
衝突の強さ。サンプルではこれに応じて星の数を決めています
shape1、shape2
接触している物体(b2Shape型の変数⁠⁠。これを参照すれば衝突した物体に応じて動作を変えることができます

ContactListenerクラスでの処理

クラスの全体像

では実際にサンプルプログラムがどのように書かれているかを説明します。まずはb2ContactListenerクラスを継承したContactListenerクラスからです。クラスの大まかな形は以下のようになっています。

public class ContactListener extends b2ContactListener {
  public var parent:KiraKira;
  
  public function StarContactListener() {
  }
  
  // 新しく衝突が発生したときに呼び出されるメソッド
  public override function Add(point:b2ContactPoint):void { ... }
}

メインとなるKiraKiraクラスを参照するために、parentというプロパティが定義されています。メソッドについては、コンストラクタでは何もせず、Addメソッドでは星を発生させる処理をしています。

物体が衝突した時の処理

物体が衝突したときの処理を書くAddメソッドは、以下のようになっています。

public override function Add(point:b2ContactPoint):void {
  // 衝突時の力の大きさをもとに、エフェクトを発生させる数を計算する
  var num:int = point.normalForce / 10 + 1;
  // 計算ミスで大きな力がかかることがあるので、値を10までに制限する
  if (num > 10) {
    num = 10;
  }
  for (var i:int = 0; i < num; ++i) {
    parent.addContactEffect(point);
  }
}

衝突時の力をもとに、エフェクトを発生させる数numを計算しています。実際にtraceしてみると分かりますが、normalForceの値は数十程度になることが多いので、10で割っています。ちなみにnormalForceの単位はニュートンです。

割り算の結果に1を加えているのは、最低でも1つ星を発生させるためです。また、まれにnormalForceが非常に大きくなるので、上限を10としています。

numが計算できたら、その回数だけ星を飛ばす処理を呼び出します。この処理は、KiraKiraクラスのaddContactEffectメソッドに書いてあります。

星を飛ばすには

説明の舞台をContactListener.asからKiraKira.asに移動します。この中に書かれているaddContactEffectメソッドは、星を飛ばすためのメソッドです。引数に渡されたb2ContactPointの情報から星を飛ばす位置を計算し、tweenerを使って星を飛ばします。

// 物と物が接触したときのエフェクトを発生させる
public function addContactEffect(point:b2ContactPoint):void { ... }

星の画像を読み込む

ではメソッドの中身について説明します。まずは星の画像からSpriteを作ります。ここは前回DebugDrawを使わずに物体を描画したときのパターンと似ているので、詳しい説明は省きます。

// 星の画像を読み込んで、サイズと位置を調整する
var starImage:Bitmap = new StarImage();
starImage.width = 50;
starImage.height = 50;
starImage.x = -starImage.width / 2;
starImage.y = -starImage.height / 2;

// 星の画像を表示するためのSpriteを作る
// 座標はコンタクトが発生した場所とする
var sprite:Sprite = new Sprite();
sprite.x = point.position.x * DRAW_SCALE;
sprite.y = point.position.y * DRAW_SCALE;
sprite.addChild(starImage);
addChild(sprite);

この処理の結果、sprite変数に星の画像が設定されます。場所についてはpoint.positionを参照し、物体が衝突した位置に設定します。

星を飛ばす先の座標を計算する

星を飛ばすときの原点はpoint.positionから求めることができたので、次はどこに飛ばすかを計算します。まずは飛ばす方向と距離を決めます。この値はMathクラスのrandomメソッドを使って適当な値にします。

// 星が飛んでいく方向と距離をランダムに決める
// 角度は0~360度、距離は50~150
var angle:Number = Math.random() * 360;
var length:Number = Math.random() * 100 + 50;

方向と距離が決まったら、それを使って飛んでいく先の座標を計算します。三角関数を使い、星が飛び始める点からangle度の方向にlengthだけ進んだ座標を計算しています。

// 星が飛んでいく先の座標を計算する
var dx:Number = sprite.x + length * Math.cos(angle);
var dy:Number = sprite.y + length * Math.sin(angle);

tweenerを使って星を飛ばす

座標が求まったのでいよいよ星を飛ばします。setTimeoutを使うなどして星を飛ばすプログラムを自前で書いていくと長くなってしまい、それだけでも説明を要するので、tweenerの力を借ります。星が飛んでいく先の座標dxとdyをtweenerに与えれば、そこまで移動する処理を行ってくれます。

// Tweenerで星を飛ばす
Tweener.addTween(sprite, {
  time: 3, // 3秒間
  scaleX: 0, // 消えるまで縮小する
  scaleY: 0,
  alpha: 0, // 完全に透明にする
  rotation: 200, // 少し回転させる
  x: dx,
  y: dy,
  onComplete: function():void {
    removeChild(sprite);
  }
});

単純に星を飛ばす(移動させる)だけでもいいのですが、せっかくtweenerを使うので色々な処理をしています。3秒かけて、回転しながら、どんどん小さく透明になっていくようにします。

3秒経過してアニメーションが終わるとonCompleteが呼び出され、ステージ上からspriteが取り除かれます。

KiraKiraクラスでは、この他にもクリックで円を作ったりする処理が含まれていますが、これらについては以前に触れたことなので、説明は省略します。

まとめ

コンタクトリスナを使い、物体同士が衝突したときに星が飛び散るFlashを作りました。ActionScriptの一般的なイベント処理とは異なる書き方をするので、そこだけは慣れが必要です。逆に、Javaなどの言語のイベントリスナを知っている方であれば、この方法を自然に感じるかもしれません。

コンタクトリスナの挙動が若干おかしい場合もありますが、これはBox2Dのバージョンアップで改善されていくものと思われます。また、いつかは衝突検出がイベントによって通知されるようになるかもしれません。

7回の連載を通じてBox2Dの解説をしてきましたが、いかがだったでしょうか。今回のコンタクトリスナをはじめ、少し癖のある書き方を強いられることがありますが、それだけの魅力があるライブラリだと思います。

ライブラリの全ての部分を説明することはできませんでしたが、説明できた範囲だけでも、工夫次第で面白いコンテンツが作れるのではないかと思います。そういったコンテンツが続々と出てくることを期待して、連載を締めくくらせていただきます。

おすすめ記事

記事・ニュース一覧