【pixi.js】見おろし型2Dゲームの衝突判定を作る

javascript

見下ろし型 2Dゲームの衝突判定

こんにちは。継続の錬金術士なおキーヌです。

ブログ毎日更新は105日目になります。

ゲーム作り入門者向けのpixi.js講座【衝突判定編】は全5回になります。

前回記事の最後に張り付けたコードをコピーして今回の記事を進めてください。

今回からジャンプ機能を一旦削除しているので、
パックマンタイプの見おろし型上下左右移動が出来るタイプの衝突判定を作っていこうと思います。

前回の記事では左から移動してきた時の衝突判定だけをつくっていたので、
今回はおさらいも含めて左だけではなく上下と右からの衝突も作ってみましょう。

衝突判定関数の条件を修正してみる

衝突判定関数 修正

今の状態では左からはぶつかれますが、右からはぶつかれない状態になっています。
試しに壁より右の位置にプレイヤーを設置してみるとよくわかると思います。

squareのx座標を700とかにしてみると、右からはすり抜けられますが
一回すり抜けてしまうと戻れなくなります。

これはプログラムとしては欠陥なのですが、スーパーマリオとかアクションゲームで考えると
下からはすり抜けられるけど足場として使えるタイプの判定に使えたりします。

ですが今回は上下左右に移動できるタイプのゲームにしたいのでこの仕組みは要りません。

なので右から移動してきた時の判定も付け加えてみましょう。

衝突判定の関数に条件式を付け加えてから、キー入力の左に移動する処理を
作った関数に置き換えて右からも衝突判定が出来るように書き換えてみましょう。

前回の記事で構造上欠陥があったので少しだけ判定の計算式を弄っています。

        ~~~省略~~~
          // キー入力監視
          switch(keyFlag) {
            case 1:
              square.x += wallCollisiton(10,0);
              break;
            case 2:
              square.x += wallCollisiton(-10,0);
              break;
            case 3:
              square.y += 10;
              break;
            case 4:
              square.y -= 10;
              break;
            default:
              break;
          }
        ~~~省略~~~

        function wallCollisiton(x,y) {
          // 仮引数yが0の場合
          if(y === 0) {
            // プレイヤーのy座標が壁の下辺の座標より大きい もしくは プレイヤーの下辺の座標が壁のy座標より小さければ
            if (square.y > (wall.y + wall.height) || (square.y + square.height) < wall.y) {
              // 壁にぶつかることはないのでxをそのまま返す
              return x;
            } else {
              // そうでなければ
              // プレイヤーの右辺の座標が、壁の右辺の座標未満かつ、仮引数xが0より高ければ(左から右にぶつかろうとしている場合)
              if((square.x + square.width) < wall.x + wall.width && x > 0) {
                // プレイヤーの右辺の座標+仮引数xが、壁のx座標以上であれば(めり込んでいれば)
                if ( (square.x + square.width + x) >= wall.x  ) {
                  // プレイヤーの右辺の座標+仮引数 から 壁のx座標-1を引いた数値を仮引数から引いたものを返す(位置補正)
                  return x - ( (square.x + square.width + x) - ( wall.x - 1 ) );
                } else {
                  // めり込んでいなければxをそのまま返す
                  return x;
                }
              } else if(square.x > wall.x && x < 0) {
                // プレイヤーのx座標が壁のx座標以上かつ、仮引数が0未満であれば(右から左にぶつかろうとしている場合)
                // プレイヤーのx座標+仮引数xが壁の右辺より小さければ(めり込んでいれば)
                if ( square.x + x <= (wall.x + wall.width ) ) {
                  // の右辺の座標+仮引数 から 壁のx座標-1を引いた数値を仮引数から引いたものを返す(位置補正)
                  return x + ( (wall.x + wall.width + 1) - (square.x + x) );
                } else {
                  // めり込んでなければxをそのまま返す
                  return x;
                }
              } else {
                // どこの条件にも満たさなければ
                return x;
              }
            }
          }
          else if (x === 0) {
            return y;
          }
        }

