0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【完全解説】JavaScriptで作る横スクロールシューティングゲーム入門 - グラディウス風ゲームを一から実装しよう!🚀

Posted at

JavaScriptで作る横スクロールシューティングゲーム入門 🚀

こんにちは、@YushiYamamotoです!今回は、ブラウザで動作する横スクロールシューティングゲーム(グラディウス風)の開発方法について解説します。HTML5のCanvas要素とJavaScriptを使って、懐かしの横スクロールシューティングゲームを一から作り上げていきましょう!

See the Pen シューティングゲーム by Yushi Yamamoto (@yamamotoyushi) on CodePen.

プログラミング初心者の方でも理解できるように、基本的な概念から順に説明していきますが、ゲーム開発の醍醐味である技術的な部分もしっかり掘り下げていきます。それでは早速始めましょう!

🎮 完成イメージと基本設計

まずは今回作成するゲームの完成イメージを確認しておきましょう。

  • 宇宙を舞台にした横スクロールシューティングゲーム
  • プレイヤーは宇宙船を操作して敵を倒す
  • 敵の撃破でスコアを獲得
  • パワーアップアイテムで武器強化
  • ボス戦あり

ゲーム開発では、最初に全体設計をしっかり行うことが重要です。今回のゲームは以下のような構成要素に分けて実装していきます:

🛠️ 開発環境の準備

まずは開発に必要なファイルを用意しましょう。今回はシンプルに以下の3つのファイルを作成します:

  1. index.html - ゲームのHTML構造
  2. style.css - ゲームのスタイル
  3. game.js - ゲームのロジック

任意のフォルダを作成し、その中にこれらのファイルを作成してください。

📝 基本的なHTML構造

まずはHTMLファイルを作成します。ゲームのキャンバスとUIを配置します。

    <div id="game-container">
        <canvas id="game-canvas" width="800" height="600"></canvas>
        <div id="ui-layer">
            <div id="start-screen">
              <small>powered by らくらくサイト</small>
              <h1>シューティングゲーム</h1>
                <p>矢印キー: 移動 / スペース: 発射</p>
                <button id="start-button">ゲームスタート</button>
            </div>
            <div id="game-over">
                <h2>ゲームオーバー</h2>
                <p>スコア: <span id="final-score">0</span></p>
                <button id="retry-button">リトライ</button>
            </div>
            <div id="game-clear">
                <h2>ステージクリア!</h2>
                <p>スコア: <span id="clear-score">0</span></p>
                <button id="next-button">次のステージへ</button>
            </div>
        </div>
    </div>

🎨 CSSでゲームの見た目を整える

次に、CSSファイルでゲームの見た目を整えます。

body {
    margin: 0;
    padding: 0;
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
    background-color: #111;
    font-family: 'Arial', sans-serif;
}

#game-container {
    position: relative;
    width: 800px;
    height: 600px;
}

canvas {
    border: 2px solid #444;
    box-shadow: 0 0 10px rgba(0, 100, 255, 0.5);
}

#ui-layer {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    pointer-events: none;
}

#game-over, #game-clear {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    background-color: rgba(0, 0, 0, 0.8);
    color: white;
    padding: 20px;
    border-radius: 10px;
    text-align: center;
    display: none;
}

button {
    background-color: #4CAF50;
    border: none;
    color: white;
    padding: 10px 20px;
    text-align: center;
    text-decoration: none;
    display: inline-block;
    font-size: 16px;
    margin: 10px 2px;
    cursor: pointer;
    border-radius: 5px;
    pointer-events: auto;
}

#start-screen {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-color: rgba(0, 0, 0, 0.8);
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    color: white;
}

#start-screen h1 {
    font-size: 36px;
    margin-bottom: 20px;
    color: #4CAF50;
}

🧩 JavaScriptでゲームロジックを実装

それでは、ゲームの核となるJavaScriptコードを実装していきましょう。まずは基本的なゲーム構造から始めます。

ゲームの状態管理

ゲームの状態を管理するオブジェクトを作成します。

