【12】アリクイあくしょんコード解説 386行目〜

JavaScript
たかまる
たかまる

このページではupdateの実装をしていくよ。

function update() {
  if (gameCleared) {
    ctx.fillStyle = "#222";
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    ctx.fillStyle = "#fff";
    ctx.font = "bold 36px sans-serif";
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";
    ctx.fillText("クリア!", canvas.width / 2, canvas.height / 2 - 20);
    ctx.font = "bold 24px sans-serif";
    ctx.fillText("おめでとう!", canvas.width / 2, canvas.height / 2 + 40);
    ctx.font = "16px sans-serif";
    ctx.fillText("スペースキーまたは画面をタップで最初からプレイ", canvas.width / 2, canvas.height / 2 + 80);
    ctx.font = "12px sans-serif";
    ctx.fillText("© 2025 たかまる", canvas.width / 2, canvas.height / 2 + 150);
    return;
  }

  if (gameOver) {
    drawGameOver();
    return;
  }

  ctx.fillStyle = "#87ceeb";
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  if (keys["ArrowRight"]) player.velocityX = 5;
  if (keys["ArrowLeft"]) player.velocityX = -5;
  player.velocityX *= 0.5;

  if (keys["ArrowRight"] && player.x >= screenMid) {
    worldX += player.velocityX;
    player.x = screenMid;
  } else if (keys["ArrowLeft"] && worldX > 0 && player.x <= screenMid) {
    worldX -= Math.abs(player.velocityX);
    player.x = screenMid;
    if (worldX < 0) worldX = 0;
  } else {
    player.x += player.velocityX;
  }

  if (player.x < 0) {
    player.x = 0;
    player.velocityX = 0;
  }

  player.velocityY += gravity;
  player.y += player.velocityY;

  if (player.y > canvas.height) {
    gameOver = true;
    drawGameOver();
    return;
  }

  blocks.forEach(block => {
    const blockLeft = block.x - worldX;
    const blockRight = block.x + block.width - worldX;
    const blockTop = block.y;
    const blockBottom = block.y + block.height;

    const playerLeft = player.x;
    const playerRight = player.x + player.width;
    const playerTop = player.y;
    const playerBottom = player.y + player.height;

    if (
      playerRight > blockLeft &&
      playerLeft < blockRight &&
      playerBottom > blockTop &&
      playerTop < blockBottom
    ) {
      if (
        player.velocityY > 0 &&
        playerBottom - player.velocityY <= blockTop
      ) {
        player.y = blockTop - player.height;
        player.velocityY = 0;
        player.isJumping = false;
      } else if (
        player.velocityY < 0 &&
        playerTop - player.velocityY >= blockBottom
      ) {
        player.y = blockBottom;
        player.velocityY = 0;
      } else if (player.velocityX > 0) {
        player.x = blockLeft - player.width;
        player.velocityX = 0;
      } else if (player.velocityX < 0) {
        player.x = blockRight;
        player.velocityX = 0;
      }
    }
  });

  hiddenBlocks.forEach(block => {
    if (!block.visible) return;
    const blockLeft = block.x - worldX;
    const blockRight = block.x + block.width - worldX;
    const blockTop = block.y;
    const blockBottom = block.y + block.height;

    const playerLeft = player.x;
    const playerRight = player.x + player.width;
    const playerTop = player.y;
    const playerBottom = player.y + player.height;

    if (
      playerRight > blockLeft &&
      playerLeft < blockRight &&
      playerBottom > blockTop &&
      playerTop < blockBottom
    ) {
      if (
        player.velocityY > 0 &&
        playerBottom - player.velocityY <= blockTop
      ) {
        player.y = blockTop - player.height;
        player.velocityY = 0;
        player.isJumping = false;
      } else if (
        player.velocityY < 0 &&
        playerTop - player.velocityY >= blockBottom
      ) {
        player.y = blockBottom;
        player.velocityY = 0;
      } else if (player.velocityX > 0) {
        player.x = blockLeft - player.width;
        player.velocityX = 0;
      } else if (player.velocityX < 0) {
        player.x = blockRight;
        player.velocityX = 0;
      }
    }
  });

  // ゴール判定
  if (
    player.x + player.width > goal.x - worldX &&
    player.x < goal.x + goal.width - worldX &&
    player.y + player.height > goal.y &&
    player.y < goal.y + goal.height
  ) {
    if (currentStage < stages.length - 1) {
      currentStage++;
      loadStage(currentStage);
      update();
      return;
    } else {
      gameCleared = true;
    }
  }

  hiddenBlocks.forEach(block => {
    const blockLeft = block.x - worldX;
    const blockRight = block.x + block.width - worldX;
    const blockTop = block.y;
    const blockBottom = block.y + block.height;

    const playerLeft = player.x;
    const playerRight = player.x + player.width;
    const playerTop = player.y;
    const playerBottom = player.y + player.height;

    if (
      playerRight > blockLeft &&
      playerLeft < blockRight &&
      playerTop < blockBottom &&
      playerBottom > blockTop &&
      player.velocityY < 0 &&
      playerTop - player.velocityY >= blockBottom
    ) {
      block.visible = true;
      player.y = blockBottom;
      player.velocityY = 0;
    }
  });

  if (isAttacking) {
    attackTimer--;
    if (attackTimer <= 0) {
      isAttacking = false;
    }
  }

  if (isAttacking) {
    enemies.forEach((enemy, i) => {
      const attackRange = 20;
      if (
        player.x + player.width + attackRange > enemy.x - worldX &&
        player.x - attackRange < enemy.x + enemy.width - worldX &&
        player.y + player.height > enemy.y &&
        player.y < enemy.y + enemy.height
      ) {
        enemies.splice(i, 1);
      }
    });
  }

  enemies.forEach((enemy, i) => {
    const enemyLeft = enemy.x - worldX;
    const enemyRight = enemy.x + enemy.width - worldX;
    const enemyTop = enemy.y;
    const enemyBottom = enemy.y + enemy.height;

    const playerLeft = player.x;
    const playerRight = player.x + player.width;
    const playerTop = player.y;
    const playerBottom = player.y + player.height;

    if (
      playerRight > enemyLeft &&
      playerLeft < enemyRight &&
      playerBottom > enemyTop &&
      playerTop < enemyBottom
    ) {
      if (
        player.velocityY > 0 &&
        playerBottom - player.velocityY <= enemyTop + 5
      ) {
        enemies.splice(i, 1);
        player.velocityY = jumpPower / 2;
      } else if (!isAttacking) {
        gameOver = true;
        return;
      }
    }
  });

  enemies.forEach(enemy => {
    if (enemy.stopTimer === 0 && Math.random() < 0.01) {
      enemy.stopTimer = Math.floor(Math.random() * 60) + 30;
    }
    if (enemy.stopTimer > 0) {
      enemy.stopTimer--;
      return;
    }
    if (Math.random() < 0.01) {
      enemy.vx *= -1;
    }
    const nextX = enemy.x + enemy.vx;
    const enemyBottom = enemy.y + enemy.height;
    let onBlock = false;
    blocks.forEach(block => {
      const blockLeft = block.x;
      const blockRight = block.x + block.width;
      const blockTop = block.y;
      if (
        nextX + enemy.width / 2 > blockLeft &&
        nextX + enemy.width / 2 < blockRight &&
        Math.abs(enemyBottom - blockTop) < 2
      ) {
        onBlock = true;
      }
    });
    if (!onBlock) {
      enemy.vx *= -1;
    } else {
      enemy.x += enemy.vx;
    }
    if (enemy.x < 0) {
      enemy.x = 0;
      enemy.vx *= -1;
    }
    if (enemy.x > 2000) {
      enemy.x = 2000;
      enemy.vx *= -1;
    }
  });

  drawBlocks();
  drawHiddenBlocks();
  drawGoal();
  drawEnemies();
  drawPlayer();

  requestAnimationFrame(update);
}
たかまる
たかまる

