HTML・CSS・JSでシューティングゲームを自作しよう!全コード完全解説

「プログラミングでゲームを作ってみたい!」 そう思ったとき、最初に挑戦するジャンルとしておすすめなのがシューティングゲームです。

本記事では、これまで数回に分けて解説していたシューティングゲームの作成手順を1つの記事に統合・完全リニューアルしました。 特別なゲームエンジン(Unityなど)を使わず、Web標準の技術(HTML / CSS / JavaScript)だけで動作するため、ブラウザさえあれば誰でもすぐに開発を始められます。

現役エンジニアの視点から、単なるコードのコピペではなく「なぜこの処理が必要なのか?」という仕組みの部分まで丁寧に解説します。

目次

1. 開発環境と全体の構成

今回作成するゲームの構成は非常にシンプルです。

  • HTML: ゲーム画面の土台を作ります。
  • CSS: 自機や敵、弾のデザイン(見た目)を決めます。
  • JavaScript: 「動く」「当たる」「スコアが増える」といったゲームのロジックを担当します。

フォルダ構成

PCの適当な場所にフォルダを作り、以下のファイルを作成してください。

  • index.html
  • style.css
  • script.js

完成映像

2. HTML:ゲーム画面の構築

まずはゲームの表示領域(キャンバス)を作成します。

HTML

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>シンプルシューティングゲーム</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="game-container">
        <h1>Shooting Game</h1>
        <p>スコア: <span id="score">0</span></p>
        <canvas id="gameCanvas" width="400" height="600"></canvas>
        <p>操作方法: ←→キーで移動、スペースキーで発射</p>
    </div>
    <script src="script.js"></script>
</body>
</html>

解説:

  • <canvas> タグは、JavaScriptを使って図形や画像を描画するための専用エリアです。今回はここにゲームの世界を描き出します。

3. CSS:デザインの適用

ゲーム画面が見やすくなるよう、スタイルを調整します。

CSS style.css

body {
    background-color: #222;
    color: #fff;
    text-align: center;
    font-family: 'Arial', sans-serif;
}

.game-container {
    margin-top: 20px;
}

canvas {
    background-color: #000; /* 宇宙空間をイメージした黒背景 */
    border: 2px solid #444;
    box-shadow: 0 0 20px rgba(0, 255, 255, 0.2);
    display: block;
    margin: 0 auto;
}

4. JavaScript:ゲームロジックの実装

ここが開発のメインパートです。以下の機能を実装します。

  1. 自機の描画と移動
  2. 弾の発射処理
  3. 敵の出現と移動
  4. 当たり判定(衝突検知)

少し長いですが、コピペして動作を確認してみてください。

JavaScript script.js

// script.js

// キャンバスの取得
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');

// ゲームの状態変数
let score = 0;
let isGameOver = false;

// 自機の設定
const player = {
    x: canvas.width / 2 - 15,
    y: canvas.height - 50,
    width: 30,
    height: 30,
    color: '#00f0ff',
    speed: 5
};

// 弾と敵の管理配列
let bullets = [];
let enemies = [];

// キー入力の監視
let keys = {};
document.addEventListener('keydown', (e) => {
    // ゲームオーバー時にスペースキーでリスタート
    if (isGameOver && e.code === 'Space') {
        resetGame();
        return;
    }
    keys[e.code] = true;
});
document.addEventListener('keyup', (e) => keys[e.code] = false);

// ■ ゲームのリセット(再スタート)処理
function resetGame() {
    score = 0;
    isGameOver = false;
    bullets = [];
    enemies = [];
    player.x = canvas.width / 2 - 15;
    player.y = canvas.height - 50;
    
    // スコア表示もリセット
    document.getElementById('score').innerText = score;
    
    // ゲームループ再開
    gameLoop();
}

// ■ 1. 自機の描画
function drawPlayer() {
    ctx.fillStyle = player.color;
    ctx.fillRect(player.x, player.y, player.width, player.height);
}

// ■ 2. 自機の移動
function updatePlayer() {
    if (isGameOver) return; // ゲームオーバー中は動かせない

    if (keys['ArrowLeft'] && player.x > 0) {
        player.x -= player.speed;
    }
    if (keys['ArrowRight'] && player.x < canvas.width - player.width) {
        player.x += player.speed;
    }
    // スペースキーで弾発射
    if (keys['Space']) {
        shootBullet();
        keys['Space'] = false; // 連射防止
    }
}

// ■ 3. 弾の生成と移動
function shootBullet() {
    bullets.push({
        x: player.x + player.width / 2 - 2.5,
        y: player.y,
        width: 5,
        height: 10,
        color: '#ff0',
        speed: 7
    });
}

function updateBullets() {
    bullets.forEach((bullet, index) => {
        bullet.y -= bullet.speed;
        ctx.fillStyle = bullet.color;
        ctx.fillRect(bullet.x, bullet.y, bullet.width, bullet.height);

        if (bullet.y < 0) bullets.splice(index, 1);
    });
}

