ニコ生ゲームを作ろうと思ったら物理的に考える その5


今回は接触する物体の情報を利用する方法と物体をつかむ方法を紹介していきます。

 

以下がサンプルです。音は抜いています。

・ニコニコ煮込み鍋 ファイル アツマール


■目次

  • 物体に力を加える
  • 物体に衝撃を加える
  • 接触イベントで物体の情報を利用する .GetFixtureA().GetBody()
  • b2BodyからEbodyとentityを特定する .getEBodyFromb2Body()
  • 接触した物体を操作する
  • 物体をつかんで操作する
  • ジョイントの種類
  • マウスジョイントを作る
  • ローカル座標のグローバル座標を取得する localToGlobal
  • マウスジョイントの目的地を変える
  • マウスジョイントを解除する
  • 物体の角度を固定する body.b2Body.SetFixedRotation(true)
  • 物体をすり抜けさせない bullet

 

衝突物体に力を加える

今回のゲームはなべに具材を浮かせて、ヘラでかき混ぜるゲームです。

液体の中で浮かばせるために、重力とは別に浮力を加えます

 

力と衝撃を加えるのは基本中の基本ですが紹介が遅れてしまいました。

力が衝撃と違うのは継続するか一瞬かです。力は重力のように継続して加わります。

         body.b2Body.ApplyForce(box2d.vec2(0, -1000), body.b2Body.GetPosition());

 

このような形でApplyForceを使います。カッコの中には2つの座標を入れます。

1つ目のbox2d.vec2(0, -1000)は力の大きさと向きです。Yの値は適当ですがマイナスなら上向きに力が加わります。

2つ目のbody.b2Body.GetPosition()力を加える場所です。今回は物体の中心に力を加えるためにGetPositon()を使って物体の中心座標を入れています。
box2d.vec2(entity.x, entity.y)のようにbox2d.vec2で指定してもいけます。

 

力を加える場所を中心以外にすると回転してしまいます。あと、重心が物体の中心にないような場合もちょっとややこしくなりそうです。

 

 

しかし、今回このApplyForceを使ったかというと、使いませんでした

 

今回は鍋から出ると浮力がなくなってほしいので、力のOFFが必要です。

この方法を調べたところ、ClearForcesというものがありそうでしたが使い方がわからず。結局、次の章にあるApplyImpulseで継続的に衝撃を加えて代用しました。


物体に衝撃を加える

ApplyImpulseも基本中の基本で公式のサンプルコードでもよく出てきます。

         body.b2Body.ApplyImpluse(box2d.vec2(0, -1000), body.b2Body.GetPosition());

という形でApplyForceに似ています。

1つ目は衝撃の大きさ。2つ目は衝撃が加わる場所で、box2d.vec2でも指定可です。

 

衝撃の大きさの目安は物理の計算が面倒です。数字を適当に入れて確認すると楽です。値が小さいと動いていないように見えるので1000000とか大きな数字を試しましょう。

 

ApplyImpluseがLinearVelocityより優れているのは、慣性を残せるところです。
LinearVelocityだとそれまでの動きを無視して動きを決めますが、ApplyImpulseだと勢いと衝撃を組み合わせるためより複雑な動きができます。

 

ApplyImpluseのデメリットは、衝撃が加わる場所を指定する必要があるところです。
1つ目と2つ目の値のどっちが場所の指定なのかも忘れがちですし、場所の座標の算出にも気を使います。

入力する座標ゲーム全体のグローバルな座標なので、物体を回転させる場合は回転に合わせて座標(と方向も?)を調整する必要があります。回転させたい場合はジョイントというものもあるのでそちらがいいかもしれません。

 

そして、今回は浮力の代用にするため、onUpdateの常に処理するイベントの中でApplyImpluseを使いました。607行目にあります。

  entity.onUpdate.add(function () {
      body.b2Body.SetLinearDamping(0.2);
      if (Math.abs(entity.x - g.game.width/2) < 400) { //鍋の中の処理
          if (entity.y > 350) {
              body.b2Body.ApplyImpulse(box2d.vec2(0, -18 * body.b2Body.GetMass()), body.b2Body.GetPosition());
              body.b2Body.SetLinearDamping(2);
          }
      }

               ︙
  });

座標で鍋の中にあるかどうか判定し、ギリギリ上に動く大きさの衝撃を加えています。ついでに水の中はDampingを大きくして抵抗がある感じに指定しています。

 


接触イベントで物体の情報を利用する

接触イベントはcontactListener.BeginContactの中に記載します。前々回やりました。

今回は接触した物体の情報をもとに処理を変えます

 

