サイトトップ

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

HTML5テクニカルノート

EaselJSのオブジェクトを物理演算エンジンのBox2Dで落とす

ID: FN1209008 Technique: HTML5 and JavaScript

Box2D」はもともとC++で開発された2次元空間の物理演算エンジンです[*1]。重力と物体の質量や摩擦、弾性にもとづく位置と動きの物理計算を行い、シミュレーションしてくれます。ActionScript 3.0やJava、C#、Pythonなど、さまざまな言語に移植されています。JavaScriptのライブラリとしても、いくつか公開されてきました。その中でも対応するバージョンが新しく、解説も整っているBox2dWebを使ってみましょう。

[*1] Box2Dを使ったもっとも有名なアプリケーションに「Crayon Physics Deluxe」があります。


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

物理演算エンジンというのは、物体の位置や動きの数値計算をするだけで、オブジェクトの座標を変えたりはしません。つまり、エンジンがいくら演算を進めても、コンテンツの見た目は止まったままです。演算結果をオブジェクトの座標に与えて、初めてアニメーションとして動きます。

物理演算のシミュレーションをする前に、定めておかなければならないことが3つあります。

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

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

第2に、物理エンジンで扱う物体となる「剛体(Rigid body)」を定めます。Box2Dで剛体をつくるには、剛体の定義を先にb2BodyDefオブジェクトとしてつくらなければなりません。

第3に、物理演算の結果をオブジェクトの動きとして見せるため、コンテンツのオブジェクトを剛体の定義と関連づけます。本稿では、EaselJSのShapeオブジェクトを用います。

手始めに、この3つの手順にしたがって物理シミュレーションの準備をしましょう。Box2dWebのサイトからダウンロードしたJavaScript(JS)ファイルを、適切なフォルダに入れます。ファイルはひとつで、名前に「min」のつく方が容量を縮めたコンパクト版です。このBox2dWebをscript要素に読込んでおきます。上記3つの準備までを終えたscript要素の全体は、コード001として後に掲げます。抜書きした行番号は、そのコード001にもとづきます。

  1. <script src="lib/Box2dWeb-2.1.a.3.min.js"></script>

では、3つの手順に沿ってご説明します。なお、Box2dWebは、ActionScript 3.0に開発されたBox2DFlashを移植したものです。そのため、リファレンスとしてBox2DFlashのドキュメントを参照します。

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

Box2Dは数多くのクラスから成立ちます。Box2dWebは、それらのクラスをファイルでは分けず、代わりに名前空間により分類・識別します。そのため、クラスを参照するときには、頭にそれぞれの名前空間を添えなければなりません。同じクラスを何度も用いる場合には、名前空間で参照したクラスを予め変数に入れておくと便利でしょう。本稿はこの書き方をとります。

var world = new Box2D.Dynamics.b2World(重力, スリープ);

または

var b2World = Box2D.Dynamics.b2World;
var world = new b2World(重力, スリープ);

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

  1. var world;
  2. var gravityVertical = 10;
  1. function initialize() {
  1.   initializeBox2D(canvasElement.width);
  1. }
  2. function initializeBox2D(nWidth) {
  3.   var b2Vec2 = Box2D.Common.Math.b2Vec2;
  1.   var b2World = Box2D.Dynamics.b2World;
  2.   world = new b2World(new b2Vec2(0, gravityVertical), true);
  1. }

第2は、剛体の定義です。後で落下させる予定の剛体は、1辺20ピクセルの正方形とし、その長さを変数(boxEdge)に置きます(第18行目)。そして、剛体を定義する関数(createBodyDef())は別に定めました(第34〜40行目)。関数の第1および第2引数(nXとnY)は、剛体のxy座標です。第3引数(nType)は剛体の種類で、b2Bodyの定数(静的プロパティ)が定める整数とします。

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

  1. var SCALE = 1 / 30;
  1. var boxEdge = 20;
  2. function initialize() {
  3.   var canvasElement = document.getElementById("myCanvas");
  1.   initializeBox2D(canvasElement.width);
  1. }
  2. function initializeBox2D(nWidth) {
  1.   var b2Body = Box2D.Dynamics.b2Body;
  1.   var bodyDef = createBodyDef(nWidth / 2, boxEdge, b2Body.b2_dynamicBody);
  1. }
  2. function createBodyDef(nX , nY, nType) {
  3.   var b2BodyDef = Box2D.Dynamics.b2BodyDef;
  4.   var bodyDef = new b2BodyDef();
  5.   bodyDef.position.Set(nX * SCALE, nY * SCALE);
  6.   bodyDef.type = nType;
  7.   return bodyDef;
  8. }

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