ゲームは「止まっている絵」ではなく、1秒間に何十回も「絵(フレーム)」を更新して動いている。
update() は、その1回の絵を描くための「動きや計算をする役割の関数」ってことだよ。

ゲームの状態チェック

// gameClearedが true の場合(クリアした時)
if (gameCleared) {
  ctx.fillStyle = "#222";  // 背景を濃いグレーで塗りつぶす
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  ctx.fillStyle = "#fff";  // 文字色を白に設定

  // 「クリア!」メッセージのフォントと位置設定
  ctx.font = "bold 36px sans-serif";
  ctx.textAlign = "center";
  ctx.textBaseline = "middle";
  ctx.fillText("クリア!", canvas.width / 2, canvas.height / 2 - 20);

  // 「おめでとう!」メッセージを少し小さめのフォントで表示
  ctx.font = "bold 24px sans-serif";
  ctx.fillText("おめでとう!", canvas.width / 2, canvas.height / 2 + 40);

  // スペースキーや画面タップで再スタートできる案内メッセージ
  ctx.font = "16px sans-serif";
  ctx.fillText("スペースキーまたは画面をタップで最初からプレイ", canvas.width / 2, canvas.height / 2 + 80);

  // 著作権表示を一番下に小さく表示
  ctx.font = "12px sans-serif";
  ctx.fillText("© 2025 たかまる", canvas.width / 2, canvas.height / 2 + 150);

  return; // ここで処理終了。以降のupdate処理はスキップ
}