取得する情報はfixturecontact.GetFixtureA()でまず取得します。

そして続けて.b2BodyGetBody()で取得します。以下のように書きます。

    contactListener.BeginContact = function (contact) {
        let a = contact.GetFixtureA().GetBody();
        let b = contact.GetFixtureB().GetBody();
    ︙
    }

これでaとbが接触している2つの物体のb2Bodyになります。

aとbは勝手に名前をつけていますが、FixtureAとFixtureBは決められた名前です。

 

b2BodyがわかればUserDataも取得できます。a.GetUserData() もしくは最初から書くとcontact.GetFixtureA().GetBody().GetUserData() になります。

 

今回のゲームでは同じ具材が重なると消えるルールなので、2つのUserDataが同じかどうかを判定して処理を進めています。

            if (a.GetUserData() == b.GetUserData()){
                ︙
            }

 

b2Bodyだけでなくentityの情報も取得することが可能です。次の章に続きます

 

 

b2BodyからEbodyとentityを特定する

物体には Ebody と b2Body と entity の3つがあります。

Ebodyの下に残り2つがぶら下がっている形なので、b2Bodyからentityを特定するにはまず.getEBodyFromb2Body()を使ってEbodyを特定します。

        let a = contact.GetFixtureA().GetBody();
        let b = contact.GetFixtureB().GetBody();
        let ea = box2d.getEBodyFromb2Body(a);
        let eb = box2d.getEBodyFromb2Body(b);

 

これでeaとebがそれぞれのEbodyになります。

entityを特定するにはEbodyに.entityをつけるだけです。ea.entity と eb.entity ですね。

 

色々と特定が完了したら操作もしていきたいですが、contactListener.の中では制約があります。次の章に続きます

 


接触した物体を操作する

今回は同じ種類の物体が接触すると消えるルールなので、removeBodyをやりたいところですがcontactListener.の中で以下のように記載するとエラーになります。

        contactListener.BeginContact = function (contact) {
            let a = contact.GetFixtureA().GetBody();
            let b = contact.GetFixtureB().GetBody();
            let ea = box2d.getEBodyFromb2Body(a);
            let eb = box2d.getEBodyFromb2Body(b);
            if (a.GetUserData() == b.GetUserData()){
                box2d.removeBody(ea);
                ea.entity.destroy();
                box2d.removeBody(eb);
                eb.entity.destroy();
           }
        }

 

同じようにApplyImpulseなどもエラーが出ます

詳細はわかりませんが、b2Bodyに関わるところは操作できないと思われます。

一方で、それ以外の変数の操作・entity.tagの編集・entity.modified()などはできます

scene.setTimeoutで発動するタイミングをずらせばb2Bodyも操作できるみたいです。

 

これを利用して今回はentity.tagをfalseからtrueに変更しておいて、別途用意したonUpdateのイベントでentity.tagを見て削除しました。

 

485行目に以下のように記載しています。

    contactListener.BeginContact = function (contact) {
        let a = contact.GetFixtureA().GetBody();
        let b = contact.GetFixtureB().GetBody();
        let ea = box2d.getEBodyFromb2Body(a);
        let eb = box2d.getEBodyFromb2Body(b);
        if (a.GetUserData() != "wall" && b.GetUserData() != "wall" && touch.opacity == 0){
            if (a.GetUserData() == b.GetUserData()){
                if (!ea.entity.tag && !eb.entity.tag) {
                    ea.entity.tag = true;
                    eb.entity.tag = true;
                    addscore(a.GetUserData());
                }
            }
        }
    }

処理が長いのでaddscore()という関数にまとめていますが、その中でもEbodyとb2Bodyの操作はしていません。

 

onUpdateの方は615行目にあります。

entity.onUpdate.add(function () {
      ︙
     if (entity.tag || entity.y > (g.game.height + d)) { //同種接触または画面下に行った処理
        scene.assets["se_gu"].stop();
        if (soundstate) scene.assets["se_gu"].play().changeVolume(0.3);
        addlabel(entity.frames[0], entity.x, entity.y);
        addgu++;
        box2d.removeBody(body);
        entity.destroy();
    }
});

 

 

物体をつかんで操作する

今回のゲームでは物体としてのヘラをつかんでドラッグで操作します。

 

物体をタッチしている位置に追随させるには、マウスジョイントを使います。

タッチ位置と物体の位置から計算してApplyImpluseを調整することもできますが、マウスジョイントなら簡単にできます。

公式のサンプルコードでもこちらで使われています。

 

 

ジョイントの種類

マウスジョイントはジョイントの一種です。