第3に、Shapeインスタンスをつくって、剛体の定義されたb2BodyDefオブジェクトと関連づけます。この処理も別に関数(createQuadForBodyDef())を定めました(第41〜48行目)。新たなShapeインスタンスに矩形を描く(draw()関数を呼出す)ところは(第42〜43行目)、とくに説明は要らないでしょう。

注目してほしいのは、関数(createShapeForBodyDef())が引数にb2BodyDefオブジェクトを受取って、そのb2BodyDef.userDataプロパティにShapeオブジェクトを設定していることです(第46行目)。これで、Shapeオブジェクトは剛体を定義するb2BodyDefオブジェクトに関連づけられます。剛体を表す表示オブジェクトの基準点は、基本的には中心(重心)に置きます(第44〜45行目)。

そして、関数(createShapeForBodyDef())から返されたShapeインスタンスは、Stageオブジェクトの表示リストに加えられます(第31〜32行目)。

  1. var boxEdge = 20;
  2. function initialize() {
  3.   var canvasElement = document.getElementById("myCanvas");
  4.   stage = new Stage(canvasElement);
  5.   initializeBox2D(canvasElement.width);
  1. }
  2. function initializeBox2D(nWidth) {
  1.   var bodyDef = createBodyDef(nWidth / 2, boxEdge, b2Body.b2_dynamicBody);
  2.   var myShape = createShapeForBodyDef(bodyDef, boxEdge, boxEdge, "#0000FF");
  3.   stage.addChild(myShape);
  4. }
  1. function createShapeForBodyDef(bodyDef, nWidth, nHeight, nColor) {
  2.   var myShape = new Shape();
  3.   draw(myShape, nWidth, nHeight, nColor);
  4.   myShape.regX = nWidth / 2;
  5.   myShape.regY = nHeight / 2;
  6.   bodyDef.userData = myShape;
  7.   return myShape;
  8. }
  9. function draw(myShape, nWidth, nHeight, nColor) {
  10.   var myGraphics = myShape.graphics;
  11.   myGraphics.beginFill(nColor);
  12.   myGraphics.drawRect(0, 0, nWidth, nHeight);
  13. }

これで、3つの準備が整いました。書上がったscript要素全体は、つぎのコード001のとおりです。物理演算のシミュレーションはまだ行っていませんので、青い正方形のShapeインスタンスがCanvasの左上角に表れるだけです(図001)。

コード001■Box2Dの物理ワールドをつくり剛体を定義する
  1. <script>
  2. var createjs = window;
  3. </script>
  4. <script src="lib/Box2dWeb-2.1.a.3.min.js"></script>
  5. <script src="easeljs/utils/UID.js"></script>
  6. <script src="easeljs/geom/Matrix2D.js"></script>
  7. <script src="easeljs/events/MouseEvent.js"></script>
  8. <script src="easeljs/display/DisplayObject.js"></script>
  9. <script src="easeljs/display/Container.js"></script>
  10. <script src="easeljs/display/Stage.js"></script>
  11. <script src="easeljs/display/Graphics.js"></script>
  12. <script src="easeljs/display/Shape.js"></script>
  13. <script>
  14. var stage;
  15. var SCALE = 1 / 30;
  16. var world;
  17. var gravityVertical = 10;
  18. var boxEdge = 20;
  19. function initialize() {
  20.   var canvasElement = document.getElementById("myCanvas");
  21.   stage = new Stage(canvasElement);
  22.   initializeBox2D(canvasElement.width);
  23.   stage.update();
  24. }
  25. function initializeBox2D(nWidth) {
  26.   var b2Vec2 = Box2D.Common.Math.b2Vec2;
  27.   var b2Body = Box2D.Dynamics.b2Body;
  28.   var b2World = Box2D.Dynamics.b2World;
  29.   world = new b2World(new b2Vec2(0, gravityVertical), true);
  30.   var bodyDef = createBodyDef(nWidth / 2, boxEdge, b2Body.b2_dynamicBody);
  31.   var myShape = createShapeForBodyDef(bodyDef, boxEdge, boxEdge, "#0000FF");
  32.   stage.addChild(myShape);
  33. }
  34. function createBodyDef(nX , nY, nType) {
  35.   var b2BodyDef = Box2D.Dynamics.b2BodyDef;
  36.   var bodyDef = new b2BodyDef();
  37.   bodyDef.position.Set(nX * SCALE, nY * SCALE);
  38.   bodyDef.type = nType;
  39.   return bodyDef;
  40. }
  41. function createShapeForBodyDef(bodyDef, nWidth, nHeight, nColor) {
  42.   var myShape = new Shape();
  43.   draw(myShape, nWidth, nHeight, nColor);
  44.   myShape.regX = nWidth / 2;
  45.   myShape.regY = nHeight / 2;
  46.   bodyDef.userData = myShape;
  47.   return myShape;
  48. }
  49. function draw(myShape, nWidth, nHeight, nColor) {
  50.   var myGraphics = myShape.graphics;
  51.   myGraphics.beginFill(nColor);
  52.   myGraphics.drawRect(0, 0, nWidth, nHeight);
  53. }
  54. </script>

