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?

🎮 初心者でも作れる!ドラッグ&ドロップで学ぶブラウザゲーム開発入門 「ラク子のWEB制作大冒険」実装解説 ✨

Last updated at Posted at 2025-03-08

ブラウザゲーム開発入門:初心者でも作れる「ラク子のWEB制作大冒険」🎮

はじめに

みなさん、こんにちは!@YushiYamamotoです。今日は私の大好きな話題、ブラウザゲーム開発についてお話しします。「プログラミングは難しそう...」「ゲーム開発なんて敷居が高い...」と思っている方も多いのではないでしょうか?実は、基本的なHTML、CSS、JavaScriptの知識があれば、誰でも楽しいブラウザゲームを作ることができるんです!✨

今回は、私が最近作った「ラク子のWEB制作大冒険」というパズルゲームを例に、ブラウザゲーム開発の基礎から実装までを解説します。このゲームは、WEB制作の基本要素をブロックとして配置し、正しいコードを完成させるというコンセプトです。

See the Pen Untitled by Yushi Yamamoto (@yamamotoyushi) on CodePen.

ゲーム開発の基本ステップ 🚀

ブラウザゲームを開発する際の基本的な流れは次のとおりです:

  1. コンセプト設計 - ゲームの基本アイデアを固める
  2. プロトタイプ作成 - 簡単な動作確認ができるものを作る
  3. メカニクス実装 - ゲームの核となる機能を実装する
  4. UI/UXデザイン - ユーザーインターフェースを設計・実装する
  5. テスト&改善 - バグ修正や遊びやすさの向上
  6. 公開&フィードバック - ゲームを公開し、ユーザーの反応を見る

今回のゲーム「ラク子のWEB制作大冒険」では、これらのステップに沿って開発を進めました。

開発環境の準備 🛠️

まずは開発環境を整えましょう。ブラウザゲーム開発には特別な環境は必要なく、以下のものがあれば十分です:

  • テキストエディタ(VSCode、Sublime Textなど)
  • モダンなWebブラウザ(Chrome、Firefox、Edgeなど)
  • 基本的なHTML/CSS/JavaScriptの知識

高度なゲームエンジンは必要ありません!シンプルなゲームであれば、純粋なHTML5とJavaScriptだけで十分作れます。

ゲーム構造の設計 📝

「ラク子のWEB制作大冒険」の基本構造は次のようになっています:

game/
├── index.html     # ゲームのHTMLファイル
├── style.css      # スタイルシート
└── game.js        # ゲームロジック

シンプルな構成ですが、これだけで十分機能的なゲームが作れます。

HTML:ゲームの骨組み 🏗️

まずはHTMLファイルを作成し、ゲームの基本構造を定義します:

    <div class="game-container">
        <header>
            <h1>ラク子のWEB制作大冒険</h1>
            <div class="game-info">
                <div class="level">レベル: <span id="level">1</span></div>
                <div class="score">スコア: <span id="score">0</span></div>
                <div class="timer">残り時間: <span id="timer">60</span></div>
            </div>
        </header>
        
        <div class="game-area">
            <div class="character">
                <img src="https://rakuraku-site.prodouga.com/img/RakuRaku.avif" alt="ラク子さん" id="rakuko">
            </div>
            
            <div class="code-area">
                <div class="code-problem">
                    <h3>問題:<span id="problem-title">シンプルなHTMLページを作ろう</span></h3>
                    <p id="problem-description">以下の要素を正しい順序で配置して、HTMLページを完成させましょう。</p>
                </div>
                
                <div class="code-blocks-container">
                    <div class="code-blocks" id="available-blocks">
                        <!-- ここにブロックが動的に追加されます -->
                    </div>
                    
                    <div class="code-editor" id="code-editor">
                        <!-- ここにドロップされたブロックが表示されます -->
                    </div>
                </div>
            </div>
        </div>
        
        <div class="controls">
            <button id="check-btn">コードをチェック</button>
            <button id="hint-btn">ヒント</button>
            <button id="reset-btn">リセット</button>
        </div>
        
        <div class="modal" id="result-modal">
            <div class="modal-content">
                <h2 id="result-title">結果</h2>
                <p id="result-message"></p>
                <div class="modal-buttons">
                    <button id="next-level-btn">次のレベルへ</button>
                    <button id="retry-btn">やり直す</button>
                </div>
            </div>
        </div>
        
        <div class="modal" id="hint-modal">
            <div class="modal-content">
                <h2>ヒント</h2>
                <p id="hint-text"></p>
                <button id="close-hint-btn">閉じる</button>
            </div>
        </div>
    </div>