// ゲームの状態管理
const gameState = {
    isRunning: false,
    score: 0,
    lives: 3,
    level: 1,
    powerLevel: 0,
    gameOver: false,
    gameClear: false
};

// キャンバスとコンテキストの設定
const canvas = document.getElementById('game-canvas');
const ctx = canvas.getContext('2d');
const WIDTH = canvas.width;
const HEIGHT = canvas.height;

// UI要素
const startScreen = document.getElementById('start-screen');
const gameOverScreen = document.getElementById('game-over');
const gameClearScreen = document.getElementById('game-clear');
const startButton = document.getElementById('start-button');
const retryButton = document.getElementById('retry-button');
const nextButton = document.getElementById('next-button');
const finalScoreElement = document.getElementById('final-score');
const clearScoreElement = document.getElementById('clear-score');

アセット管理

ゲームで使用する画像やサウンドを管理します。今回はデータURLを使用して画像を埋め込みます。

// アセットの読み込み
const ASSETS = {
    images: {
        player: 'https://rakuraku-site.prodouga.com/img/RakuRaku.avif',
        enemy1: 'data:image/svg+xml;base64,...', // SVGデータ
        enemy2: 'data:image/svg+xml;base64,...', // SVGデータ
        boss: 'data:image/svg+xml;base64,...',   // SVGデータ
        powerup: 'data:image/svg+xml;base64,...', // SVGデータ
        obstacle: 'data:image/svg+xml;base64,...' // SVGデータ
    },
    sounds: {
        bgm: 'https://soundbible.com/mp3/Sci-Fi_Drone-Sweeper-1760111493.mp3',
        shoot: 'https://soundbible.com/mp3/Laser_Shoot-Zapper-1135544508.mp3',
        explosion: 'https://soundbible.com/mp3/Explosion-SoundBible.com-2019248186.mp3',
        powerup: 'https://soundbible.com/mp3/Power_Up_Ray-Mike_Koenig-800933783.mp3',
        gameOver: 'https://soundbible.com/mp3/Sad_Trombone-Joe_Lamb-665429450.mp3',
        gameClear: 'https://soundbible.com/mp3/Ta Da-SoundBible.com-1884170640.mp3'
    }
};

// アセットのプリロード
const loadedImages = {};
const loadedSounds = {};

// 画像の読み込み関数
function loadImages() {
    const promises = [];
    
    for (const key in ASSETS.images) {
        const promise = new Promise((resolve, reject) => {
            const img = new Image();
            img.onload = () => {
                loadedImages[key] = img;
                resolve();
            };
            img.onerror = () => {
                console.error(`画像の読み込みに失敗しました: ${key}`);
                // エラー時にもresolveして処理を続行
                loadedImages[key] = createFallbackImage(key);
                resolve();
            };
            img.src = ASSETS.images[key];
        });
        promises.push(promise);
    }
    
    return Promise.all(promises);
}

// 画像読み込み失敗時のフォールバック
function createFallbackImage(type) {
    // フォールバック画像の生成ロジック
    // ...(省略)
}

// サウンドの読み込み
function loadSounds() {
    for (const key in ASSETS.sounds) {
        const audio = new Audio();
        audio.src = ASSETS.sounds[key];
        
        if (key === 'bgm') {
            audio.loop = true;
            audio.volume = 0.5;
        } else {
            audio.volume = 0.7;
        }
        
        loadedSounds[key] = audio;
    }
}

ゲームオブジェクトの定義

プレイヤー、敵、弾などのゲームオブジェクトを定義します。

// プレイヤーオブジェクト
const player = {
    x: 100,
    y: HEIGHT / 2 - 20,
    width: 60,
    height: 40,
    speed: 5,
    bullets: [],
    lastShot: 0,
    shootDelay: 250, // ミリ秒
    isInvulnerable: false,
    invulnerableTime: 0,
    invulnerableDuration: 2000 // 無敵時間(ミリ秒)
};

// ゲーム内の各種オブジェクト配列
const enemies = [];
const powerups = [];
const obstacles = [];
const explosions = [];
const stars = [];

背景の星を生成

宇宙の雰囲気を出すために、背景に星を生成します。