図001■Canvas左上角に青い正方形のShapeオブジェクトが表れる
図001


02 剛体とフィクスチャの定義から剛体をつくる

物理演算のシミュレーションに入るには、剛体(b2Bodyオブジェクト)をつくらなければなりません。そのためには、剛体の定義だけでは足りません。フィクスチャという(b2Fixtureクラスの)オブジェクトを剛体に定めます[*1]。フィクスチャも、先にb2FixtureDefクラスで定義します。b2FixtureDefオブジェクトには、重さ(密度)や滑りやすさ(摩擦)、跳ね返りの度合い(弾性)などを定めます。また、かたちを決めるオブジェクトも加えます。そして、剛体とフィクスチャのふたつの定義から剛体をつくるのです。

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

前掲コード001に、この剛体をつくるふたつの処理を加えます。script要素全体は後にコード002として掲げます。その中からこれらふたつの手順に関わるところを、それぞれ抜書きしてご説明します(行番号は後継コード002にもとづきます)。

第1に、b2FixtureDefオブジェクトでフィクスチャを定義するために、関数を3つ定めました。まず、b2FixtureDefオブジェクトをつくって返す関数(createFixtureDefWithShape())です(第64〜69行目)。この関数の中から、さらにふたつ目の、オブジェクトにかたちを与える関数(setShapeToFixtureDef())を呼出しています(第67行目)。

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

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

  1. var boxEdge = 20;
  1. function initializeBox2D() {
  1.   var fixtureDef = createFixtureDefWithShape(boxEdge, boxEdge);
  2.   setFixtureDef(fixtureDef, 1, 0.5, 0.5);
  1. }
  1. function createFixtureDefWithShape(nWidth, nHeight) {
  2.   var b2FixtureDef = Box2D.Dynamics.b2FixtureDef;
  3.   var fixtureDef = new b2FixtureDef();
  4.   setShapeToFixtureDef(fixtureDef, nWidth / 2, nHeight / 2);
  5.   return fixtureDef;
  6. }
  7. function setShapeToFixtureDef(fixtureDef, nX, nY) {
  8.   var b2PolygonShape = Box2D.Collision.Shapes.b2PolygonShape;
  9.   var myPolygonShape = new b2PolygonShape();
  10.   myPolygonShape.SetAsBox(nX * SCALE, nY * SCALE);
  11.   fixtureDef.shape = myPolygonShape;
  12. }
  13. function setFixtureDef(fixtureDef, density, friction, restitution) {
  14.   fixtureDef.density = density;
  15.   fixtureDef.friction = friction;
  16.   fixtureDef.restitution = restitution;
  17. }

第2に、剛体とフィクスチャのふたつの定義から、剛体のb2Bodyオブジェクトをつくります。そのために定めた関数(createBodyWithFixture())はb2Worldとb2BodyDefおよびb2FixtureDefの3オブジェクトを引数として受取り、それらからつくられたb2Bodyインスタンスを返します(第81〜85行目)。まず、b2Bodyオブジェクトは、b2World.CreateBody()メソッドでb2BodyDefオブジェクトを引数としてつくり、物理ワールドに加えます(第82行目)。そしてつぎに、b2Body.CreateFixture()メソッドにより、引数のb2FixtureDefオブジェクトにもとづいて、フィクスチャ(b2Fixtureオブジェクト)をb2Bodyオブジェクトに定めます(第83行目)。

  1. function initializeBox2D() {
  1.   world = new b2World(new b2Vec2(0, gravityVertical), true);
  2.   var bodyDef = createBodyDef(centerX, boxEdge, b2Body.b2_dynamicBody);
  1.   var fixtureDef = createFixtureDefWithShape(boxEdge, boxEdge);
  1.   var body = createBodyWithFixture(world, bodyDef, fixtureDef);
  1. }
  1. function createBodyWithFixture(world, bodyDef, fixtureDef) {
  2.   var body = world.CreateBody(bodyDef);
  3.   body.CreateFixture(fixtureDef);
  4.   return body;
  5. }