// gameOverが true の場合(ゲームオーバーした時)
  if (gameOver) {
    drawGameOver(); // ゲームオーバー画面描画関数呼び出し
    return;
  }

もし return; がなければ、ゲームクリア画面を描いたあとに普通のゲーム更新処理も走ってしまい、プレイヤーが動いたり敵が動いたりしてしまい、画面表示やゲーム状態がおかしくなってしまう。

背景の描画

  // --- 背景の描画 ---
  ctx.fillStyle = "#87ceeb"; // 空色背景
  ctx.fillRect(0, 0, canvas.width, canvas.height);

プレイヤーの入力と移動処理

  // --- プレイヤーの入力と移動処理 ---
  if (keys["ArrowRight"]) player.velocityX = 5; // 右矢印で右方向へ加速
  if (keys["ArrowLeft"]) player.velocityX = -5; // 左矢印で左方向へ加速
  player.velocityX *= 0.5; // 摩擦のように速度を減衰
たかまる
たかまる

Xは横、Yは縦
がすぐに判断できるとイメージしやすいよ。

イカクくん
イカクくん

player.velocityX *= 0.5;
これ何?

*「かけ算(乗算)」 を表す演算子。

player.velocityX *= 0.5;

player.velocityX = player.velocityX * 0.5;
の、短縮形!

player.velocityX が 5 だとしたら

5 * 0.5 = 2.5 (半分になる)

プレイヤーとワールドのスクロール制御

  // --- プレイヤーとワールドのスクロール制御 ---
  if (keys["ArrowRight"] && player.x >= screenMid) {  // →キーが押されていてかつプレイヤーが画面中央より右なら
    worldX += player.velocityX; // カメラが右に動く(背景が左に流れて見える)
    player.x = screenMid; // プレイヤーは中央に固定
  } else if (keys["ArrowLeft"] && worldX > 0 && player.x <= screenMid) {  // ←キーが押されていてかつプレイヤーが画面中央より左なら
    worldX -= Math.abs(player.velocityX); // カメラが左に動く(背景が右に流れて見える)
    player.x = screenMid;
    if (worldX < 0) worldX = 0;  // カメラがステージの左端より左には進まないように制限
  } else {
    player.x += player.velocityX; // カメラが動かない場合はプレイヤー自身を動かす
  }

a >= b, a > b

