
このページでは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;
}
背景の描画
// --- 背景の描画 --- 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 の一般的な使い方
- 配列(リスト)の要素数
- 一番よく使われるのがこれ。
配列の要素の数。 - 例:
const numbers = [10, 20, 30];この場合、numbers.lengthは3。
- 一番よく使われるのがこれ。
- 文字列の文字数
- 文字列に何文字含まれているかを知りたいときに使用。
- 例:
const text = "Hello";この場合、text.lengthは5。

今回の場合は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.01は 1%(= 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()の実装は終わり!
全体のコードの実装もあと少しだから頑張ろう!
コメント