これでBox2Dの物理演算シミュレーションの仕込みは済みました。けれど、まだステージ上のShapeインスタンスは動きません。残るは、シミュレーションの計算を繰返し進めて、インスタンスにアニメーションとして演算結果を反映することです。

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

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

03 剛体を落下させる − 物理演算シミュレーションの実行

物理演算は目に見えません。表示オブジェクトのアニメーションと違って、フレームレートで再描画されるという決まりもありません。ですから、一定の時間進めては演算し直すという繰返しを、意図的に組込みます。そして、演算結果を表示オブジェクトの座標や回転角に反映するのです。この時間を進めて物理演算を行うメソッドが、b2World.Step()です。

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

第1引数は、シミュレーションを行うに当たって進める単位時間です。表示オブジェクトをフレームレートでアニメーションさせるなら、フレームごとに進める時間はフレームレートの逆数(1 / FPS)になります。第2および第3引数は、物理的な制約に合うように、速度と位置をそれぞれ調整する再計算の回数です[*2]。回数を増やすとシミュレーションの精度は上がるものの、処理の負荷が上がります。

EaselJSのアニメーションは、Tickerクラスで扱うのがお約束です。物理演算のシミュレーションを組入れて、インスタンスをアニメーションさせましょう。前掲コード001にシミュレーションの処理を加えたscript要素全体は、後にコード002として掲げます。抜書きしてご説明する行番号は、コード002にもとづきます。

アニメーションの準備として、script要素でTickerクラスを読込み(第13行目)、Ticker.addListener()メソッドでtickイベントを起こします(第30行目)。また、物理演算シミュレーションの単位時間を変数(time)に定めましたので(第21行目)、Ticker.setInterval()メソッドでアニメーションの単位時間を合わせます(第29行目)。

  1. <script src="easeljs/utils/Ticker.js"></script>
  1. var time = 1 / 24;
  1. function initialize() {
  1.   Ticker.setInterval(time * 1000);
  2.   Ticker.addListener(this);
  3. }

tick()関数は、まずb2World.Step()メソッドで物理演算シミュレーションを進めます(第87行目)。b2World.Step()メソッドに渡す3つの引数値は、変数に宣言しました(第19〜21行目)。

つぎに、b2World.GetBodyList()メソッドで、物理ワールド(b2Worldオブジェクト)に加えた最初の剛体となるb2Bodyインスタンスを取出します(第88行目)。そして、b2Body.GetUserData()メソッドにより、その剛体の定義に関連づけた表示オブジェクトの参照を得ます(第89行目)。念のため、表示オブジェクト(DisplayObject)があるかどうかを、if条件で確かめておきます(第90行目)。

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

  1. var velocityIterations = 10;
  2. var positionIterations = 10;
  3. var time = 1 / 24;
  1. function tick() {
  2.   world.Step(time, velocityIterations, positionIterations);
  3.   var body = world.GetBodyList();
  4.   var myObject = body.GetUserData();
  5.   if (myObject) {
  6.     var position = body.GetPosition();
  7.     myObject.x = position.x / SCALE;
  8.     myObject.y = position.y / SCALE;
  9.     myObject.rotation = body.GetAngle();
  10.     stage.update();
  11.   }
  12. }

これでBox2Dの物理演算シミュレーションにもとづいて、インスタンスが自然落下します(図002)。ここまでの書替えをすべて加えたscript要素の全体は、つぎのコード002のとおりです。

図002■正方形の剛体が落下する
図002