たかまる
たかまる

a >= b
aがb以上ならtrue、それ以外はfalse。

a > b
aがbより大きいならtrue、それ以外はfalse。

イカクくん
イカクくん

以上のには「〜から」という意味があるってどっかの誰かが言ってた。
5 >= 5
この場合、5 は 5 上だからtrueだね。

たかまる
たかまる

5 > 5
これは、5が5より大きいならtrue、それ以外はfalse。
5よりも大きくない(等しい)からfalseだよ。

Math.abs()

イカクくん
イカクくん

Math.abs()
ってなんだっけ?

たかまる
たかまる

絶対値 を返すJavaScriptの組み込み関数だよ。

絶対値とは、数字の「符号(プラスかマイナスか)」を無視した大きさのこと。

  • 例:Math.abs(5)5
  • 例:Math.abs(-5)5

プレイヤーがカメラの左端より左に行かないようにする

  if (player.x < 0) { // もし、プレイヤーのX値が0より小さいなら
    player.x = 0; // X値を0にする
    player.velocityX = 0;
  }
たかまる
たかまる

先ほどカメラはステージの左端(0)より左(マイナスの領域)に行かないようにしたけど、このままだとプレイヤーは左端より左に行ってしまう。
なので、プレイヤーにも似たような実装をするよ。

イカクくん
イカクくん

ふむふむ。
これで0より小さくならないから左端より左に行かないってわけだ。

重力処理と縦方向の位置更新

  // --- 重力処理と縦方向の位置更新 ---
  player.velocityY += gravity; // 重力の影響で速度が増える
  player.y += player.velocityY; // その速度に応じて位置が変わる(下方向に移動)

画面下に落ちたらゲームオーバー

  if (player.y > canvas.height) { // プレイヤーのY値のほうがキャンバスの高さより大きい場合(下方向)
    gameOver = true;
    drawGameOver();
    return;
  }
たかまる
たかまる

高さっていうと、
数字が大きくなるにつれ上に向かうイメージがするけど、
画面の左上が0だから、上から下に増えていくよ。

ブロックとの当たり判定

  // --- 通常のブロックとの当たり判定 ---
  blocks.forEach(block => {
    // ブロックの画面上の座標計算(スクロールを考慮)
    const blockLeft = block.x - worldX;
    const blockRight = block.x + block.width - worldX;
    const blockTop = block.y;
    const blockBottom = block.y + block.height;

    // プレイヤーの座標
    const playerLeft = player.x;
    const playerRight = player.x + player.width;
    const playerTop = player.y;
    const playerBottom = player.y + player.height;

    // 当たり判定(AABB判定)
    if (
      playerRight > blockLeft &&
      playerLeft < blockRight &&
      playerBottom > blockTop &&
      playerTop < blockBottom
    ) {
      // プレイヤーがブロックの上に落ちてきた場合
      if (
        player.velocityY > 0 &&
        playerBottom - player.velocityY <= blockTop
      ) {
        player.y = blockTop - player.height; // プレイヤーをブロックの上に乗せる
        player.velocityY = 0; // 縦速度リセット
        player.isJumping = false; // ジャンプ状態解除
      }
      // プレイヤーがブロックの下からぶつかった場合
      else if (
        player.velocityY < 0 &&
        playerTop - player.velocityY >= blockBottom
      ) {
        player.y = blockBottom; // プレイヤーをブロックの下に押し出す
        player.velocityY = 0;
      }
      // プレイヤーがブロックの左側にぶつかった場合
      else if (player.velocityX > 0) {
        player.x = blockLeft - player.width;
        player.velocityX = 0;
      }
      // プレイヤーがブロックの右側にぶつかった場合
      else if (player.velocityX < 0) {
        player.x = blockRight;
        player.velocityX = 0;
      }
    }
  });

  // --- 隠しブロックとの当たり判定 ---
  hiddenBlocks.forEach(block => {
    if (!block.visible) return; // 見えていなければ判定しない

    const blockLeft = block.x - worldX;
    const blockRight = block.x + block.width - worldX;
    const blockTop = block.y;
    const blockBottom = block.y + block.height;

    const playerLeft = player.x;
    const playerRight = player.x + player.width;
    const playerTop = player.y;
    const playerBottom = player.y + player.height;

    if (
      playerRight > blockLeft &&
      playerLeft < blockRight &&
      playerBottom > blockTop &&
      playerTop < blockBottom
    ) {
      if (
        player.velocityY > 0 &&
        playerBottom - player.velocityY <= blockTop
      ) {
        player.y = blockTop - player.height;
        player.velocityY = 0;
        player.isJumping = false;
      } else if (
        player.velocityY < 0 &&
        playerTop - player.velocityY >= blockBottom
      ) {
        player.y = blockBottom;
        player.velocityY = 0;
      } else if (player.velocityX > 0) {
        player.x = blockLeft - player.width;
        player.velocityX = 0;
      } else if (player.velocityX < 0) {
        player.x = blockRight;
        player.velocityX = 0;
      }
    }
  });