ジョイントは何かの物体と物体をつなげて特定の動きをさせるもので、よく使われそうな動きが複数用意されています。

 

私もまだ使ったことがないものばかりなので、ざっくり想像で機能を書きます。

  • b2DistanceJointDef
    物体と物体の距離を制御する。ひも。振り子
    使用例はこちら

  • b2FrictionJointDef
    摩擦? 見下ろし視点の摩擦 カーリングの停止のようなイメージか?

  • b2GearJointDef
    歯車。物体の回転が連動する?

  • b2MouseJointDef
    マウスで物を動かす。特定の地点への引力にも使えそう
    使用例はこちら

  • b2PrismaticJointDef
    直線上を動く。ピストンやバネの動き。ピンボールの発射とか
    なぜPrismaticという名前なのかは不明
    使用例はこちら

  • b2PulleyJointDef
    プーリー。滑車。井戸のようなものに使える?

  • b2RevoluteJointDef
    物体と物体をピンで止める。自由に回転させたり、回転する力を加えたり
    関節やタイヤに使えそう

  • b2WeldJointDef
    溶接。おそらく物体同士をつなげて固定する
    ゲーム中に別々の物体をくっつける場合に使えそう

 

あとb2MotorJointDef 、b2WheelJointDef というのも一般的なBox2dにはあるようなのですが、エディターの候補に出ないのでAkashicEngineでは使えないのかもしれません。

 

 

マウスジョイントを作る

マウスジョイントを作るには、b2.Box2DWeb.Dynamics.Joints.b2MouseJointDef()を使って「マウスジョイントの設定」を作ります。

 let jointdef = new b2.Box2DWeb.Dynamics.Joints.b2MouseJointDef();

 

そしてその後に基本のパラメーターを5つ設定します

  • bodyA
    つなげる1つ目の物体
    マウスジョイントの場合は、ワールド全体を一つの物体とみなす値としてbox2d.world.GetGroundBody()を指定します

  • bodyB
    つなげる2つ目の物体
    マウスで操作したい物体をb2Bodyで指定します

  • collideConnected
    bodyAとbodyBが衝突するかどうか
    Web上の記事だとマウスジョイントの場合trueにするように書かれているんですが、falseでも特に問題ない気がします。よくわかりません

  • maxForce
    引き寄せる力
    大きければぴったり追従し、小さければ遅れたり引っかかって止まります

  • target
    引き寄せる場所の座標
    通常の座標ではなくBox2D用の座標にするため、box2d.vec2()を使います

 

最後にマウスジョイント」に「マウスジョイントの設定」を登録します。

マウスジョイントは適当に変数を作っておきます。

  let mousejoint = null;

設定の登録はbox2d.world.CreateJoint()を使います。

        mousejoint = box2d.world.CreateJoint(jointdef);

 

まとめると以下の形です。

  let mousejoint = null;
     ︙
       let jointdef = new b2.Box2DWeb.Dynamics.Joints.b2MouseJointDef();
        jointdef.bodyA = box2d.world.GetGroundBody();
        jointdef.bodyB = hera.b2Body;
        jointdef.collideConnected = false;
        jointdef.maxForce = 1000;
        jointdef.target = box2d.vec2(point.x, point.y);
        mousejoint = box2d.world.CreateJoint(jointdef);

 

mousejointとjointdefの名前は勝手につけているので別名でも大丈夫です。

 

ジョイントの設定は完了ですが、実際にマウスで操作するまで3章ほど続きます。

 


ローカル座標のグローバル座標を取得する

今回はtouchという四角いエンティティをヘラの物体の上に配置し、touchをクリックすることでヘラをつかんでいる感触にします。

 

マウスジョイントはtouchのonPointDownイベントで生成し、touchをクリックした場所を登録します。

 

クリックした場所はonPointDownのイベントの中ではev.point.xとev.point.yで取得できますが、これはtouchというエンティティの中での座標です。

 

グローバルな座標にするには、 touch.x + ev.point.x のように補正が必要です。
さらにanchorXが0.5になっている場合は、touch.x - touch.width/2 + ev.point.x になり、
回転があるとさらにややこしくなります。

この面倒な計算を省略するために .localToGlobal() を使います。

961行目にあります。

        let point = { x: 0, y: 0 };
    ︙
        point = touch.localToGlobal({ x: ev.point.x, y: ev.point.y });

 

上のような使い方をしますが、エンティティの名前につづけて.localToGlobalと記載し、カッコの中にxとyを持つ連想配列を入れます。

結果としてxとyを持つ連想配列ができるのでpoint.xやpoint.yのようにして使います。

 