コード002■剛体に定めたShapeインスタンスが自然落下するアニメーション
  1. <script>
  2. var createjs = window;
  3. </script>
  4. <script src="lib/Box2dWeb-2.1.a.3.min.js"></script>
  5. <script src="easeljs/utils/UID.js"></script>
  6. <script src="easeljs/geom/Matrix2D.js"></script>
  7. <script src="easeljs/events/MouseEvent.js"></script>
  8. <script src="easeljs/display/DisplayObject.js"></script>
  9. <script src="easeljs/display/Container.js"></script>
  10. <script src="easeljs/display/Stage.js"></script>
  11. <script src="easeljs/display/Graphics.js"></script>
  12. <script src="easeljs/display/Shape.js"></script>
  13. <script src="easeljs/utils/Ticker.js"></script>
  14. <script>
  15. var stage;
  16. var SCALE = 1 / 30;
  17. var world;
  18. var gravityVertical = 10;
  19. var velocityIterations = 10;
  20. var positionIterations = 10;
  21. var time = 1 / 24;
  22. var boxEdge = 20;
  23. var centerX;
  24. function initialize() {
  25.   var canvasElement = document.getElementById("myCanvas");
  26.   stage = new Stage(canvasElement);
  27.   centerX = canvasElement.width / 2;
  28.   initializeBox2D();
  29.   Ticker.setInterval(time * 1000);
  30.   Ticker.addListener(this);
  31. }
  32. function initializeBox2D() {
  33.   var b2Vec2 = Box2D.Common.Math.b2Vec2;
  34.   var b2Body = Box2D.Dynamics.b2Body;
  35.   var b2World = Box2D.Dynamics.b2World;
  36.   world = new b2World(new b2Vec2(0, gravityVertical), true);
  37.   var bodyDef = createBodyDef(centerX, boxEdge, b2Body.b2_dynamicBody);
  38.   var myShape = createShapeForBodyDef(bodyDef, boxEdge, boxEdge, "#0000FF");
  39.   var fixtureDef = createFixtureDefWithShape(boxEdge, boxEdge);
  40.   setFixtureDef(fixtureDef, 1, 0.5, 0.5);
  41.   var body = createBodyWithFixture(world, bodyDef, fixtureDef);
  42.   stage.addChild(myShape);
  43. }
  44. function createBodyDef(nX , nY, nType) {
  45.   var b2BodyDef = Box2D.Dynamics.b2BodyDef;
  46.   var bodyDef = new b2BodyDef();
  47.   bodyDef.position.Set(nX * SCALE, nY * SCALE);
  48.   bodyDef.type = nType;
  49.   return bodyDef;
  50. }
  51. function createShapeForBodyDef(bodyDef, nWidth, nHeight, nColor) {
  52.   var myShape = new Shape();
  53.   draw(myShape, nWidth, nHeight, nColor);
  54.   myShape.regX = nWidth / 2;
  55.   myShape.regY = nHeight / 2;
  56.   bodyDef.userData = myShape;
  57.   return myShape;
  58. }
  59. function draw(myShape, nWidth, nHeight, nColor) {
  60.   var myGraphics = myShape.graphics;
  61.   myGraphics.beginFill(nColor);
  62.   myGraphics.drawRect(0, 0, nWidth, nHeight);
  63. }
  64. function createFixtureDefWithShape(nWidth, nHeight) {
  65.   var b2FixtureDef = Box2D.Dynamics.b2FixtureDef;
  66.   var fixtureDef = new b2FixtureDef();
  67.   setShapeToFixtureDef(fixtureDef, nWidth / 2, nHeight / 2);
  68.   return fixtureDef;
  69. }
  70. function setShapeToFixtureDef(fixtureDef, nX, nY) {
  71.   var b2PolygonShape = Box2D.Collision.Shapes.b2PolygonShape;
  72.   var myPolygonShape = new b2PolygonShape();
  73.   myPolygonShape.SetAsBox(nX * SCALE, nY * SCALE);
  74.   fixtureDef.shape = myPolygonShape;
  75. }
  76. function setFixtureDef(fixtureDef, density, friction, restitution) {
  77.   fixtureDef.density = density;
  78.   fixtureDef.friction = friction;
  79.   fixtureDef.restitution = restitution;
  80. }
  81. function createBodyWithFixture(world, bodyDef, fixtureDef) {
  82.   var body = world.CreateBody(bodyDef);
  83.   body.CreateFixture(fixtureDef);
  84.   return body;
  85. }
  86. function tick() {
  87.   world.Step(time, velocityIterations, positionIterations);
  88.   var body = world.GetBodyList();
  89.   var myObject = body.GetUserData();
  90.   if (myObject) {
  91.     var position = body.GetPosition();
  92.     myObject.x = position.x / SCALE;
  93.     myObject.y = position.y / SCALE;
  94.     myObject.rotation = body.GetAngle();
  95.     stage.update();
  96.   }
  97. }

[*2] 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)」に記載されています。


04 静的な剛体を加える − 衝突のシミュレーション

前掲コード002で剛体を落とすことはできたものの、Canvasの下端に消えてしまいます。また、フィクスチャにかたちを定め、密度や摩擦、弾性といった値を与えても、他の剛体にぶつけてみないとわかりません。そこで、物理ワールドに床を加えます。

その前に、スクリプトを少し整理しましょう。落下する矩形の剛体をつくる処理(前掲コード002第37〜41行目)は、関数として分けることにします。床に矩形の剛体を落とすように書替えたスクリプトのでき上がりは、後にコード003として掲げます。抜書きを示すスクリプトの行番号はこのコード003にもとづきます。

