【pixi.js】衝突判定関数を使いやすいように改良してみよう

javascript

衝突判定 改良

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

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

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

今回は衝突判定の関数を使いまわしやすいように改良してみましょう。

現状、1つの壁にしか衝突判定を設定していないので、これから用意する壁すべてに
衝突判定を付けられるようにしたいので使いまわしやすいようにしましょう。

と、その前に前回まで計算式が少し間違っていたことをお詫びいたします。

気付いた方はいらっしゃるかと思いますが前回の記事で衝突した時に、
矩形と矩形の間に1px隙間が出来ていたと思います。

問題だったのはxの場合は右辺の座標、yの場合は下辺の座標を取得する
getEndPosという関数がありました。

ここでx座標+矩形のwidthで計算していたので実際の大きさ+1の位置を取得していました。

例えば50×50の矩形の場合、座標で見ると(0,0)-(49,49)の座標に居ます。

なのでwidth-1 or height-1という風にしてあげないと正確な座標を取得できませんでした。

関数にまとめている時に気付いたので、前回の記事をコピーしてgetEndPos関数を以下のように書き換えてください。

        // 矩形の右端座標を取得する関数 flag: true => x, false => y
        function getEndPos(sprite,flag=true) {
          if(flag) {
            // 大きさじゃなく座標なので-1している
            // 50x50の矩形の場合座標0~49にいるため
            return sprite.x + sprite.width-1;
          } else {
            return sprite.y + sprite.height-1;
          }
        }

これで衝突しても1pxの隙間が開かなくなりました。

これをやっていないとギリギリの隙間を通ろうとしたら、
行き詰ったので試行錯誤してみたらこの計算式の間違えでした。

プログラミングは大抵こういう小さなミスが原因で
数時間奪われることもあるので気を付けましょう。

衝突判定関数の書き換えと壁を増やしてみよう

壁を増やす

それでは衝突判定の関数を変更するのと、壁の数を増やしてみようと思います。

まずは今まで使っていた壁の変数を削除して、配列にしてみましょう。

        // 壁の位置を配列に格納
        const wallPos = [
          0,0, // 壁1のポジション x,y
          100,100, // 壁2のポジション x,y
        ]
        // 壁の位置を配列に格納
        const walls = [] 
        //
        for (let i = 0; i < 2; i++) {
          // PIXI.Graphicsを配列にnewしていく
          walls.push(new PIXI.Graphics());
          walls[i].width = 50;
          walls[i].height = 50;
          walls[i].beginFill(0x00ff00);
          walls[i].drawRect(0,0,50,50);
          walls[i].endFill();
        }
        // 初期位置設定
        walls[0].x = wallPos[0];
        walls[0].y = wallPos[1];
        walls[1].x = wallPos[2];
        walls[1].y = wallPos[3];

        ~~~省略~~~

        // 【変更】壁をすべてステージに追加
        for (let i = 0; i < walls.length; i++) {
          app.stage.addChild(walls[i]);
        }

まず壁の座標を格納したは配列を作ります。

本当はクラスを使ったり構造体を使ったりしてきれいにするのですが
今はわかりやすくするためにクラスも構造体も使用していません。

そして壁のオブジェクトを複数生成するために空の配列を作ります。

その後、生成したい数だけforで回してその中で配列のpush関数を使って
その中でPIXI.Graphicsをnewして壁オブジェクトを複数生成します。

forで回していると配列の添え字にアクセスできるので、
ついでに大きさと塗りつぶしをしておきましょう。

初期座標に関してはループの中ではやり辛いので、
ループが終わってから壁の初期位置の設定を行いましょう。

その後に壁配列のlengthメソッドを扱って配列の数を取得できますので
ループの条件式に組み込んでステージに壁をすべて追加しましょう。

衝突判定の修正

次に、衝突判定の関数が1つの壁を直接指定している状態になっているので
このまま実行してしまってはエラーが出てしまいます。

ここは大幅に変更しますが、やっていることはそこまで変わらないので
どこが変化したかをしっかり見ておいてください。

