【1】アリクイあくしょんのコード解説〜はじめに

JavaScript
projectフォルダ/
├── img/
│   ├── ari.gif
│   ├── arikui-attack.gif
│   ├── arikui-stand.gif
│   ├── arikui-walk-1.gif
│   └── arikui-walk-2.gif
├── index.html
├── index.js
└── style.css
たかまる
たかまる

このゲームは、3つのファイル+画像でできているよ。

・index.html
・style.css
・index.js

index.jsのコードを見てみよう

たかまる
たかまる

index.htmlとstyle.cssについては省略するね。
希望が多ければ書くけど、知りたい人はWebサイト作りを勉強してみてね。

イカクくん
イカクくん

ふむ。
以下がindex.jsの中身か。

const canvas = document.getElementById("gameCanvas");
const ctx = canvas.getContext("2d");

// プレイヤー画像の読み込み
const playerImgs = [
  new Image(),
  new Image(),
  new Image()
];
playerImgs[0].src = "img/arikui-stand.gif";
playerImgs[1].src = "img/arikui-walk-1.gif";
playerImgs[2].src = "img/arikui-walk-2.gif";

const attackImg = new Image();
attackImg.src = "img/arikui-attack.gif";

const enemyImg = new Image();
enemyImg.src = "img/ari.gif";

let playerFrame = 0;
let playerAnimCount = 0;

// プレイヤー情報
let player = {
  x: 50,
  y: 250,
  width: 40,
  height: 40,
  velocityX: 0,
  velocityY: 0,
  isJumping: false,
};

const gravity = 0.5;
const jumpPower = -12;
let worldX = 0;
const screenMid = 200;

let keys = {};
let jumpKeyPressed = false;
let isAttacking = false;
let attackTimer = 0;
let canAttack = true;

// ステージデータ
const stages = [
  { // ステージ1
    blocks: [
      { x: 0, y: 500, width: 1000, height: 200 },
      { x: 1100, y: 500, width: 1000, height: 200 },
      { x: 250, y: 380, width: 100, height: 40 },
      { x: 400, y: 300, width: 100, height: 40 },
      { x: 500, y: 150, width: 50, height: 40 },
    ],
    hiddenBlocks: [
      { x: 450, y: 200, width: 40, height: 40, visible: false },
      { x: 890, y: 400, width: 40, height: 40, visible: false },
      { x: 990, y: 350, width: 40, height: 40, visible: false },
    ],
    enemies: [
      { x: 600, y: 460, width: 40, height: 40, vx: 2, stopTimer: 0 },
      { x: 1200, y: 460, width: 40, height: 40, vx: -2, stopTimer: 0 }
    ],
    goal: { x: 1800, y: 440, width: 40, height: 60 }
  },
  { // ステージ2
    blocks: [
      { x: 0, y: 500, width: 800, height: 200 },
      { x: 900, y: 300, width: 300, height: 40 },
      { x: 1300, y: 200, width: 100, height: 40 },
      { x: 1500, y: 120, width: 200, height: 40 },
      { x: 1800, y: 400, width: 400, height: 100 },
    ],
    hiddenBlocks: [
      { x: 750, y: 400, width: 40, height: 40, visible: false }
    ],
    enemies: [
      { x: 1000, y: 260, width: 40, height: 40, vx: 2, stopTimer: 0 }
    ],
    goal: { x: 2100, y: 340, width: 40, height: 60 }
  },
  {  // ステージ3
    blocks: [
      { x: 0, y: 500, width: 300, height: 200 },
      { x: 400, y: 400, width: 100, height: 40 },
      { x: 600, y: 350, width: 100, height: 40 },
      { x: 800, y: 300, width: 100, height: 40 },
      { x: 1000, y: 400, width: 100, height: 40 },
      { x: 1200, y: 500, width: 400, height: 100 },
    ],
    hiddenBlocks: [
      { x: 900, y: 170, width: 40, height: 40, visible: false }
    ],
    enemies: [
      { x: 800, y: 260, width: 40, height: 40, vx: 2, stopTimer: 0 }
    ],
    goal: { x: 1500, y: 440, width: 40, height: 60 }
  }
];

let currentStage = 0;
let blocks = [];
let hiddenBlocks = [];
let enemies = [];
let goal = {};

// ゲーム状態フラグ
let gameStarted = false;
let gameCleared = false;
let gameOver = false;

// ステージ切り替え
function loadStage(stageIndex) {
  const stage = stages[stageIndex];
  blocks.length = 0;
  blocks.push(...stage.blocks.map(b => ({ ...b })));
  hiddenBlocks.length = 0;
  hiddenBlocks.push(...stage.hiddenBlocks.map(b => ({ ...b })));
  enemies.length = 0;
  enemies.push(...stage.enemies.map(e => ({ ...e })));
  goal.x = stage.goal.x;
  goal.y = stage.goal.y;
  goal.width = stage.goal.width;
  goal.height = stage.goal.height;

  player.x = 50;
  if (stageIndex === 1) {
    player.y = 360;
  } else if (stageIndex === 2) {
    player.y = 460;
  } else {
    player.y = 250;
  }
  player.velocityX = 0;
  player.velocityY = 0;
  player.isJumping = false;
  worldX = 0;
}