だいぶコードが増えていますが、やっていることは結構単純なので紐解いてみてみましょう。

プレイヤーが壁の高さより下か上にいるときはぶつかり判定をしない

前回のままだと高さを一切見ていなかったので、どこの高さに居てもx座標が衝突判定をしていたら
動けなくなっていましたが、プレイヤーが壁のどこにもぶつかっていない状態であれば、
ぶつかっている判定を一切しないことで壁を乗り越えられるようにしました。

逆にそのままx座標を動かしてどこかに触れてしまう場合は判定を取るようにしています。

それ以外は前回とほぼ変わらずですが、1つだけ変わっているのが位置補正の部分で、
加算する仮引数xを補正して壁にぶつかっているようにするときに、
-1もしくは+1をして位置補正するようにしました。

こうしないとx座標が密接している時にy座標を動かそうとするとバグが発生してしまったので
1ドット分離すことで一旦バグを回避しています。

見た目は少し隙間が開いてしまいますが、実際ゲームを作るときは矩形マックスまでドット絵を書くことが
あまりないとおもうので一旦これで逃げてしまうというのも手です。

当たり判定はこれが最適解!といいものが存在しないのでゲームごとに微調整が必要になります。

今回作ろうとしている見下ろし型2Dゲームは今回作る判定で十分です。

y座標も作ってみよう

作るも何も、x座標の判定をそっくりそのままxをy、widthをheightに変更するだけでy座標の判定を作ることが出来ます。

現在y座標は何も判定を行っていませんので、xと同じように変更してみましょう。

        ~~~省略~~~

          // キー入力監視
          switch(keyFlag) {
            case 1:
              square.x += wallCollisiton(10,0); // 変更
              break;
            case 2:
              square.x += wallCollisiton(-10,0); // 変更
              break;
            case 3:
              square.y += wallCollisiton(0,10); // 変更
              break;
            case 4:
              square.y += wallCollisiton(0,-10); // 変更
              break;
            default:
              break;
          }

        ~~~省略~~~

        function wallCollisiton(x,y) {
          // 仮引数yが0の場合
          if(y === 0) {
            // プレイヤーのy座標が壁の下辺の座標より大きい もしくは プレイヤーの下辺の座標が壁のy座標より小さければ
            if (square.y > (wall.y + wall.height) || (square.y + square.height) < wall.y) {
              // 壁にぶつかることはないのでxをそのまま返す
              return x;
            } else {
              // そうでなければ
              // プレイヤーの右辺の座標が、壁の右辺の座標未満かつ、仮引数xが0より高ければ(左から右にぶつかろうとしている場合)
              if((square.x + square.width) < wall.x + wall.width && x > 0) {
                // プレイヤーの右辺の座標+仮引数xが、壁のx座標以上であれば(めり込んでいれば)
                if ( (square.x + square.width + x) >= wall.x  ) {
                  // プレイヤーの右辺の座標+仮引数 から 壁のx座標-1を引いた数値を仮引数から引いたものを返す(位置補正)
                  return x - ( (square.x + square.width + x) - ( wall.x - 1 ) );
                } else {
                  // めり込んでいなければxをそのまま返す
                  return x;
                }
              } else if(square.x > wall.x && x < 0) {
                // プレイヤーのx座標が壁のx座標以上かつ、仮引数が0未満であれば(右から左にぶつかろうとしている場合)
                // プレイヤーのx座標+仮引数xが壁の右辺より小さければ(めり込んでいれば)
                if ( square.x + x <= (wall.x + wall.width ) ) {
                  // の右辺の座標+仮引数 から 壁のx座標-1を引いた数値を仮引数から引いたものを返す(位置補正)
                  return x + ( (wall.x + wall.width + 1) - (square.x + x) );
                } else {
                  // めり込んでなければxをそのまま返す
                  return x;
                }
              } else {
                // どこの条件にも満たさなければ
                return x;
              }
            }
          }
          else if (x === 0) {
            if (square.x > (wall.x + wall.width) || (square.x + square.width) < wall.x) {
              return y;
            } else {
              if((square.y + square.height) < wall.y + wall.height && y > 0) {
                if ( (square.y + square.height + y) >= wall.y  ) {
                  return y - ( (square.y + square.height + y) - ( wall.y - 1 ) );
                } else {
                  return y;
                }
              } else if(square.y > wall.y && y < 0) {
                if ( square.y + y <= (wall.y + wall.height ) ) {
                  return y + ( (wall.y + wall.height + 1) - (square.y + y) );
                } else {
                  return y;
                }
              } else {
                return y;
              }
            }
          }
        }