// 星の生成
function createStars() {
    for (let i = 0; i  {
    keys[e.key] = true;
    
    // スペースキーでの発射
    if (e.key === ' ' && gameState.isRunning) {
        shootBullet();
    }
});

window.addEventListener('keyup', (e) => {
    keys[e.key] = false;
});

プレイヤーの弾発射

プレイヤーが弾を発射する処理を実装します。

// プレイヤーの弾発射
function shootBullet() {
    const currentTime = Date.now();
    if (currentTime - player.lastShot > player.shootDelay) {
        player.lastShot = currentTime;
        
        // パワーレベルに応じた弾の生成
        switch(gameState.powerLevel) {
            case 0: // 基本の弾
                player.bullets.push({
                    x: player.x + player.width,
                    y: player.y + player.height / 2 - 2,
                    width: 10,
                    height: 4,
                    speed: 10,
                    power: 1,
                    color: '#00ffff'
                });
                break;
            case 1: // 2連射
                // 2連射の実装
                // ...(省略)
                break;
            case 2: // 3連射
                // 3連射の実装
                // ...(省略)
                break;
            case 3: // レーザー
                // レーザーの実装
                // ...(省略)
                break;
        }
        
        playSound('shoot');
    }
}

衝突判定

オブジェクト同士の衝突判定を行う関数を実装します。

// 衝突判定
function checkCollision(obj1, obj2) {
    return obj1.x  obj2.x &&
           obj1.y  obj2.y;
}

ゲームの更新処理

ゲームの状態を更新する関数を実装します。これがゲームの中核となります。

// ゲームの更新
function update() {
    if (!gameState.isRunning) return;
    
    // プレイヤーの移動
    if (keys['ArrowUp'] && player.y > 0) {
        player.y -= player.speed;
    }
    if (keys['ArrowDown'] && player.y  0) {
        player.x -= player.speed;
    }
    if (keys['ArrowRight'] && player.x < WIDTH - player.width) {
        player.x += player.speed;
    }
    
    // 自動発射(スペースキー長押し)
    if (keys[' ']) {
        shootBullet();
    }
    
    // 星の移動
    for (let i = 0; i < stars.length; i++) {
        stars[i].x -= stars[i].speed;
        if (stars[i].x < 0) {
            stars[i].x = WIDTH;
            stars[i].y = Math.random() * HEIGHT;
        }
    }
    
    // プレイヤーの弾の移動
    // ...(省略)
    
    // 敵の移動とAI
    // ...(省略)
    
    // 障害物の移動と衝突判定
    // ...(省略)
    
    // パワーアップアイテムの移動と衝突判定
    // ...(省略)
    
    // 爆発エフェクトの更新
    // ...(省略)
    
    // 無敵時間の処理
    // ...(省略)
    
    // 敵の生成(ウェーブ管理)
    manageEnemyWaves();
}

描画処理

ゲームオブジェクトを描画する関数を実装します。

// 描画処理
function draw() {
    // 背景のクリア
    ctx.fillStyle = '#000';
    ctx.fillRect(0, 0, WIDTH, HEIGHT);
    
    // 星の描画
    ctx.fillStyle = '#fff';
    for (const star of stars) {
        ctx.beginPath();
        ctx.arc(star.x, star.y, star.size, 0, Math.PI * 2);
        ctx.fill();
    }
    
// プレイヤーの描画(無敵時は点滅)
    if (!player.isInvulnerable || Math.floor(Date.now() / 100) % 2 === 0) {
        ctx.drawImage(loadedImages.player, player.x, player.y, player.width, player.height);
    }
    
    // プレイヤーの弾の描画
    for (const bullet of player.bullets) {
        ctx.fillStyle = bullet.color;
        ctx.fillRect(bullet.x, bullet.y, bullet.width, bullet.height);
    }
    
    // 敵の描画
    for (const enemy of enemies) {
        let enemyImage;
        switch(enemy.type) {
            case 'small':
                enemyImage = loadedImages.enemy1;
                break;
            case 'medium':
                enemyImage = loadedImages.enemy2;
                break;
            case 'boss':
                enemyImage = loadedImages.boss;
                break;
        }
        ctx.drawImage(enemyImage, enemy.x, enemy.y, enemy.width, enemy.height);
        
        // 敵の弾の描画
        // ...(省略)
        
        // 敵のヘルスバー(ボスのみ)
        // ...(省略)
    }
    
    // 障害物、パワーアップ、爆発エフェクトの描画
    // ...(省略)
    
    // UI表示
    ctx.fillStyle = '#fff';
    ctx.font = '20px Arial';
    ctx.fillText(`スコア: ${gameState.score}`, 20, 30);
    ctx.fillText(`残機: ${gameState.lives}`, 20, 60);
    ctx.fillText(`レベル: ${gameState.level}`, 20, 90);
    ctx.fillText(`パワー: ${gameState.powerLevel}`, 20, 120);
    
    // ウェーブ表示
    ctx.fillStyle = '#ff0';
    ctx.fillText(`ウェーブ: ${currentWave + 1}`, WIDTH - 150, 30);
}

ゲームループ

ゲームのメインループを実装します。requestAnimationFrameを使用して、滑らかなアニメーションを実現します。

// ゲームループ
let lastTime = 0;
function gameLoop(timestamp) {
    if (!lastTime) lastTime = timestamp;
    const deltaTime = timestamp - lastTime;
    lastTime = timestamp;
    
    if (gameState.isRunning) {
        update();
        draw();
    }
    
    requestAnimationFrame(gameLoop);
}

ゲーム開始処理

ゲームを開始する関数を実装します。

// ゲーム開始
async function startGame() {
    // アセットの読み込み
    await loadImages();
    loadSounds();
    
    // 星の生成
    createStars();
    
    // イベントリスナー
    startButton.addEventListener('click', () => {
        startScreen.style.display = 'none';
        resetGame();
    });
    
    retryButton.addEventListener('click', () => {
        resetGame();
    });
    
    nextButton.addEventListener('click', () => {
        nextLevel();
    });
    
    // ゲームループの開始
    requestAnimationFrame(gameLoop);
}

// ゲーム初期化
startGame();

🔍 ゲーム開発の重要なポイント

ここからは、ゲーム開発において特に重要なポイントについて詳しく解説します。

1. ゲームループの仕組み 🔄

ゲーム開発の核となるのが「ゲームループ」です。一般的なゲームループは以下のような流れになります:

┌─────────────┐
│ ゲーム開始   │
└─────┬───────┘
      ↓
┌─────────────┐
│ 入力処理    │
└─────┬───────┘
      ↓
┌─────────────┐
│ 状態更新    │
└─────┬───────┘
      ↓
┌─────────────┐
│ 描画処理    │
└─────┬───────┘
      ↓
┌─────────────┐
│ ゲーム終了? │─── はい ──→ 終了
└─────┬───────┘
      │ いいえ
      ↓
   (ループ)

今回のゲームでは、requestAnimationFrameを使用してブラウザのリフレッシュレートに合わせたゲームループを実装しています。これにより、滑らかなアニメーションが実現できます。

2. 衝突判定のテクニック 💥

ゲームにおいて衝突判定は非常に重要な要素です。今回は単純な矩形(AABB)衝突判定を使用していますが、より複雑なゲームでは以下のような衝突判定方法も考慮する必要があります:

  • 円形衝突判定(Circle Collision)
  • ピクセル単位の衝突判定(Pixel-Perfect Collision)
  • 空間分割による最適化(Spatial Partitioning)

3. パフォーマンス最適化 ⚡

ブラウザゲームでは、パフォーマンスの最適化が非常に重要です。以下のような点に注意しましょう:

  • オブジェクトプーリング:頻繁に生成・破棄されるオブジェクト(弾など)は、事前に作成したオブジェクトを再利用する
  • 画面外のオブジェクトの処理を省略する
  • 複雑な計算は事前に行い、結果をキャッシュする

例えば、弾のオブジェクトプーリングは以下のように実装できます:

// 弾のプール
const bulletPool = [];
const POOL_SIZE = 100;

// プールの初期化
function initBulletPool() {
    for (let i = 0; i  highScore) {
        localStorage.setItem('highScore', score);
        return true; // 新記録
    }
    return false;
}

// ハイスコアの取得
function getHighScore() {
    return localStorage.getItem('highScore') || 0;
}

2. モバイル対応(タッチ操作)

// タッチイベントの追加
canvas.addEventListener('touchstart', handleTouchStart);
canvas.addEventListener('touchmove', handleTouchMove);
canvas.addEventListener('touchend', handleTouchEnd);

let touchX, touchY;
let isShooting = false;

function handleTouchStart(e) {
    e.preventDefault();
    const touch = e.touches;
    touchX = touch.clientX - canvas.getBoundingClientRect().left;
    touchY = touch.clientY - canvas.getBoundingClientRect().top;
    
    // 画面右側タッチで発射
    if (touchX > WIDTH / 2) {
        isShooting = true;
    }
}

function handleTouchMove(e) {
    e.preventDefault();
    const touch = e.touches;
    const newTouchX = touch.clientX - canvas.getBoundingClientRect().left;
    const newTouchY = touch.clientY - canvas.getBoundingClientRect().top;
    
    // 画面左側でのタッチ移動でプレイヤー移動
    if (newTouchX < WIDTH / 2) {
        player.x += (newTouchX - touchX) * 0.5;
        player.y += (newTouchY - touchY) * 0.5;
        
        // 画面内に収める
        player.x = Math.max(0, Math.min(WIDTH - player.width, player.x));
        player.y = Math.max(0, Math.min(HEIGHT - player.height, player.y));
    }
    
    touchX = newTouchX;
    touchY = newTouchY;
}

function handleTouchEnd(e) {
    e.preventDefault();
    isShooting = false;
}

3. 複数のステージ設計

// ステージデータ
const STAGES = [
    {
        name: "宇宙空間",
        enemyRate: { small: 0.02, medium: 0.01 },
        bossHealth: 20,
        background: "#000022"
    },
    {
        name: "小惑星帯",
        enemyRate: { small: 0.03, medium: 0.015 },
        obstacleRate: 0.02,
        bossHealth: 30,
        background: "#220022"
    },
    {
        name: "敵惑星軌道",
        enemyRate: { small: 0.04, medium: 0.02 },
        obstacleRate: 0.01,
        bossHealth: 40,
        background: "#002222"
    }
];

// 現在のステージデータを取得
function getCurrentStage() {
    const stageIndex = Math.min(gameState.level - 1, STAGES.length - 1);
    return STAGES[stageIndex];
}

📚 まとめ

今回は、JavaScriptとHTML5 Canvasを使用して、横スクロールシューティングゲームを一から開発する方法を解説しました。ゲーム開発は、プログラミングの様々な要素(グラフィックス、物理演算、入力処理、サウンド)を組み合わせる必要があり、非常に学びが多い分野です。

今回のコードをベースに、自分だけのオリジナルゲームを作ってみてください!例えば、以下のようなアレンジが考えられます:

  • 独自の敵キャラクターやボスを追加する
  • 新しい武器やパワーアップを実装する
  • ステージの背景や音楽を変更する
  • 2人プレイ機能を追加する

ゲーム開発は創造性を発揮できる楽しい分野です。ぜひ挑戦してみてください!

🔗 参考リソース

次回は、Three.jsを使った3Dゲーム開発について解説する予定です。お楽しみに!


この記事が参考になりましたら、いいね👍やコメントをいただけると嬉しいです。また、質問や要望があれば、お気軽にコメント欄に書き込んでください。

それでは、楽しいゲーム開発ライフを!🎮✨


最後に:業務委託のご相談を承ります

私は、業務委託エンジニアとしてWEB制作やシステム開発を請け負っています。最新技術を活用し、レスポンシブなWebサイトやインタラクティブなアプリケーション、API連携など、幅広いニーズに対応可能です。

「課題解決に向けた即戦力が欲しい」「高品質なWeb制作を依頼したい」という方は、ぜひお気軽にご相談ください。一緒にビジネスの成長を目指しましょう!

👉 ポートフォリオ

🌳 らくらくサイト

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?