床に落とす剛体をつくる関数(createDynamicBox())は、xy座標と幅、高さ、塗り色の引数5つを渡して呼出します(第28行目)。剛体のフィクスチャ定義に定める密度と摩擦および弾性の値は、変数に定めました(第9〜11行目)。また、床をつくるときCanvasの幅や高さが要るので、Box2Dの初期化関数(initializeBox2D())にはふたつの値を引数として渡すことにします(第15行目)。すると、水平方向真ん中の位置を、変数(centerX)にとっておかなくて済みます。

    // var centerX;
  1. var density = 1;
  2. var friction = 0.5;
  3. var restitution = 0.5;
  4. function initialize() {
  5.   var canvasElement = document.getElementById("myCanvas");
      // centerX = canvasElement.width / 2;
      // initializeBox2D();
  1.   initializeBox2D(canvasElement.width, canvasElement.height);
  1. }
    // function initializeBox2D() {
  2. function initializeBox2D(stageWidth, staggeHeight) {
      // var b2Body = Box2D.Dynamics.b2Body;

      /*
      var bodyDef = createBodyDef(centerX, boxEdge, b2Body.b2_dynamicBody);
      var myShape = createShapeForBodyDef(bodyDef, boxEdge, boxEdge, "#0000FF");
      var fixtureDef = createFixtureDefWithShape(boxEdge, boxEdge);
      setFixtureDef(fixtureDef, 1, 0.5, 0.5);
      setFixtureDef(fixtureDef, density, friction, restitution);
      var body = createBodyWithFixture(world, bodyDef, fixtureDef);
      */
  1.   var centerX = stageWidth / 2;
  1.   var myShape = createDynamicBox(centerX, boxEdge, boxEdge, boxEdge, "#0000FF");
  2.   stage.addChild(myShape);
  3. }
  1. function createDynamicBox(nX, nY, nWidth, nHeight, nColor) {
  2.   var b2Body = Box2D.Dynamics.b2Body;
  3.   var bodyDef = createBodyDef(nX, nY, b2Body.b2_dynamicBody);
  4.   var myShape = createShapeForBodyDef(bodyDef, nWidth, nHeight, nColor);
  5.   var fixtureDef = createFixtureDefWithShape(nWidth, nHeight);
  6.   setFixtureDef(fixtureDef, density, friction, restitution);
  7.   var body = createBodyWithFixture(world, bodyDef, fixtureDef);
  8.   return myShape;
  9. }

それでは、床を加える処理も関数(createStaticFloor())として定め(第31〜38行目)、落下させる剛体をつくる関数(createDynamicBox())と同じように、Box2Dの初期化関数(initializeBox2D())から呼出すことにします(第26行目)。渡す引数も処理の中身も、基本的には落下させる剛体の関数と変わりません。

  1. function initializeBox2D(stageWidth, staggeHeight) {
  1.   var centerX = stageWidth / 2;
  2.   var floorWidth = stageWidth * 0.8;
  3.   var floorHeight = boxEdge;
  4.   var floorShape = createStaticFloor(centerX, staggeHeight - floorHeight, floorWidth, floorHeight, "#CCCCCC");
  5.   stage.addChild(floorShape);
  1. }
  2. function createStaticFloor(nX, nY, nWidth, nHeight, nColor) {
  3.   var b2Body = Box2D.Dynamics.b2Body;
  4.   var bodyDef = createBodyDef(nX, nY, b2Body.b2_staticBody);
  5.   var myShape = createShapeForBodyDef(bodyDef, nWidth, nHeight, nColor);
  6.   var fixtureDef = createFixtureDefWithShape(nWidth, nHeight);
  7.   var body = createBodyWithFixture(world, bodyDef, fixtureDef);
  8.   return myShape;
  9. }

床を加える関数(createStaticFloor())と落下する剛体をつくる関数(createDynamicBox())との違いはふたつです。ひとつは、床は動きませんので、剛体の定義(b2BodyDefオブジェクト)に定めるb2BodyDef.typeプロパティには、静的となるb2Body.b2_staticBodyを指定したことです(第33行目)。

もうひとつ、床の剛体をつくるとき、フィクスチャに密度・摩擦・弾性を定める関数(setFixtureDef())は呼出しません(前掲コード第44行目と比較)。なぜなら、b2FixtureDefオブジェクトは、デフォルト設定のままとくに変えなくてよいからです。