forEach

イカクくん
イカクくん

forEachってなんだっけ?

たかまる
たかまる

forEach は、JavaScriptの配列(Array)に使われるメソッドで、配列の各要素に対して関数を実行するよ!

array.forEach((element, index, array) => {
  // 処理
});
  • element:配列の要素1つ1つ。arrayの中身。
  • index省略可):その要素のインデックス番号(0から始まる)
  • array省略可):元の配列そのもの
  blocks.forEach(block => {
イカクくん
イカクくん

blocks 配列のすべての要素に対して、この中の処理を実行してね」という意味ってことかぁ。

当たり判定(AABB判定)

    // 当たり判定(AABB判定)
    if (
      playerRight > blockLeft &&      // プレイヤーの右端がブロックの左端より右にある
      playerLeft < blockRight &&      // プレイヤーの左端がブロックの右端より左にある
      playerBottom > blockTop &&      // プレイヤーの下端がブロックの上端より下にある
      playerTop < blockBottom         // プレイヤーの上端がブロックの下端より上にある
    )

AABB判定(Axis-Aligned Bounding Box判定)とは
「軸にそろった矩形同士が重なっているかどうかを調べる方法」のこと。

どういうことか?

「判定」とは、その四角形同士が 重なっているかどうか(衝突しているか) をチェックすること。

「軸にそろった」とは、四角形(箱=Bounding Box)が 横と縦の軸に対してまっすぐ(回転なし) 置かれていることを意味する。
→ 斜めに回転していない普通の長方形・四角形

具体的な判定方法

2つの長方形(箱)があったとき、

  • 横軸で重なっているか?
    → 片方の右端がもう片方の左端より右にあるか
    → 片方の左端がもう片方の右端より左にあるか
  • 縦軸で重なっているか?
    → 同様に上下で判定

簡単に言うと:

「長方形Aの範囲」と「長方形Bの範囲」が

横も縦もかぶっていれば衝突している(重なっている)

if ( 条件 ) { 命令 ( 処理 ) }

      // (先ほどの当たり判定の場合)プレイヤーがブロックの上に落ちてきたか確認する
      if (
        player.velocityY > 0 && // プレイヤーが下降中かつ
        playerBottom - player.velocityY <= blockTop // 前のフレームではブロックより上だったか
      ) {
        player.y = blockTop - player.height; // プレイヤーをブロックの上に乗せる
        player.velocityY = 0; // 縦速度リセット
        player.isJumping = false; // ジャンプ状態解除
      }

playerBottom – player.velocityY <= blockTop

現在のプレイヤーの足元の位置から落下速度を引く=1つ前のフレームでどの位置だったかがわかる
その結果、ブロックの上面以上なら{ }の中身を実行。
(上からブロックの上面に当たったと判明するから、プレイヤーをブロックの上に立たせたい)

上からではない場合

      // プレイヤーがブロックの下からぶつかった場合
      else if (
        player.velocityY < 0 &&
        playerTop - player.velocityY >= blockBottom
      ) {
        player.y = blockBottom; // プレイヤーの縦位置をブロックの下にする
        player.velocityY = 0;
      }
      // プレイヤーがブロックの左側にぶつかった場合
      else if (player.velocityX > 0) {
        player.x = blockLeft - player.width;  //プレイヤーの横位置をブロックの左端にする
        player.velocityX = 0;
      }
      // プレイヤーがブロックの右側にぶつかった場合
      else if (player.velocityX < 0) {
        player.x = blockRight;  //プレイヤーの横位置をブロックの右端にする
        player.velocityX = 0;
      }
たかまる
たかまる

隠しブロックも同じような実装だから解説は省くね。

ゴール判定

  // ゴール判定
  if (
    player.x + player.width > goal.x - worldX &&  //  プレイヤーの右端のほうがゴールの左端より右、かつ
    player.x < goal.x + goal.width - worldX &&  //  プレイヤーの左端よりもゴールの右端のほうが右、かつ
    player.y + player.height > goal.y &&  //  プレイヤーの下端のほうがゴールの上端より下、かつ
    player.y < goal.y + goal.height  //  プレイヤーの上端よりもゴールの下端のほうが下
  ) {
    if (currentStage < stages.length - 1) {
      currentStage++;
      loadStage(currentStage);
      update();
      return;
    } else {
      gameCleared = true;
    }
  }

player.x + player.width

プレイヤーの左が基準なので、そこにプレイヤーの幅を足す=プレイヤーの右端のx座標

goal.x – worldX

ゴールの左端から、現在のカメラの左端を引く=カメラから見たゴールの位置がわかる。

例えばゴールのx座標が100の場合、
カメラがワールド座標0を写している時、ゴールは画面上の100の位置に見える。
そしてカメラがワールド座標50を映す位置に移動した場合(worldX = 50)、
ゴールのx座標 100 は、カメラから見ると 100 – 50 の位置に見える。
つまり、画面上では 50 の位置に表示されることになる。

if (currentStage < stages.length – 1) {

イカクくん
イカクくん

currentStage
現在のステージだったよね!

length(組み込みプロパティ)

プロパティ:オブジェクトが持っている「情報」や「状態」を表すもの。

メソッド(関数):オブジェクトに対して実行できる「動作」を表すもの。

length の一般的な使い方
  1. 配列(リスト)の要素数
    • 一番よく使われるのがこれ。
      配列の要素の数。
    • 例:const numbers = [10, 20, 30]; この場合、numbers.length3
  2. 文字列の文字数
    • 文字列に何文字含まれているかを知りたいときに使用。
    • 例:const text = "Hello"; この場合、text.length5
たかまる
たかまる

今回の場合は1番だね。

イカクくん
イカクくん

stagesの要素数ってことは
全部で3ステージあるから
3かな?

たかまる
たかまる

正解!
そこから1引いた値(つまり2)が現在のステージより大きいなら実行。

イカクくん
イカクくん

ってことは、
0ステージと
1ステージの2つだね。

次のステージへ移行させる

      currentStage++; // 1増やす
      loadStage(currentStage);
      update();
      return;

currentStageが2だった場合(ラストステージ)

else {
      gameCleared = true;
    }

プレイヤーが隠しブロックを下から叩いたとき、ブロックを出現させる

  hiddenBlocks.forEach(block => {
    const blockLeft = block.x - worldX;
    const blockRight = block.x + block.width - worldX;
    const blockTop = block.y;
    const blockBottom = block.y + block.height;

    const playerLeft = player.x;
    const playerRight = player.x + player.width;
    const playerTop = player.y;
    const playerBottom = player.y + player.height;

    if (
      playerRight > blockLeft &&
      playerLeft < blockRight &&
      playerTop < blockBottom &&
      playerBottom > blockTop &&
      player.velocityY < 0 && // プレイヤーが上方向に移動している(ジャンプ中)
      playerTop - player.velocityY >= blockBottom  // 前のフレームではプレイヤーがブロックの下にいた
    ) {
      block.visible = true;  // 衝突したブロックを表示状態にする
      player.y = blockBottom;
      player.velocityY = 0;  // 上方向の速度をゼロにする(ジャンプを止める)
    }
  });

攻撃中なら攻撃タイマーを減らす

// 攻撃中なら攻撃タイマーを減らす
if (isAttacking) {
  attackTimer--; // 毎フレーム1ずつ減らす
  if (attackTimer <= 0) {
    isAttacking = false; // タイマーが0になったら攻撃終了
  }
}

attackTimer は攻撃の持続時間を制御している(時間経過で攻撃終了)。

プレイヤーが攻撃中であれば、敵との当たり判定を行う

// プレイヤーが攻撃中であれば、敵との当たり判定を行う
if (isAttacking) {
  enemies.forEach((enemy, i) => {
    const attackRange = 20;   // 攻撃範囲(左右に20ピクセル分広げて判定)

    // プレイヤーの攻撃範囲が敵の範囲と重なっているかをチェック
    if (
      player.x + player.width + attackRange > enemy.x - worldX &&        // プレイヤーの右端 + 攻撃範囲が、敵の左端に届いているか
      player.x - attackRange < enemy.x + enemy.width - worldX &&         // プレイヤーの左端 - 攻撃範囲が、敵の右端に届いているか
      player.y + player.height > enemy.y &&                              // プレイヤーの下端が、敵の上端より下にあるか
      player.y < enemy.y + enemy.height                                  // プレイヤーの上端が、敵の下端より上にあるか
    ) {
      enemies.splice(i, 1); // 敵を配列から削除して「倒す」
    }
  });
}

splice() 組み込み関数

JavaScript の配列メソッド splice() は、配列から要素を「取り除いたり」「追加したり」できる万能なメソッド

array.splice(開始位置, 削除する個数, [追加する要素…]);

enemies.splice(i, 1);

i: 配列の中で何番目の要素を対象にするか(インデックス)
1: そこから1個だけ削除する

追加要素はないので、純粋に削除だけ行っている。

プレイヤーが敵にぶつかったときの処理

// 敵それぞれに対してプレイヤーとの当たり判定を行う
enemies.forEach((enemy, i) => {
  // 敵の当たり判定用の座標を計算(スクロール補正)
  const enemyLeft = enemy.x - worldX;
  const enemyRight = enemy.x + enemy.width - worldX;
  const enemyTop = enemy.y;
  const enemyBottom = enemy.y + enemy.height;

  // プレイヤーの当たり判定用の座標を計算
  const playerLeft = player.x;
  const playerRight = player.x + player.width;
  const playerTop = player.y;
  const playerBottom = player.y + player.height;

  // プレイヤーと敵の当たり判定(矩形の重なり)をチェック
  if (
    playerRight > enemyLeft &&      // プレイヤー右端が敵左端より右にある
    playerLeft < enemyRight &&      // プレイヤー左端が敵右端より左にある
    playerBottom > enemyTop &&      // プレイヤー下端が敵上端より下にある
    playerTop < enemyBottom         // プレイヤー上端が敵下端より上にある
  ) {
    // プレイヤーが上から踏みつけた場合
    if (
      player.velocityY > 0 &&                                    // プレイヤーが下向きに落下していて
      playerBottom - player.velocityY <= enemyTop + 5            // 直前の位置が敵の頭より少し上(=踏んだと判断)
    ) {
      enemies.splice(i, 1);             // 敵を倒す(配列から削除)
      player.velocityY = jumpPower / 2; // プレイヤーが少し跳ね返る
    } else if (!isAttacking) {
      // 攻撃していない状態で、横や下からぶつかった場合はゲームオーバー
      gameOver = true;
      return; // 処理を中断
    }
  }
});

敵の動きの処理

// すべての敵に対して、移動や停止などのAI処理を行う
enemies.forEach(enemy => {
  // 敵が現在うごいていて、1%の確率で「止まる」ように指示する
  if (enemy.stopTimer === 0 && Math.random() < 0.01) {
    // 30~89フレームの間、停止する
    enemy.stopTimer = Math.floor(Math.random() * 60) + 30;
  }

  // stopTimerが0より大きい場合は敵が動かないようにする
  if (enemy.stopTimer > 0) {
    enemy.stopTimer--; // カウントダウン(1フレームごとに減らす)
    return; // このフレームは動かない
  }

  // 1%の確率で向きをランダムに変える
  if (Math.random() < 0.01) {
    enemy.vx *= -1;
  }

  // 次に移動する位置を計算
  const nextX = enemy.x + enemy.vx;
  const enemyBottom = enemy.y + enemy.height;

  // 足元にブロックがあるかどうかチェック
  let onBlock = false;
  blocks.forEach(block => {
    const blockLeft = block.x;
    const blockRight = block.x + block.width;
    const blockTop = block.y;

    // 敵キャラがブロックの上に乗っているかを確認、乗っているなら onBlock = true にする
    // nextX は「敵が次に移動する予定のX座標」
    if (
      nextX + enemy.width / 2 > blockLeft &&
      nextX + enemy.width / 2 < blockRight &&
      Math.abs(enemyBottom - blockTop) < 2
    ) {
      onBlock = true;
    }
  });

  // 足元のブロックが途切れていたら、方向を変える(ブロックから落ちないように)
  if (!onBlock) {
    enemy.vx *= -1;  //反転処理
  } else {
    enemy.x += enemy.vx;  // vxの値に応じて左右に進む
  }

  // 左端の壁にぶつかったら位置を補正して方向転換
  if (enemy.x < 0) {
    enemy.x = 0;
    enemy.vx *= -1;
  }

  // 右端の壁(2000px)にぶつかっても同様に方向転換
  if (enemy.x > 2000) {
    enemy.x = 2000;
    enemy.vx *= -1;
  }
});

この敵の動きの特徴

  • たまに止まる(stopTimer)
  • たまにランダムで向きを変える
  • 足元にブロックがなければ進まずに引き返す(ブロックから落ちないように)
  • 画面端(0〜2000px)で壁に当たったら反転

enemy.stopTimer === 0

動いている状態を意味する。

stopTimer停止時間を数値で表しているため、0なら動いているということになる。

stopTimer の値状態
0動いている
1以上止まっている

Math.random() 組み込み関数

0以上1未満のランダムな小数(0.0~0.999…)を返す。

💡 Math.random() < 0.01 とは?

ランダムな小数が 0.01 より小さいとき true
それ以外は false

確率に変換すると:

  • 0.011%(= 1/100)
  • つまり 1%の確率で true になる

Math.floor() 組み込み関数

floor は「小数点以下を切り捨てて整数にする」関数。

Math.random() * 60) + 30;

  • 小数 × 60 になるので、結果は:
    • 0.0 ~ 59.999...小数

floor なので小数点以下を切り捨てると、0〜59

最後に+ 30なので、30~89 の整数を表している。

enemy.vx *= -1;

マイナス1をかけると正→負、負→正となる(反転)。

例:
2 ✖️ -1 = -2
-2 ✖️ -1 = 2

各関数を呼び出す

drawBlocks();  // 通常のブロックを描画
drawHiddenBlocks();  // 隠しブロックを描画
drawGoal();  // ゴールを描画
drawEnemies();  // 敵キャラクターを描画
drawPlayer();  // プレイヤーキャラクターを描画

requestAnimationFrame(update);  // 次のフレームで update() を呼び出す(つまりループする)
たかまる
たかまる

これでupdate()の実装は終わり!
全体のコードの実装もあと少しだから頑張ろう!

コメント

タイトルとURLをコピーしました