// ■ 4. 敵の生成と移動
function spawnEnemy() {
    if (Math.random() < 0.02) { 
        enemies.push({
            x: Math.random() * (canvas.width - 30),
            y: 0,
            width: 30,
            height: 30,
            color: '#ff0055',
            speed: 2 + Math.random() * 2
        });
    }
}

function updateEnemies() {
    enemies.forEach((enemy, eIndex) => {
        enemy.y += enemy.speed;
        ctx.fillStyle = enemy.color;
        ctx.fillRect(enemy.x, enemy.y, enemy.width, enemy.height);

        if (enemy.y > canvas.height) {
            isGameOver = true;
        }

        // 当たり判定
        bullets.forEach((bullet, bIndex) => {
            if (
                bullet.x < enemy.x + enemy.width &&
                bullet.x + bullet.width > enemy.x &&
                bullet.y < enemy.y + enemy.height &&
                bullet.y + bullet.height > enemy.y
            ) {
                enemies.splice(eIndex, 1);
                bullets.splice(bIndex, 1);
                score += 100;
                document.getElementById('score').innerText = score;
            }
        });
    });
}

// ■ メインループ
function gameLoop() {
    if (isGameOver) {
        // ゲームオーバー画面の描画
        ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        
        ctx.fillStyle = '#fff';
        ctx.font = '30px Arial';
        ctx.textAlign = 'center'; // 文字を中央揃えに
        ctx.fillText("GAME OVER", canvas.width / 2, canvas.height / 2);
        
        ctx.font = '16px Arial';
        ctx.fillText("Press Space to Restart", canvas.width / 2, canvas.height / 2 + 40);
        return; // ここでループを止める
    }

    ctx.clearRect(0, 0, canvas.width, canvas.height);

    updatePlayer();
    drawPlayer();
    updateBullets();
    spawnEnemy();
    updateEnemies();

    requestAnimationFrame(gameLoop);
}

// ゲーム開始
gameLoop();

5. 技術的なポイント解説

ただ動かすだけでなく、仕組みを理解することが重要です。

① なぜ requestAnimationFrame を使うのか?

ゲームは「パラパラ漫画」と同じ仕組みで動いています。 かつては setInterval というタイマー機能が使われていましたが、これはブラウザの描画タイミングとズレが生じやすく、動きがカクつく原因でした。

今回使用した requestAnimationFrame は、「ブラウザが画面を更新する準備ができたタイミング(通常は1秒間に60回)」に合わせて関数を呼び出してくれます。これにより、PCのスペックに合わせた滑らかなアニメーション(60FPS)を実現しています。

② 配列(Array)によるオブジェクト管理

弾や敵キャラクターは、画面上に「複数」存在します。これらを管理するために、bulletsenemies という配列を使用しています。

  • 生成時: push() メソッドで新しい弾や敵を配列に追加します。
  • 削除時: splice() メソッドで配列から取り除きます。

★エンジニアの視点: 特に重要なのが「削除処理」です。画面外に出た弾や敵を削除せず配列に残し続けると、メモリ使用量が無限に増え続け、最終的にブラウザがフリーズします。これを防ぐために、if (bullet.y < 0) のような条件でこまめにデータを消去(ガベージコレクションへの配慮)を行っています。

③ 入力遅延を防ぐキー操作ロジック

初心者向けのコードでは、keydown イベントの中で直接 player.x += 5 のように移動処理を書くことが多いです。しかし、これだと「キーを押しっぱなしにしたとき」にOSの設定によって一瞬動きが止まる現象が起きます。

これを防ぐため、本コードでは以下の2段階構想を採用しています。

  1. イベントリスナー: 「今、キーが押されているか?」という**状態(フラグ)**だけを keys オブジェクトに記録する。
  2. ゲームループ: ループの中で keys の状態を見て、押されていれば移動させる。

これにより、キーボード連打や長押しでも、常に滑らかな移動が可能になります。

まとめ

今回はHTML・CSS・JavaScriptを使って、基礎的なシューティングゲームを作成しました。 このコードをベースに、以下のような改造を加えてみるのも勉強になります。

  • 画像の差し替え: 四角形ではなく、フリー素材の宇宙船画像を表示してみる
  • 音の追加: <audio> タグを使って、発射音や爆発音を鳴らす
  • スマホ対応: タッチ操作で動けるようにイベントリスナーを追加する

プログラミングは「作って動かす」のが一番の学習です。ぜひ自分だけのオリジナルゲームに進化させてみてください!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

神奈川県出身
どこにでもいる現役ゲーム会社サーバーエンジニア。大学ではひたすらゲーム!ゲーム!という人生を送ってました。

このブログは「ゲームを作ってみたい!」「プログラミングに関する知識をつけたい!」そんな皆さんの少しでもお役になれば嬉しいなと思い開設しました。

趣味
YouTube鑑賞、ストリートな格闘ゲーム、キャンプ

最近の出来事
・痔主になってしまいました....
・Google Cloud Professional Cloud Architect取得しました。

コメント

コメントする

目次