ここまで手を加えてアニメーションを確かめると、落下する矩形の剛体は確かに床とおぼしきあたりで弾むものの、床のShapeインスタンスがCanvas左上角に取り残されています(図003)。これは、物理演算シミュレーションの結果が床のインスタンスに反映されていないためです。つまり、演算とアニメーションを扱うtick()関数も書替えなければなりません。

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

前掲コード002のtick()関数では、物理ワールド(b2Worldオブジェクト)に加えた最初のb2Bodyインスタンスを、b2World.GetBodyList()メソッドで取出し、物理演算の結果をインスタンスに適用しました。ですから、矩形のインスタンスが落下したのです。けれど、新たに加えた床のb2Bodyインスタンスはまだ取出してさえいませんから、演算結果が反映されるはずがありません。

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

なお、矩形と床の剛体はTicker.tickイベントごとに一緒に描画すればよく、whileループでそれぞれを分けて描くのは無駄です。そのため、Stage.update()メソッドは、whileループの外に出しました(第103行目)。

  1. function tick() {
  2.   world.Step(time, velocityIterations, positionIterations);
  3.   var body = world.GetBodyList();
  4.   while (body) {
  5.     var myObject = body.GetUserData();
  6.     if (myObject) {
  7.       var position = body.GetPosition();
  8.       myObject.x = position.x / SCALE;
  9.       myObject.y = position.y / SCALE;
  10.       myObject.rotation = body.GetAngle();
          // stage.update();
  11.     }
  12.     body = body.GetNext();
  13.   }
  14.   stage.update();
  15. }

コンテンツの動きを確かめると、床がステージ下側に置かれ、落下した矩形が軽く弾みながら、やがて床の上で止まります(図004)。ここまで書き加えたスクリプト全体が、つぎのコード003です。

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

コード003■動的な剛体が落下して静的な剛体のうえで弾むアニメーション
  1. var stage;
  2. var SCALE = 1 / 30;
  3. var world;
  4. var gravityVertical = 10;
  5. var velocityIterations = 10;
  6. var positionIterations = 10;
  7. var time = 1 / 24;
  8. var boxEdge = 20;
  9. var density = 1;
  10. var friction = 0.5;
  11. var restitution = 0.5;
  12. function initialize() {
  13.   var canvasElement = document.getElementById("myCanvas");
  14.   stage = new Stage(canvasElement);
  15.   initializeBox2D(canvasElement.width, canvasElement.height);
  16.   Ticker.setInterval(time * 1000);
  17.   Ticker.addListener(this);
  18. }
  19. function initializeBox2D(stageWidth, staggeHeight) {
  20.   var b2Vec2 = Box2D.Common.Math.b2Vec2;
  21.   var b2World = Box2D.Dynamics.b2World;
  22.   world = new b2World(new b2Vec2(0, gravityVertical), true);
  23.   var centerX = stageWidth / 2;
  24.   var floorWidth = stageWidth * 0.8;
  25.   var floorHeight = boxEdge;
  26.   var floorShape = createStaticFloor(centerX, staggeHeight - floorHeight, floorWidth, floorHeight, "#CCCCCC");
  27.   stage.addChild(floorShape);
  28.   var myShape = createDynamicBox(centerX, boxEdge, boxEdge, boxEdge, "#0000FF");
  29.   stage.addChild(myShape);
  30. }
  31. function createStaticFloor(nX, nY, nWidth, nHeight, nColor) {
  32.   var b2Body = Box2D.Dynamics.b2Body;
  33.   var bodyDef = createBodyDef(nX, nY, b2Body.b2_staticBody);
  34.   var myShape = createShapeForBodyDef(bodyDef, nWidth, nHeight, nColor);
  35.   var fixtureDef = createFixtureDefWithShape(nWidth, nHeight);
  36.   var body = createBodyWithFixture(world, bodyDef, fixtureDef);
  37.   return myShape;
  38. }
  39. function createDynamicBox(nX, nY, nWidth, nHeight, nColor) {
  40.   var b2Body = Box2D.Dynamics.b2Body;
  41.   var bodyDef = createBodyDef(nX, nY, b2Body.b2_dynamicBody);
  42.   var myShape = createShapeForBodyDef(bodyDef, nWidth, nHeight, nColor);
  43.   var fixtureDef = createFixtureDefWithShape(nWidth, nHeight);
  44.   setFixtureDef(fixtureDef, density, friction, restitution);
  45.   var body = createBodyWithFixture(world, bodyDef, fixtureDef);
  46.   return myShape;
  47. }
  48. function createBodyDef(nX , nY, nType) {
  49.   var b2BodyDef = Box2D.Dynamics.b2BodyDef;
  50.   var bodyDef = new b2BodyDef();
  51.   bodyDef.position.Set(nX * SCALE, nY * SCALE);
  52.   bodyDef.type = nType;
  53.   return bodyDef;
  54. }
  55. function createShapeForBodyDef(bodyDef, nWidth, nHeight, nColor) {
  56.   var myShape = new Shape();
  57.   draw(myShape, nWidth, nHeight, nColor);
  58.   myShape.regX = nWidth / 2;
  59.   myShape.regY = nHeight / 2;
  60.   bodyDef.userData = myShape;
  61.   return myShape;
  62. }
  63. function draw(myShape, nWidth, nHeight, nColor) {
  64.   var myGraphics = myShape.graphics;
  65.   myGraphics.beginFill(nColor);
  66.   myGraphics.drawRect(0, 0, nWidth, nHeight);
  67. }
  68. function createFixtureDefWithShape(nWidth, nHeight) {
  69.   var b2FixtureDef = Box2D.Dynamics.b2FixtureDef;
  70.   var fixtureDef = new b2FixtureDef();
  71.   setShapeToFixtureDef(fixtureDef, nWidth / 2, nHeight / 2);
  72.   return fixtureDef;
  73. }
  74. function setShapeToFixtureDef(fixtureDef, nX, nY) {
  75.   var b2PolygonShape = Box2D.Collision.Shapes.b2PolygonShape;
  76.   var myPolygonShape = new b2PolygonShape();
  77.   myPolygonShape.SetAsBox(nX * SCALE, nY * SCALE)
  78.   fixtureDef.shape = myPolygonShape;
  79. }
  80. function setFixtureDef(fixtureDef, density, friction, restitution) {
  81.   fixtureDef.density = density;
  82.   fixtureDef.friction = friction;
  83.   fixtureDef.restitution = restitution;
  84. }
  85. function createBodyWithFixture(world, bodyDef, fixtureDef) {
  86.   var body = world.CreateBody(bodyDef);
  87.   body.CreateFixture(fixtureDef);
  88.   return body;
  89. }
  90. function tick() {
  91.   world.Step(time, velocityIterations, positionIterations);
  92.   var body = world.GetBodyList();
  93.   while (body) {
  94.     var myObject = body.GetUserData();
  95.     if (myObject) {
  96.       var position = body.GetPosition();
  97.       myObject.x = position.x / SCALE;
  98.       myObject.y = position.y / SCALE;
  99.       myObject.rotation = body.GetAngle();
  100.     }
  101.     body = body.GetNext();
  102.   }
  103.   stage.update();
  104. }