まずは配列をすべてチェックするために新しい関数を作ります。

        // 全ての衝突判定をチェック
        function allCollisitonCheck(x,y,player,targets) {
          for (let i = 0; i<targets.length; i++) {
            // 0以外になったらぶつかっている判定をされる
            switch( wallCollisiton(x,y,player,targets[i]) ) {
              case 1:
                return x - ( (getEndPos(player)+x) - ( targets[i].x - 1 ) );
                break;
              case 2:
                return x + ( (getEndPos(targets[i]) + 1) - (player.x + x) );
                break;
              case 3:
                return y - ( (getEndPos(player,flag=false)+y) - ( targets[i].y - 1 ) );
                break;
              case 4:
                return y + ( (getEndPos(targets[i],flag=false) + 1) - (player.y + y) );
                break;
            }
          }
          // 衝突判定されなかった場合はそのまま値を返す
          if(y === 0) {
            return x;
          } else {
            return y;
          }
        }

引数は衝突判定とほぼ同じですが、違っているところはターゲットの部分が配列を受け取るようにしています。

ちょっとプログラミング的な話になるのですが、ここでターゲットの部分に配列が渡されるのが理想です。

しかしjavascriptはC言語とかのような厳格な型指定がないので、配列じゃないものも渡される可能性が出てきます。

この関数は配列が来ると見越して中身の処理を作っているので、もし配列じゃないただの変数が渡されたりすると
配列の要素にアクセスできずエラーが発生してプログラムが強制終了してしまします。

これを対策するにはTypeScriptという型情報を指定できるjavascriptを利用するのがベストです。

TypeScriptについては調べてもらうと色々出てくるので興味があったら調べてください。

ちなみに私はTypeScriptでpixi.jsとElectronでゲームを作る予定です。

……話が脱線してしまいましたので元に戻りましょう。

このallCollisitonxの移動距離とyの移動距離とプレイヤーと壁の配列を渡しています。

allCollisitonxの処理内容

まずは渡されたターゲット配列でforを使って繰り返します。

次に、switchを使って値に前回作ったwallCollisiton関数を指定して帰ってくる値を0~4と仮定して
caseで条件を分岐させましょう。

  • 1: 右を押してぶつかったときに返ってくる値
  • 2: 左を押してぶつかったときに返ってくる値
  • 3: 下を押してぶつかったときに返ってくる値
  • 4: 上を押してぶつかったときに返ってくる値
  • 0: 何にもぶつからなかったときに帰ってくる値

それぞれのcaseの処理がwallCollision関数のぶつかったときの位置補正処理を返していますね。

どのcaseにも満たさない時はそのまま座標を変更したいので、
xかyの数値が0の方を条件として数値のある方を返しましょう。

次に、wallCollision関数でぶつかったときの位置補正処理を返していたところを、
条件分岐をするための数値を返す処理に書き換えます。

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

            } else {
              if(getEndPos(player,flag=false) < getEndPos(target,flag=false) && y > 0) {
                if ( getEndPos(player,flag=false) + y >= target.y  ) {
                  return 3 // 【変更点】
                } else {
                  return 0;
                }
              } else if(player.y > target.y && y < 0) {
                if ( player.y + y <= getEndPos(target,flag=false) ) {
                  return 4 // 【変更点】
                } else {
                  return 0; // 【変更点】
                }
              } else {
                return 0; // 【変更点】
              }
            }
          }
        }

基本的に変更点はwallになっていたところを引数のtargetに置き換えています。

次に重要な部分がreturnのところが0~4にしています。

これは先ほどのallCollisionCheck関数ように対応しているところに1~4の数値を返して下さい。

これで実行すると左上に壁の緑が2つ現れるのでちゃんと衝突判定が出来ているかチェックしてみましょう。

──今回で一旦衝突判定の処理の基礎は出来上がりました。

後は壁の数を増やしたり、どこまで増やしたら自分のPCでは重たくなるのかを調べてみるのも勉強になります。