このHTMLでは、ゲームの基本的なUI要素を定義しています:

  • ヘッダー部分(タイトル、レベル、スコア、タイマー)
  • ゲームエリア(キャラクター表示部分とコード編集部分)
  • コントロール部分(ボタン類)
  • モーダルウィンドウ(結果表示やヒント表示用)

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

次に、CSSでゲームの見た目を整えます:

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}

body {
    background-color: #f0f8ff;
    color: #333;
}

.game-container {
    max-width: 1000px;
    margin: 0 auto;
    padding: 20px;
}

header {
    text-align: center;
    margin-bottom: 20px;
}

h1 {
    color: #4a6fa5;
    margin-bottom: 15px;
}

.game-info {
    display: flex;
    justify-content: space-around;
    background-color: #e0eaf9;
    padding: 10px;
    border-radius: 8px;
    font-weight: bold;
}

.game-area {
    display: flex;
    margin-bottom: 20px;
}

.character {
    width: 30%;
    display: flex;
    justify-content: center;
    align-items: flex-start;
}

.character img {
    max-width: 100%;
    max-height: 300px;
    border-radius: 10px;
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}

.code-area {
    width: 70%;
    background-color: white;
    border-radius: 8px;
    padding: 15px;
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}

/* コードブロックのスタイル */
.code-blocks {
    min-height: 100px;
    background-color: #f5f5f5;
    padding: 10px;
    border-radius: 5px;
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
}

.code-editor {
    min-height: 200px;
    background-color: #2d2d2d;
    color: white;
    padding: 15px;
    border-radius: 5px;
    font-family: 'Courier New', Courier, monospace;
}

.code-block {
    background-color: #4a6fa5;
    color: white;
    padding: 8px 12px;
    border-radius: 5px;
    cursor: grab;
    user-select: none;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
    transition: transform 0.2s;
}

.code-block:hover {
    transform: translateY(-2px);
}

/* 言語別のブロックカラー */
.code-block.html {
    background-color: #e44d26;
}

.code-block.css {
    background-color: #264de4;
}

.code-block.js {
    background-color: #f7df1e;
    color: black;
}

/* ボタンスタイル */
.controls {
    display: flex;
    justify-content: center;
    gap: 15px;
    margin-top: 20px;
}

button {
    padding: 10px 20px;
    background-color: #4a6fa5;
    color: white;
    border: none;
    border-radius: 5px;
    cursor: pointer;
    font-weight: bold;
    transition: background-color 0.3s;
}

button:hover {
    background-color: #3a5a8a;
}

/* モーダルウィンドウ */
.modal {
    display: none;
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-color: rgba(0, 0, 0, 0.5);
    z-index: 1000;
    justify-content: center;
    align-items: center;
}

.modal-content {
    background-color: white;
    padding: 20px;
    border-radius: 8px;
    max-width: 500px;
    width: 90%;
    text-align: center;
}

/* アニメーション */
@keyframes celebrate {
    0% { transform: scale(1); }
    50% { transform: scale(1.1); }
    100% { transform: scale(1); }
}

.celebrate {
    animation: celebrate 0.5s ease-in-out;
}

CSSでは、ゲームの視覚的な部分を整えています。特に注目すべき点は:

  • フレックスボックスを使ったレイアウト
  • コードブロックの視覚的な区別(HTML、CSS、JSで色分け)
  • ホバーエフェクトやアニメーション
  • レスポンシブデザイン対応

JavaScript:ゲームロジックの実装 ⚙️

最後に、JavaScriptでゲームのロジックを実装します。これが最も重要な部分です:

document.addEventListener('DOMContentLoaded', function() {
    // ゲーム状態の管理
    const gameState = {
        level: 1,
        score: 0,
        timeLeft: 60,
        timer: null,
        draggingElement: null
    };

    // DOM要素の取得
    const levelElement = document.getElementById('level');
    const scoreElement = document.getElementById('score');
    const timerElement = document.getElementById('timer');
    const rakukoElement = document.getElementById('rakuko');
    const problemTitleElement = document.getElementById('problem-title');
    const problemDescriptionElement = document.getElementById('problem-description');
    const availableBlocksElement = document.getElementById('available-blocks');
    const codeEditorElement = document.getElementById('code-editor');
    const checkButton = document.getElementById('check-btn');
    const hintButton = document.getElementById('hint-btn');
    const resetButton = document.getElementById('reset-btn');
    const resultModal = document.getElementById('result-modal');
    const resultTitleElement = document.getElementById('result-title');
    const resultMessageElement = document.getElementById('result-message');
    const nextLevelButton = document.getElementById('next-level-btn');
    const retryButton = document.getElementById('retry-btn');
    const hintModal = document.getElementById('hint-modal');
    const hintTextElement = document.getElementById('hint-text');
    const closeHintButton = document.getElementById('close-hint-btn');

    // レベルデータの定義
    const levels = [
        {
            title: "シンプルなHTMLページを作ろう",
            description: "以下の要素を正しい順序で配置して、HTMLページを完成させましょう。",
            blocks: [
                { id: "html1", text: "", type: "html" },
                { id: "html2", text: "", type: "html" },
                { id: "html3", text: "", type: "html" },
                { id: "html4", text: "マイページ", type: "html" },
                { id: "html5", text: "", type: "html" },
                { id: "html6", text: "", type: "html" },
                { id: "html7", text: "こんにちは!", type: "html" },
                { id: "html8", text: "", type: "html" },
                { id: "html9", text: "", type: "html" }
            ],
            solution: ["html1", "html2", "html3", "html4", "html5", "html6", "html7", "html8", "html9"],
            hint: "HTMLドキュメントはから始まり、タグで囲みます。その中にとがあります。"
        },
        {
            title: "CSSでスタイリングしよう",
            description: "以下のCSSプロパティを使って、テキストを中央揃えにし、青色で表示するスタイルを作成しましょう。",
            blocks: [
                { id: "css1", text: "h1 {", type: "css" },
                { id: "css2", text: "  color: blue;", type: "css" },
                { id: "css3", text: "  text-align: center;", type: "css" },
                { id: "css4", text: "  font-size: 24px;", type: "css" },
                { id: "css5", text: "}", type: "css" }
            ],
            solution: ["css1", "css2", "css3", "css4", "css5"],
            hint: "CSSルールはセレクタで始まり、中括弧{}の中にプロパティと値のペアを記述します。"
        },
        // 他のレベルも同様に定義...
    ];

    // ゲーム初期化
    function initGame() {
        updateUI();
        loadLevel(gameState.level);
        startTimer();
        setupDragAndDrop();
    }

    // UIの更新
    function updateUI() {
        levelElement.textContent = gameState.level;
        scoreElement.textContent = gameState.score;
        timerElement.textContent = gameState.timeLeft;
    }

    // レベルのロード
    function loadLevel(levelNumber) {
        const levelIndex = levelNumber - 1;
        if (levelIndex >= levels.length) {
            // ゲームクリア
            showGameComplete();
            return;
        }

        const level = levels[levelIndex];
        problemTitleElement.textContent = level.title;
        problemDescriptionElement.textContent = level.description;

        // 利用可能なブロックを表示(シャッフルして)
        availableBlocksElement.innerHTML = '';
        const shuffledBlocks = [...level.blocks].sort(() => Math.random() - 0.5);
        
        shuffledBlocks.forEach(block => {
            const blockElement = document.createElement('div');
            blockElement.className = `code-block ${block.type}`;
            blockElement.textContent = block.text;
            blockElement.dataset.id = block.id;
            blockElement.draggable = true;
            availableBlocksElement.appendChild(blockElement);
        });

        // コードエディタをクリア
        codeEditorElement.innerHTML = '';
    }

    // ドラッグ&ドロップの設定
    function setupDragAndDrop() {
        // ドラッグ開始
        document.addEventListener('dragstart', (e) => {
            if (e.target.classList.contains('code-block')) {
                gameState.draggingElement = e.target;
                e.target.classList.add('dragging');
                e.dataTransfer.setData('text/plain', e.target.dataset.id);
            }
        });

        // ドラッグ終了
        document.addEventListener('dragend', (e) => {
            if (e.target.classList.contains('code-block')) {
                e.target.classList.remove('dragging');
            }
        });

        // ドラッグオーバー
        codeEditorElement.addEventListener('dragover', (e) => {
            e.preventDefault();
            codeEditorElement.classList.add('drop-zone');
        });

        // ドラッグリーブ
        codeEditorElement.addEventListener('dragleave', () => {
            codeEditorElement.classList.remove
codeEditorElement.classList.remove('drop-zone');
        });

        // ドロップ
        codeEditorElement.addEventListener('drop', (e) => {
            e.preventDefault();
            codeEditorElement.classList.remove('drop-zone');
            
            if (gameState.draggingElement) {
                if (gameState.draggingElement.parentElement === availableBlocksElement) {
                    codeEditorElement.appendChild(gameState.draggingElement);
                }
                gameState.draggingElement = null;
            }
        });

        // 利用可能なブロックエリアにもドロップ可能に
        availableBlocksElement.addEventListener('dragover', (e) => {
            e.preventDefault();
            availableBlocksElement.classList.add('drop-zone');
        });

        availableBlocksElement.addEventListener('dragleave', () => {
            availableBlocksElement.classList.remove('drop-zone');
        });

        availableBlocksElement.addEventListener('drop', (e) => {
            e.preventDefault();
            availableBlocksElement.classList.remove('drop-zone');
            
            if (gameState.draggingElement) {
                if (gameState.draggingElement.parentElement === codeEditorElement) {
                    availableBlocksElement.appendChild(gameState.draggingElement);
                }
                gameState.draggingElement = null;
            }
        });
    }

    // コードのチェック
    function checkCode() {
        const currentLevel = levels[gameState.level - 1];
        const codeBlocks = Array.from(codeEditorElement.children);
        const userSolution = codeBlocks.map(block => block.dataset.id);
        
        // 解答が正しいかチェック
        const isCorrect = arraysEqual(userSolution, currentLevel.solution);
        
        if (isCorrect) {
            // 正解
            const timeBonus = Math.max(0, gameState.timeLeft);
            const levelBonus = gameState.level * 100;
            const pointsEarned = 500 + timeBonus + levelBonus;
            gameState.score += pointsEarned;
            
            // 結果モーダルを表示
            resultTitleElement.textContent = "正解!";
            resultMessageElement.textContent = `おめでとうございます!正しいコードが完成しました。\n${pointsEarned}ポイント獲得しました!`;
            resultModal.style.display = 'flex';
            
            // ラク子さんを喜ばせる
            rakukoElement.classList.add('celebrate');
            setTimeout(() => {
                rakukoElement.classList.remove('celebrate');
            }, 1000);
        } else {
            // 不正解
            resultTitleElement.textContent = "残念!";
            resultMessageElement.textContent = "コードが正しくありません。もう一度挑戦してみましょう。";
            resultModal.style.display = 'flex';
        }
    }
    
    // 配列が等しいかチェック
    function arraysEqual(arr1, arr2) {
        if (arr1.length !== arr2.length) return false;
        for (let i = 0; i  {
            gameState.timeLeft--;
            timerElement.textContent = gameState.timeLeft;

            if (gameState.timeLeft  {
        gameState.level++;
        gameState.timeLeft = 60 + (gameState.level * 10); // レベルが上がるごとに時間を増やす
        resultModal.style.display = 'none';
        loadLevel(gameState.level);
        updateUI();
        startTimer();
    });
    retryButton.addEventListener('click', () => {
        resultModal.style.display = 'none';
        loadLevel(gameState.level);
        gameState.timeLeft = 60;
        updateUI();
        startTimer();
    });
    hintButton.addEventListener('click', () => {
        const currentLevel = levels[gameState.level - 1];
        hintTextElement.textContent = currentLevel.hint;
        hintModal.style.display = 'flex';
    });
    closeHintButton.addEventListener('click', () => {
        hintModal.style.display = 'none';
    });
    resetButton.addEventListener('click', () => {
        loadLevel(gameState.level);
    });
    
    // ゲーム開始
    initGame();
});

ゲーム開発のポイント解説 💡

1. ゲーム状態の管理 📊

ゲーム開発で最も重要なのは「状態管理」です。今回のゲームでは、以下の状態を管理しています:

const gameState = {
    level: 1,        // 現在のレベル
    score: 0,        // スコア
    timeLeft: 60,    // 残り時間
    timer: null,     // タイマーID
    draggingElement: null // ドラッグ中の要素
};

状態をオブジェクトとして一元管理することで、ゲームの様々な部分で一貫した状態を参照できます。これは小規模なゲームでも非常に重要な設計パターンです。

2. ドラッグ&ドロップの実装 🖱️

HTML5のドラッグ&ドロップAPIを使用して、コードブロックの移動を実装しています。

// ドラッグ開始
document.addEventListener('dragstart', (e) => {
    if (e.target.classList.contains('code-block')) {
        gameState.draggingElement = e.target;
        e.target.classList.add('dragging');
        e.dataTransfer.setData('text/plain', e.target.dataset.id);
    }
});

// ドロップ
codeEditorElement.addEventListener('drop', (e) => {
    e.preventDefault();
    codeEditorElement.classList.remove('drop-zone');
    
    if (gameState.draggingElement) {
        if (gameState.draggingElement.parentElement === availableBlocksElement) {
            codeEditorElement.appendChild(gameState.draggingElement);
        }
        gameState.draggingElement = null;
    }
});

ここでのポイントは:

  1. dragstartイベントでドラッグ開始を検知
  2. dragoverイベントでpreventDefault()を呼び出し、ドロップを許可
  3. dropイベントでドロップ処理を実行
  4. 親要素を確認して、同じ要素内での移動を防止

3. レベルデータの設計 🎯

ゲームのレベルデータを配列として定義することで、簡単に新しいレベルを追加できます:

const levels = [
    {
        title: "シンプルなHTMLページを作ろう",
        description: "以下の要素を正しい順序で配置して、HTMLページを完成させましょう。",
        blocks: [
            { id: "html1", text: "", type: "html" },
            // ...他のブロック
        ],
        solution: ["html1", "html2", "html3", "html4", "html5", "html6", "html7", "html8", "html9"],
        hint: "HTMLドキュメントはから始まり..."
    },
    // ...他のレベル
];

このデータ駆動型の設計により、ゲームロジックとコンテンツを分離でき、後からレベルを追加するのが容易になります。

ゲームフロー図 🔄

以下は、このゲームの基本的なフロー図です:

┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│  ゲーム開始  │────▶│ レベルロード │────▶│ ブロック配置 │
└─────────────┘     └─────────────┘     └───────┬─────┘
                                                │
┌─────────────┐     ┌─────────────┐     ┌──────▼──────┐
│  次レベルへ  │◀────│  正解判定   │◀────│ コードチェック│
└─────────────┘     └───────┬─────┘     └─────────────┘
                            │
                    ┌───────▼─────┐
                    │  不正解時   │
                    │ やり直し    │
                    └─────────────┘

拡張アイデア 🚀

このゲームは基本的な機能を実装していますが、さらに以下のような拡張が考えられます:

  1. 難易度設定: 初級・中級・上級の難易度を選べるようにする
  2. タイムアタックモード: 制限時間内にどれだけのレベルをクリアできるかを競う
  3. ヒントシステムの強化: ヒントを見るとスコアが減少するなどのペナルティを設ける
  4. ソーシャル機能: スコアをSNSでシェアできる機能
  5. プログレッシブウェブアプリ(PWA)化: オフラインでもプレイできるようにする

パフォーマンス最適化のヒント ⚡

ブラウザゲームを開発する際は、パフォーマンスも重要です。以下のポイントに注意しましょう:

  1. イベントデリゲーション: 多数の要素に個別にイベントリスナーを設定するのではなく、親要素でイベントを捕捉する
  2. アニメーションの最適化: CSSアニメーションを使用し、JavaScriptでのDOM操作を最小限に抑える
  3. メモリリーク防止: 不要になったタイマーやイベントリスナーは適切に解除する
  4. アセット最適化: 画像は適切なフォーマット(WebP等)で最適化する

まとめ 📝

今回は「ラク子のWEB制作大冒険」というブラウザゲームの開発過程を解説しました。HTML、CSS、JavaScriptの基本的な知識を組み合わせることで、教育的かつ楽しいゲームを作ることができます。

ポイントをまとめると:

  • シンプルな構造: HTML/CSS/JavaScriptの3ファイル構成
  • 状態管理: ゲーム状態を一元管理
  • ドラッグ&ドロップ: HTML5 APIを活用
  • データ駆動設計: レベルデータを分離して管理
  • UI/UX: 視覚的フィードバックとアニメーション

ぜひこのコードを基に、自分だけのオリジナルゲームを作ってみてください!質問やフィードバックがあれば、コメント欄でお待ちしています。

次回は、このゲームにローカルストレージを使った進行状況の保存機能や、より高度なアニメーション効果を追加する方法について解説する予定です。お楽しみに!


補足:開発中によくあるトラブルと解決策 🔧

開発中に遭遇しやすい問題とその解決策をいくつか紹介します:

1. ドラッグ&ドロップが動作しない

原因: dragoverイベントでpreventDefault()を呼び出し忘れている

解決策:

element.addEventListener('dragover', (e) => {
    e.preventDefault(); // これが必須!
});

2. タイマーが重複して動作する

原因: タイマーを開始する前に古いタイマーをクリアしていない

解決策:

clearInterval(gameState.timer); // 既存のタイマーをクリア
gameState.timer = setInterval(() => { /* ... */ }, 1000);

3. モバイルデバイスでドラッグ&ドロップが機能しない

原因: HTML5のドラッグ&ドロップAPIはモバイルデバイスでのサポートが限定的

解決策: タッチイベントを使用した独自のドラッグ&ドロップ実装を検討するか、ライブラリ(SortableJSなど)を使用する


皆さんも楽しいブラウザゲーム開発にチャレンジしてみてください!プログラミング学習も、ゲーム開発を通じて行うと楽しく続けられますよ。😊


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

私は、業務委託エンジニアとして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?