// 最初のステージをロード
loadStage(currentStage);

// 入力イベント
window.addEventListener("keydown", (e) => {
  keys[e.code] = true;
  if ((e.code === "Space") && !player.isJumping && !jumpKeyPressed) {
    player.velocityY = jumpPower;
    player.isJumping = true;
    jumpKeyPressed = true;
  }
  if (e.code === "ArrowUp" && !isAttacking && canAttack) {
    isAttacking = true;
    canAttack = false;
    attackTimer = 10;
  }
});
window.addEventListener("keyup", (e) => {
  keys[e.code] = false;
  if (e.code === "ArrowUp") canAttack = true;
  if (e.code === "Space") jumpKeyPressed = false;
});

// 左
const leftBtn = document.getElementById("left");
leftBtn.addEventListener("touchstart", (e) => { e.preventDefault(); keys["ArrowLeft"] = true; });
leftBtn.addEventListener("touchend", (e) => { e.preventDefault(); keys["ArrowLeft"] = false; });
leftBtn.addEventListener("mousedown", () => keys["ArrowLeft"] = true);
leftBtn.addEventListener("mouseup", () => keys["ArrowLeft"] = false);
leftBtn.addEventListener("mouseleave", () => keys["ArrowLeft"] = false);

// 右
const rightBtn = document.getElementById("right");
rightBtn.addEventListener("touchstart", (e) => { e.preventDefault(); keys["ArrowRight"] = true; });
rightBtn.addEventListener("touchend", (e) => { e.preventDefault(); keys["ArrowRight"] = false; });
rightBtn.addEventListener("mousedown", () => keys["ArrowRight"] = true);
rightBtn.addEventListener("mouseup", () => keys["ArrowRight"] = false);
rightBtn.addEventListener("mouseleave", () => keys["ArrowRight"] = false);

// ジャンプ
const jumpBtn = document.getElementById("jump");
jumpBtn.addEventListener("touchstart", () => {
  if (!player.isJumping && !jumpKeyPressed) {
    player.velocityY = jumpPower;
    player.isJumping = true;
    jumpKeyPressed = true;
  }
  keys["Space"] = true;
});
jumpBtn.addEventListener("touchend", () => {
  keys["Space"] = false;
  jumpKeyPressed = false;
});
jumpBtn.addEventListener("mousedown", () => {
  if (!player.isJumping && !jumpKeyPressed) {
    player.velocityY = jumpPower;
    player.isJumping = true;
    jumpKeyPressed = true;
  }
  keys["Space"] = true;
});
jumpBtn.addEventListener("mouseup", () => {
  keys["Space"] = false;
  jumpKeyPressed = false;
});
jumpBtn.addEventListener("mouseleave", () => {
  keys["Space"] = false;
  jumpKeyPressed = false;
});

// 攻撃
const attackBtn = document.getElementById("attack");
attackBtn.addEventListener("touchstart", () => {
  if (!isAttacking && canAttack) {
    isAttacking = true;
    canAttack = false;
    attackTimer = 10;
  }
  keys["ArrowUp"] = true;
});
attackBtn.addEventListener("touchend", () => {
  keys["ArrowUp"] = false;
  canAttack = true;
});
attackBtn.addEventListener("mousedown", () => {
  if (!isAttacking && canAttack) {
    isAttacking = true;
    canAttack = false;
    attackTimer = 10;
  }
  keys["ArrowUp"] = true;
});
attackBtn.addEventListener("mouseup", () => {
  keys["ArrowUp"] = false;
  canAttack = true;
});
attackBtn.addEventListener("mouseleave", () => {
  keys["ArrowUp"] = false;
  canAttack = true;
});

// 描画関数
function drawBlocks() {
  ctx.fillStyle = "brown";
  blocks.forEach(block => {
    ctx.fillRect(block.x - worldX, block.y, block.width, block.height);
  });
}
function drawHiddenBlocks() {
  hiddenBlocks.forEach(block => {
    if (block.visible) {
      ctx.fillStyle = "orange";
      ctx.fillRect(block.x - worldX, block.y, block.width, block.height);
    }
  });
}
function drawGoal() {
  ctx.fillStyle = "gold";
  ctx.fillRect(goal.x - worldX, goal.y, goal.width, goal.height);
}
function drawPlayer() {
  let img;
  let drawWidth = player.width;
  if (isAttacking) {
    img = attackImg;
    drawWidth = 50;
  } else if (player.isJumping) {
    img = playerImgs[0];
    playerFrame = 0;
    playerAnimCount = 0;
  } else if (Math.abs(player.velocityX) > 1) {
    playerAnimCount++;
    if (playerAnimCount % 8 === 0) {
      playerFrame = (playerFrame + 1) % 3;
    }
    img = playerImgs[playerFrame];
  } else {
    img = playerImgs[0];
    playerFrame = 0;
    playerAnimCount = 0;
  }
  if (player.velocityX < -1) {
    ctx.save();
    ctx.translate(player.x + drawWidth / 2, player.y + player.height / 2);
    ctx.scale(-1, 1);
    ctx.drawImage(img, -drawWidth / 2, -player.height / 2, drawWidth, player.height);
    ctx.restore();
  } else {
    if (img.complete) {
      ctx.drawImage(img, player.x, player.y, drawWidth, player.height);
    } else {
      ctx.fillStyle = "red";
      ctx.fillRect(player.x, player.y, drawWidth, player.height);
    }
  }
}
function drawEnemies() {
  enemies.forEach(enemy => {
    if (enemy.vx && enemy.vx > 0) {
      ctx.save();
      ctx.translate(enemy.x - worldX + enemy.width / 2, enemy.y + enemy.height / 2);
      ctx.scale(-1, 1);
      ctx.drawImage(
        enemyImg,
        -enemy.width / 2,
        -enemy.height / 2,
        enemy.width,
        enemy.height
      );
      ctx.restore();
    } else {
      ctx.drawImage(
        enemyImg,
        enemy.x - worldX,
        enemy.y,
        enemy.width,
        enemy.height
      );
    }
  });
}