床の幅は、あえてCanvas幅より狭めました(前掲コード003第24および第26行目)。したがって、剛体の水平位置が端になると、床から落ちてしまうこともあります(図005)。コード003で、たとえば落下する剛体をつくる関数(createDynamicBox())の呼出しに、ランダムな水平位置を引数として渡せば試せるでしょう(第28行目)。

図005■矩形の剛体が床の端から落ちる
図005

  1. function initializeBox2D(stageWidth, staggeHeight) {
    // var myShape = createDynamicBox(centerX, boxEdge, boxEdge, boxEdge, "#0000FF");
  1.   var myShape = createDynamicBox(stageWidth * Math.random(), boxEdge, boxEdge, boxEdge, "#0000FF");
  2.   stage.addChild(myShape);
  3. }

【参考】数多くの剛体をランタムな位置から落とす

まとめとして、50個の剛体をランダムな位置から落としてみます。といっても、Box2Dについて新たに説明することはありません。Box2Dの初期化の関数(initializeBox2D())でforループを使って、落下する剛体作成の関数(createDynamicBox())を繰返し呼出すだけです。後のアニメーションに関わる物理演算は、すべてBox2Dが行ってくれます。サンプルのHTMLドキュメントをリンクして掲げましたので、興味のある方は直接コードをご覧ください。なお、剛体のカラーもランダムに変えました。

コード004■50個の剛体をランダムな位置から落とすアニメーション
図006左   図006右


作成者: 野中文雄
更新日: 2012年1月11日 コード中の無駄なステートメントを修正。
更新日: 2012年9月27日 04「静的な剛体を加える − 衝突のシミュレーション」の本文とコードに若干の追加・補正。
更新日: 2012年9月25日 リファレンスのリンクの一部を「Box2dWebで物理ワールドと剛体の定義を定める」に変更。
作成日: 2012年9月17日


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