前回の第29回「Box2Dで落としたボールを床に弾ませる」は、剛体のボールひとつを床に自由落下させた。今回は、ボールの数を増やしてランダムな大きさと位置で落とすとともに、スクリプトの組立ても整えたい。前回書き上げた第29回コード2「自由落下させたボールを静的な床の上で弾ませる」に手を加えてゆこう。
ボールをつくり続ける
第29回コード2には、ボールをつくる関数(addBall())が定められている。そして、素材画像を読込み終えたリスナー関数(loadFinished())から呼び出した。だが、ボールをいくつもつくるため、このリスナーに替えて、Ticker.tickイベントのリスナー関数(tick())から呼び出すことにする。
もっとも、Ticker.tickイベントのたびにボールを増やしたのでは、間隔が短すぎる。そこで、イベント間の経過時間をイベントオブジェクトのdeltaプロパティで調べて変数(duration)に加算し、合計値が予め決めた変数(interval)の時間より大きくなったら剛体のボールをつくることにした。そのため、経過時間はボールを増やす関数(addBall())の引数(delta)に加えて渡している。なお、ボールの剛体のxy座標値(nXとnY)および半径(radius)はランダムに定めた。
これで、大小さまざまなボールが、ランダムな位置からつぎつぎに床に向けて自由落下し、互いにぶつかり合っては床下に落ちる(図1)。これこそ、まさに物理シミュレーションの醍醐味だ。さて、これだけのコードで不安を覚えた読者はおられるだろうか。その危機感は正しい。
ボールのオブジェクトと剛体は、ステージと物理空間にひたすら加えられるばかりで、除かれることがない。ボールが床下に見えなくなったといっても、それはあくまで人形のボールから見た話だ。黒子のBox2Dは構うことなく、物理空間に加えられたすべての剛体のシミュレーションを続ける。演算の負荷は瞬く間に上がってしまう。
では、どうすればいいかというと、本連載でもすでに紹介してきたようにオブジェクトを使い回すことだ。ステージから見えなくなった人形のオブジェクトは表示リストから外すとともに、剛体も物理演算のシミュレーションから除く。けれど、その剛体とオブジェクトはとっておき、新たなボールを求められたときに初期化して使い回せばよい。そうしたところがまだ不完全とはいえ、参照の便を考えてここまでをコード1としてつぎにまとめた。
ボールのオブジェクトを使い回す
剛体のb2Bodyオブジェクトを物理演算シミュレーションから外したり、加えたりするには、b2Body.SetActive()メソッドを用いる。引数はブール(論理)値で、falseを与えると物理演算の対象から外れる。改めて、シミュレーションに加えるときはtrueを渡せばよい。
ただし、b2Body.SetActive()メソッドで剛体を物理演算シミュレーションから除いても、b2Worldオブジェクトの物理空間にはそのまま残る。つまり、b2World.GetBodyList()やb2Body.GetNext()メソッドで拾えてしまうということだ。物理演算からそれらは外して、無駄を省くようにしたい。
剛体のb2Bodyオブジェクトを使い回すときには、その物理情報を初期化することも忘れてならない。移動と回転の速度は、それぞれb2Body.SetLinearVelocity()とb2Body.SetAngularVelocity()メソッドで0にする。そして、使い回す剛体の新たな位置と角度を、b2Body.SetPositionAndAngle()メソッドで与えればよい(表1)。
ボールの剛体を増やす関数(addBall)は、いきなり剛体をつくる関数(createDynamicBall())を呼び出すのではなく、以下のように新たに定めた関数(getDynamicBall())の呼出しにより剛体のオブジェクトを使い回す。剛体のオブジェクトは、変数(balls)に定めた配列に納めておく。剛体を使い回す関数は、配列にオブジェクトがあればそれを取り出し、ない場合のみ新たな剛体をつくる。
配列から取り出したエレメントの剛体は、まずb2Body.SetActive()メソッドで物理演算シミュレーションに戻す。そのうえで、前掲表1のメソッドにより、速度は0にし、位置を引数の値に、回転角は0とした。なお、移動速度の初期化に用いるため、0ベクトルのb2Vec2オブジェクトをあらかじめ変数(ZERO_VECTOR)に定めておいた。
つぎは、ステージから見えなくなったボールを表示リストから外し、剛体を物理演算シミュレーションから除く処理だ。物理演算を進める関数(update())は、以下のように書き替えた。落ちた剛体(body)がステージ下端(bottomLimit)から消えるまではシミュレーションを続ける。下端からステージの外に出た剛体はシミュレーションから外し、ボールのオブジェクト(myObject)は表示リストから除いたうえで、使い回す剛体の配列(balls)に加えた。
なお、ステージ下端の座標(bottomLimit)は、素材をロードし終えたときのリスナー関数(loadFinished())が定めている。また、剛体を使い回しの配列(balls)に加える前に、同じ剛体がすでにエレメントとして納められていないかを念のため確かめた。indexOf()は、引数が配列エレメントならそのインデックスを、エレメントになければ-1を返すCreateJSの便利な関数だ。
これで自由落下する剛体のオブジェクトが使い回される。つまり、床から落ちてステージの下に消えたボールの剛体は、新たな設定が与えられてまたステージの上の方から落とされる。初めの見た目はコード1と同じだ。けれど、ずっとシミュレーションを続けても処理が重くなることはない。script要素全体はつぎのコード2にまとめた。
Box2Dの名前空間の扱い
仕上げとして、Box2Dの名前空間の扱いについて考えたい。CreateJSのライブラリのクラスは、「createjs」という名前空間を添えて参照する。これは、さまざまなライブラリを合わせて用いたとき、たまたまクラス名が重複して使えなくなるのを避ける仕組みだ。とはいえ、読者の多くは、Box2Dの名前空間が長過ぎると思われたろう。落語の「寿限無」ではないのだからと。たとえば、b2PolygonShapeだ。
ひとつの手は、Box2Dのクラスの参照を変数に入れてしまうことだ。だが、変数が多くなってしまう。とくに、グローバルな変数の数は、やたらと増やしたくない。そこで、変数にはオブジェクトをひとつ定め、Box2Dのクラスを以下のようにそのプロパティとして与える。これなら、変数(box2d)はひとつで済み、参照の仕方もCreateJSと変わらない[1]。
するとたとえば、前掲コード2の剛体をつくる関数(createDynamicBall())は、つぎのように書き替えられる。これで、ステートメントが短くなって、コードも書きやすい。
名前空間については、いちいち書直しの前と後のステートメントを示すのはかえって煩わしいだろう。書替えを済ませたscript要素をつぎのコード3にまとめた。今回のサンプルでは、Box2Dの同じクラスをあちこちで参照しているところがあまり多くない。したがって、書く手間でいえば、Box2Dのクラスをオブジェクトのプロパティに定めたからといって、さほど楽になった訳ではない。とはいえ、コードが見やすくなり、後あとの拡張や修正を考えれば改善されたといえよう。
このたびのBox2Dを使ったお題は、これででき上がりとする。jsdo.itにコード3を掲げた。次回は、また新たなお題を考えたい。