pixi.jsは大量のスプライト(画像)を出しても軽いことで有名なので、
私は2Dゲームを作るのに最適かなと思って採用しています。

そろそろゴールデンウィークに向けてゲームを作るために必要な処理を作りたいので、
その中で紹介できそうな便利な処理があれば記事として挙げていこうと思います。

それ以外は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 wallPos = [
          0,0, // 壁1のポジション x,y
          100,100, // 壁2のポジション x,y
        ]
        // 壁の位置を配列に格納
        const walls = [] 
        //
        for (let i = 0; i < 2; i++) {
          // PIXI.Graphicsを配列にnewしていく
          walls.push(new PIXI.Graphics());
          walls[i].width = 50;
          walls[i].height = 50;
          walls[i].beginFill(0x00ff00);
          walls[i].drawRect(0,0,50,50);
          walls[i].endFill();
          console.log(i);
        }
        // 初期位置設定
        walls[0].x = wallPos[0];
        walls[0].y = wallPos[1];
        walls[1].x = wallPos[2];
        walls[1].y = wallPos[3];

        //---------------------------------
        // 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();

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

        // 【変更】壁をすべてステージに追加
        for (let i = 0; i < walls.length; i++) {
          app.stage.addChild(walls[i]);
        }

        // ループ処理の実装
        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 += allCollisitonCheck(10,0,square,walls); // 変更
              break;
            case 2:
              square.x += allCollisitonCheck(-10,0,square,walls); // 変更
              break;
            case 3:
              square.y += allCollisitonCheck(0,10,square,walls); // 変更
              break;
            case 4:
              square.y += allCollisitonCheck(0,-10,square,walls); // 変更
              break;
            default:
              break;
          }

        }


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

        // 矩形の右端座標を取得する関数 flag: true => x, false => y
        function getEndPos(sprite,flag=true) {
          if(flag) {
            // 大きさじゃなく座標なので-1している
            // 50x50の矩形の場合座標0~49にいるため
            return sprite.x + sprite.width-1;
          } else {
            return sprite.y + sprite.height-1;
          }
        }

        // 対象の大きさの範囲に居ないか調べる flag: true => x, false => y
        function spriteRangeCheck(player,target,flag=true) {
          if(flag) {
            // x座標のみをみてプレイヤーが対象にぶつかる範囲に居ないか調べる
            return player.x > getEndPos(target) || getEndPos(player) < target.x
          } else {
            // y座標のみをみてプレイヤーが対象にぶつかる範囲に居ないか調べる
            return player.y > getEndPos(target,false) || getEndPos(player,false) < target.y
          }
        }


        // 全ての衝突判定をチェック
        function allCollisitonCheck(x,y,player,targets) {
          // trueが返ってきたら問答無用で座標を変更
          for (let i = 0; i<targets.length; i++) {
            // 0以外になったらぶつかっている判定をされる
            switch( wallCollisiton(x,y,player,targets[i]) ) {
              case 1:
                return x - ( (getEndPos(player)+x) - ( targets[i].x - 1 ) );
                break;
              case 2:
                return x + ( (getEndPos(targets[i]) + 1) - (player.x + x) );
                break;
              case 3:
                return y - ( (getEndPos(player,flag=false)+y) - ( targets[i].y - 1 ) );
                break;
              case 4:
                return y + ( (getEndPos(targets[i],flag=false) + 1) - (player.y + y) );
                break;
            }
          }
          // 衝突判定されなかった場合はそのまま値を返す
          if(y === 0) {
            return x;
          } else {
            return y;
          }
        }

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

            } else {
              if(getEndPos(player,flag=false) < getEndPos(target,flag=false) && y > 0) {
                if ( getEndPos(player,flag=false) + y >= target.y  ) {
                  return 3
                } else {
                  return 0;
                }
              } else if(player.y > target.y && y < 0) {
                if ( player.y + y <= getEndPos(target,flag=false) ) {
                  return 4
                } else {
                  return 0;
                }
              } else {
                return 0;
              }
            }
          }
        }
      </script>
    </body>
  </html>
</html>