// スタート画面
function drawStartScreen() {
  ctx.fillStyle = "#222";
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  ctx.fillStyle = "#fff";
  ctx.font = "32px sans-serif";
  ctx.textAlign = "center";
  ctx.fillText("ありくいアクション", canvas.width / 2, canvas.height / 2 - 100);
  ctx.font = "16px sans-serif";
  ctx.fillText("←→ 移動  | スペース ジャンプ |  ↑ 攻撃", canvas.width / 2, canvas.height / 2 + 50);
  ctx.fillText("スペースキーまたは画面をタップでスタート", canvas.width / 2, canvas.height / 2 + 150);
  ctx.font = "12px sans-serif";
  ctx.fillText("© 2025 たかまる", canvas.width / 2, canvas.height / 2 + 250);
}

// ゲームオーバー画面
function drawGameOver() {
  ctx.fillStyle = "rgba(0,0,0,0.7)";
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  ctx.fillStyle = "#fff";
  ctx.font = "bold 48px sans-serif";
  ctx.textAlign = "center";
  ctx.textBaseline = "middle";
  ctx.fillText("ゲームオーバー", canvas.width / 2, canvas.height / 2 - 20);
  ctx.font = "24px sans-serif";
  ctx.fillText("スペースキーまたは画面をタップで再スタート", canvas.width / 2, canvas.height / 2 + 40);
}

function mainLoop() {
  if (gameStarted) return;
  drawStartScreen();
  requestAnimationFrame(mainLoop);
}
mainLoop();

window.addEventListener("keydown", (e) => {
  if (e.code === "Space") {
    startOrRestart();
  }
});

// 画面クリック・タップ対応
canvas.addEventListener("click", startOrRestart);
canvas.addEventListener("touchstart", (e) => {
  e.preventDefault();
  startOrRestart();
});

// スペースキー or 画面クリック/タップでスタート&リスタート
function startOrRestart() {
  if (!gameStarted) {
    gameStarted = true;
    gameOver = false;
    update();
  } else if (gameCleared || gameOver) {
    currentStage = 0;
    loadStage(currentStage);
    gameCleared = false;
    gameOver = false;
    gameStarted = true;
    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);
}

// タッチデバイス判定
function isTouchDevice() {
  return (
    "ontouchstart" in window ||
    navigator.maxTouchPoints > 0 ||
    navigator.userAgent.indexOf("iPhone") > -1 ||
    navigator.userAgent.indexOf("Android") > -1
  );
}

// タッチデバイスのときだけタッチ操作用ボタン表示
function updateTouchControlsVisibility() {
  const controls1 = document.getElementById("touch-controls");
  const controls2 = document.getElementById("touch-controls-2");
  if (isTouchDevice()) {
    if (controls1) controls1.style.display = "flex";
    if (controls2) controls2.style.display = "flex";
  } else {
    if (controls1) controls1.style.display = "none";
    if (controls2) controls2.style.display = "none";
  }
}
window.addEventListener("DOMContentLoaded", updateTouchControlsVisibility);
window.addEventListener("resize", updateTouchControlsVisibility);

// キャンバスのリサイズ
function resizeCanvas() {
  const aspect = 800 / 600; // アスペクト比
  let w = window.innerWidth;
  let h = window.innerHeight;

  if (w / h > aspect) {
    w = h * aspect;
  } else {
    h = w / aspect;
  }

  if (!isTouchDevice()) {
    // PCのみ最大サイズ制限
    w = Math.min(w, 800);
    h = Math.min(h, 600);
  }

  canvas.width = 800;   // 描画解像度は固定
  canvas.height = 600;
  canvas.style.width = w + "px";
  canvas.style.height = h + "px";
}
window.addEventListener("resize", resizeCanvas);
resizeCanvas();
たかまる
たかまる

長いけど、上から順番に解説するね。

イカクくん
イカクくん

わ〜い!
次のページへレッツゴー!

コメント

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