ほぼコピペですね。

このままでは1つの壁しか判定が出来ない

今回はここまでとなりますが、このままでは見たまんま1つ壁しか判定を取っていないので汎用性がありません。

しかも毎回プレイヤーと壁の右辺の座標を取得するためにx座標と幅を足して取得していますが
これも毎回書いていると見通しが悪くなるので何かしら関数化してコードの見通しを良くした方が良いでしょう。

しかし最適化に関しては後からでもいいのでとにかく処理を完成させることを優先しましょう。

いきなり完成されたコードを目指そうとするとドツボにハマって頓挫してしまうことが多いです。

コーディングは正解というものは1つではなく効率が悪い書き方でも動いてしまえば大丈夫です。

ですがその際に何をしているのかは絶対にコメントを書いておきましょう。

コードを改善する時に何をしていたのかわからなくなってしまったら成長が妨げられます。

次回は、【pixi.js】衝突判定関数を使いやすいように改良してみようになります。

コードを見やすくしたら壁を増やすチャレンジをしてみたいと思います。

最後に完成版コードを乗せておきます。

それでは。


<!doctype html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <title>pixi.js v4 テスト</title>
    <style>
      * { margin:0; padding:0; }
    </style>
  </head>
  <body>
      <script src="https://cdnjs.cloudflare.com/ajax/libs/pixi.js/4.5.1/pixi.min.js"></script>
      <script>
        // フラグの定義
        keyFlag = 0;

        // pixi.jsのアプリケーションを作成
        const app = new PIXI.Application();

        // bodyにpixi.jsのview(ステージ)を追加する
        document.body.appendChild(app.view);

        // pixi.jsのGprahicsオブジェクトのインスタンスを生成
        const square = new PIXI.Graphics();
        const goal = new PIXI.Graphics();
        const wall = new PIXI.Graphics();

        // squareの大きさと位置を設定
        square.width = 50;
        square.height = 50;
        square.x = 750;
        square.y = 550;

        // squareの塗りつぶしと矩形描写
        square.beginFill(0xff00ff);
        square.drawRect(0,0,50,50);
        square.endFill();

        // 【追加】goalの大きさと位置を設定
        goal.width = 100;
        goal.height = 20;
        goal.x = 650;
        goal.y = 500;

        // 【追加】goalの塗りつぶしと矩形描写
        goal.beginFill(0x00ffff);
        goal.drawRect(0,0,100,20);
        goal.endFill();

        // 【追加】wallの大きさと位置を設定
        wall.width = 20;
        wall.height = 150;
        wall.x = 650;
        wall.y = 500;

        // 【追加】wallの塗りつぶしと矩形描写
        wall.beginFill(0x00ff00);
        wall.drawRect(0,0,20,150);
        wall.endFill();

        // ステージにsquareとgoalを追加
        app.stage.addChild(square);
        app.stage.addChild(goal);
        app.stage.addChild(wall); // 追加分

        // ループ処理の実装
        app.ticker.add(delta => this.gameloop(delta,square));

        // イベントリスナー登録
        window.addEventListener("keydown", (event) => { this.downHandler(event) },false);
        window.addEventListener("keyup", (event) => { this.upHandler(event) },false);

        // downHandlerを定義
        // 【更新箇所】上下の移動を消してxキーによるy座標の減算を記載
        function downHandler(event) {
          switch(event.key) {
            case 'ArrowRight':
            keyFlag = 1;
            break;
            case 'ArrowLeft':
            keyFlag = 2;
            break;
            case 'ArrowDown':
            keyFlag = 3;
            break
            case 'ArrowUp':
            keyFlag = 4;
            break
            default:
            break;
          }
        }

        // upHandlerを定義
        function upHandler(event) {
          keyFlag = 0;
        }

        // ゲームループ関数の中身(毎フレーム実行される)
        function gameloop(delta, square) {
          // 衝突監視

          // キー入力監視
          switch(keyFlag) {
            case 1:
              square.x += wallCollisiton(10,0); // 変更
              break;
            case 2:
              square.x += wallCollisiton(-10,0); // 変更
              break;
            case 3:
              square.y += wallCollisiton(0,10); // 変更
              break;
            case 4:
              square.y += wallCollisiton(0,-10); // 変更
              break;
            default:
              break;
          }
        }


        // 衝突監視関数
        function collisionJudge() {
          // プレイヤーの右端がゴールの左端以上 かつ プレイヤーの左端がゴールの右端以内であればY座標の判定に入る
          if( (square.x + square.width) >= goal.x && square.x <= (goal.x + goal.width) ) {
            // プレイヤーの下端がゴールの上端以上 かつ プレイヤーの上端がゴールの下端以内であればヒットしているとみなす
            if( (square.y + square.height) >= goal.y && square.y <= (goal.y + goal.height) ) {
              return true;
            }
          }
          return false;
        }

        function wallCollisiton(x,y) {
          // 仮引数yが0の場合
          if(y === 0) {
            // プレイヤーのy座標が壁の下辺の座標より大きい もしくは プレイヤーの下辺の座標が壁のy座標より小さければ
            if (square.y > (wall.y + wall.height) || (square.y + square.height) < wall.y) {
              // 壁にぶつかることはないのでxをそのまま返す
              return x;
            } else {
              // そうでなければ
              // プレイヤーの右辺の座標が、壁の右辺の座標未満かつ、仮引数xが0より高ければ(左から右にぶつかろうとしている場合)
              if((square.x + square.width) < wall.x + wall.width && x > 0) {
                // プレイヤーの右辺の座標+仮引数xが、壁のx座標以上であれば(めり込んでいれば)
                if ( (square.x + square.width + x) >= wall.x  ) {
                  // プレイヤーの右辺の座標+仮引数 から 壁のx座標-1を引いた数値を仮引数から引いたものを返す(位置補正)
                  return x - ( (square.x + square.width + x) - ( wall.x - 1 ) );
                } else {
                  // めり込んでいなければxをそのまま返す
                  return x;
                }
              } else if(square.x > wall.x && x < 0) {
                // プレイヤーのx座標が壁のx座標以上かつ、仮引数が0未満であれば(右から左にぶつかろうとしている場合)
                // プレイヤーのx座標+仮引数xが壁の右辺より小さければ(めり込んでいれば)
                if ( square.x + x <= (wall.x + wall.width ) ) {
                  // の右辺の座標+仮引数 から 壁のx座標-1を引いた数値を仮引数から引いたものを返す(位置補正)
                  return x + ( (wall.x + wall.width + 1) - (square.x + x) );
                } else {
                  // めり込んでなければxをそのまま返す
                  return x;
                }
              } else {
                // どこの条件にも満たさなければ
                return x;
              }
            }
          }
          else if (x === 0) {
            if (square.x > (wall.x + wall.width) || (square.x + square.width) < wall.x) {
              return y;
            } else {
              if((square.y + square.height) < wall.y + wall.height && y > 0) {
                if ( (square.y + square.height + y) >= wall.y  ) {
                  return y - ( (square.y + square.height + y) - ( wall.y - 1 ) );
                } else {
                  return y;
                }
              } else if(square.y > wall.y && y < 0) {
                if ( square.y + y <= (wall.y + wall.height ) ) {
                  return y + ( (wall.y + wall.height + 1) - (square.y + y) );
                } else {
                  return y;
                }
              } else {
                return y;
              }
            }
          }
        }
      </script>
    </body>
  </html>
</html>