これはbox2dとは関係ないので、box2dを入れていなくても使えます。
逆の関数としてGlobalToLocalもあります。

 


マウスジョイントの目的地を変える

マウスをドラッグさせるとその場所へ引き寄せられるようにします。

引き寄せる場所の更新SetTargetを使います。

        if (mousejoint != null) mousejoint.SetTarget(box2d.vec2(point.x, point.y));

 

mousejointのあとに .SetTargetをつけてカッコの中に新しい座標を入れます。

もしmousejointがうまく設定されていないとエラーになるので、if (mousejoint != null) をつけています。

 

 

目的地の座標はtouch.onPointMoveの中で更新します。

 

onPointDownと似たようなやり方で指定すると、

    touch.localToGlobal({ x: ev.point.x + ev.startDelta.x, y: ev.point.y + ev.startDelta.y });

となりますがこれはうまくいきません

おそらくtouchエンティティ自体が移動するので計算がずれていると思います。

 

代わりに、 ev.prevDelta.xev.prevDelta.y を使うとうまくいきます

        touch.onPointMove.add(function(ev) {
            if(!startstate) return;
            if(finishstate) return;
            point.x += ev.prevDelta.x;
            point.y += ev.prevDelta.y;
            if (mousejoint != null) mousejoint.SetTarget(box2d.vec2(point.x, point.y));
        });

なぜうまくいくのかはわかりませんが、とりあえずうまく行っています。

 


マウスジョイント解除する

クリックやタップを離したときは、マウスジョイントを解除して物体が自由に動くようにします。

onPointUpイベントの中でマウスジョイントを破壊し、次にonPointDownがあったときにまたマウスジョイントを生成します。

マウスジョイントの破壊にはDestroyJointを使います。

        touch.onPointUp.add(function(ev) {
            if(!startstate) return;
            if(finishstate) return;
            if (mousejoint != null) box2d.world.DestroyJoint(mousejoint);
            mousejoint = null;
        });

最後のmousejoint = nullは不要そうですが、公式のコードに有ったので残しています。

 


物体の角度を固定する

細かい設定で個人的によく使うのがSetFixedRotationです。

これをtrueにしておくと物体の角度が固定されます。

 

今回のヘラは常に立てた状態で使いたかったので551行目で設定しています。

            let body = box2d.createBody(entity, bodydef, fixturedef);
            body.b2Body.SetPosition(box2d.vec2(x, y));
            body.b2Body.SetAngle(box2d.radian(angle));
            body.b2Body.SetFixedRotation(true);

 

これがないとヘラがくるくる回って力が伝わりません。

 

 

これ以外で使える用途が、アクションゲームの操作キャラクターです。

 

エアリアルサッカーでは操作キャラクターがヘディングしているシーンをイメージしていたので.SetFixedRotationをtrueにしています。

ドアラッシュの方もキャラクター顔が回転するのが嫌だったので同じ設定です。

 

 

物体をすり抜けさせない

今回のヘラは見た目より薄くしているのですが、高速で動かしたときに具材をすり抜けるという問題が発生していました。

 

これを解決するために使用したのがBodyDefのbullet設定です。

これをtrueにしておくとすり抜けないようになります

 

もともとは高速で発射する弾丸に使う設定のようです。

どうせなら常にすり抜けないようにしたいところですが、これをtrueにすると処理が重くなるそうです。最低限の使用にしましょう。

 

535行目付近に以下の記載があります。

        function makehera(x, y, width, height, angle, color) {
      ︙

           let bodydef = box2d.createBodyDef({
                type: b2.BodyType.Dynamic, // 静止:Static 運動:Kinematic 動的:Dynamic
                linearDamping: 0, // 速度の減衰率 0~3ぐらい
                angularDamping: 0, // 回転速度の減衰率 0~3ぐらい
                bullet: true,
                userData: "wall",//識別用
            });
      ︙
        }

 

今回はここまでです。

 

今回の記事でBox2dの使いそうな機能はほぼ終わったと思います。

あとはまだ自分が使ったことのないジョイントの記事と、Box2dでマルチゲームをやるときの注意点の記事を作るかもしれません。

 

 

この記事を書いているとちょうどニコ生ゲーム開発のイベントが始まりました。

ch.nicovideo.jp

なかなかニコ生ゲーム開発者が増えてこない中で、裾野の広がりに期待したくなるありがたい企画ですね。

私もそちらのゲームを作っていこうと思います。

 

 

この記事以外にも、コピペできそうなコードや解説を公開しています。
こちらに記事のリンクがまとまっていますので御覧ください。