0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

フレームワークなしで100以上のミニゲームポータルを作ったアーキテクチャ解説

0
Posted at

1. はじめに

スキマ時間にブラウザやスマホでダウンロード不要ですぐ遊べる「暇つぶしゲーム (collect777.com)」というWebアプリを開発・運用しています。

アクション・パズル・カジノ・テーブルゲーム・物理演算など 100種類以上のミニゲーム を作りました。

近年はReactやVue、Next.jsといったフロントエンドフレームワークを用いるのが一般的ですが、フレームワーク非依存(JS + HTML5 + CSS3)の構成を採用しました。

100以上のゲームを効率的に管理・運用し、ストレスのない快適なユーザー体験を実現するために採用したアーキテクチャ設計・状態管理・多言語対応(i18n)・モバイル最適化について技術的に解説します。

スクリーンショット 2026-09-12 11.55.08.png

2. 全体アーキテクチャ & ディレクトリ構造

本Webアプリは、1つのメインポータルと100以上の自律分散型ミニゲームで構成されています。

collect777.com/
├── index.html               # メインポータル(ヒーロー、検索/タグ/お気に入りUI、ゲームグリッド)
├── css/
│   ├── main.css             # ポータル全体・UIパーツのスタイル
│   └── plugins.css          # 各種UIプラグインスタイル
├── js/
│   ├── main.js              # ポータル状態管理(カテゴリ/タグ/検索/スクロール復元/履歴/お気に入り)
│   ├── game-common.js       # 全ゲーム共通UI・レスポンシブスタイル動的注入スクリプト
│   ├── i18n-game.js         # 多言語対応(i18n)& SNSシェアテキスト動的翻訳モジュール
│   └── thumbnail-plugins.js # サムネイルアニメーション等の補助処理
├── games/                   # 100以上の個別ゲームディレクトリ
│   ├── 2048-puzzle/
│   │   ├── index.html       # 各ゲーム個別HTML
│   │   ├── style.css        # 各ゲーム個別CSS
│   │   └── script.js        # ゲームロジック(Canvas / JavaScriptJS)
│   ├── breakout/            # ブロック崩し
│   ├── minesweeper/         # マインスイーパー
│   ├── reversi/             # リバーシ
│   └── ...                  # その他のミニゲーム
├── locales/                 # 各言語(日・英・西・仏・中)の翻訳データ
├── tools/                   # サイトマップ生成等の開発・運用自動化スクリプト
└── sitemap.xml              # SEO用サイトマップ

アーキテクチャの特長

  • 疎結合な自律分散構造: 各ゲームは games/<slug>/ 以下に自己完結(HTML/CSS/JS)しており、他ゲームに依存しません。新規ゲーム追加時はディレクトリを配置し、ポータル(index.html)にリンクを追加するだけで完了します。
  • 超軽量・依存ゼロ: フロントエンドのフレームワークビルドが不要で、ページの読み込み(FCP / LCP)が非常に高速です。

3. フロントエンド設計のこだわりポイント

① 共通処理の動的注入メカニズム (game-common.js)

各ゲームは独立して開発されますが、統一されたUI/UX(スクロールバーのデザイン、モバイル画面最適化、共通スタイルなど)を全ゲームに適用するため、game-common.js を作成し、スクリプト実行時に <style> タグを動的生成して <head> に注入しています。

/**
 * 全ゲーム共通スクリプト (js/game-common.js)
 * 動的にスクロールバーやモバイルレイアウトの調整を行います。
 */
(function() {
    const style = document.createElement('style');
    style.textContent = `
        /* カスタムスクロールバー */
        ::-webkit-scrollbar { width: 10px; }
        ::-webkit-scrollbar-track { background: #0e0e0e; }
        ::-webkit-scrollbar-thumb { background: #303030; border-radius: 5px; }

        /* モバイル・小画面デバイス向けの共通調整 */
        @media (max-width: 1000px) {
            body {
                display: flex !important;
                flex-direction: column !important;
                align-items: center !important;
            }
            .container, .game-container, .wrapper {
                background: transparent !important;
                backdrop-filter: none !important;
                border: none !important;
                box-shadow: none !important;
            }
        }
    `;
    document.head.appendChild(style);
})();

これにより、既存の個別ゲームコードを汚すことなく、全ゲームのUXとレスポンシブ表示を一括で制御できます。

② フレームワークレスなSPA風状態復元 (main.js)

ポータル(index.html)からゲームへ遷移し、ブラウザの「戻る」でポータルに戻ってきた際、ユーザーが選択していたカテゴリ、タグ、検索ワード、スクロール位置を忠実に復元する仕組みを sessionStorage で構築しています。

// js/main.js より抜粋
const TOP_PAGE_STATE_KEYS = {
    category: 'selectedCategory',
    tag: 'selectedTag',
    scroll: 'topPageScroll'
};

// ページ読み込み時にセッションから状態を復元
function restoreTopPageState() {
    const savedCategory = sessionStorage.getItem(TOP_PAGE_STATE_KEYS.category);
    const savedTag = sessionStorage.getItem(TOP_PAGE_STATE_KEYS.tag);
    const savedScroll = sessionStorage.getItem(TOP_PAGE_STATE_KEYS.scroll);

    if (savedCategory) currentCategory = savedCategory;
    if (savedTag) currentTag = savedTag;

    filterGames(); // フィルター再適用

    if (savedScroll) {
        window.scrollTo(0, parseInt(savedScroll, 10));
    }
}

ライブラリなしのJavaScriptでも、この設計によりSPA並みにストレスのないナビゲーションを実現しています。

③ クライアントサイド完全動的i18n (i18n-game.js)

グローバルに遊んでもらえるよう、日本語・英語・スペイン語・フランス語・中国語の5言語に対応しています。

言語判定優先順位:

  1. URLクエリパラメータ (?lang=en)
  2. sessionStorage に保存された設定
  3. ブラウザのデフォルト言語 (navigator.language)

また、各ゲームからX(旧Twitter)等へスコアをシェアする際、シェアメッセージ内の日本語ゲーム名やサイト名をユーザーの選択言語に合わせてリアルタイム翻訳する関数を組み込んでいます。

// js/i18n-game.js より抜粋
const translateSharedText = (text, isUrlEncoded = false) => {
    const lang = window.g_lang || 'ja';
    if (lang === 'ja') return text;

    let translated = text;
    const mappings = [];

    // ゲームタイトル・UI用文言のマッピングを構築
    if (window.g_gameData && window.g_gameData.title) {
        mappings.push({ ja: window.g_gameData.title.ja, tr: window.g_gameData.title[lang] });
    }
    // ...

    // 部分一致事故を防ぐため、文字列の長い順にソートして置換
    mappings.sort((a, b) => (b.ja || '').length - (a.ja || '').length);

    mappings.forEach(m => {
        if (!m.ja || !m.tr) return;
        const search = isUrlEncoded ? encodeURIComponent(m.ja) : m.ja;
        const replace = isUrlEncoded ? encodeURIComponent(m.tr) : m.tr;
        if (translated.includes(search)) {
            translated = translated.split(search).join(replace);
        }
    });

    return translated;
};

④ タッチデバイスとマウス操作の完全吸収

スマホでのタップ誤作動や、ダブルタップによる拡大、長押しによるスクロールの引っかかりを防ぐため、独自のイベントリスナーハンドラー addClickAndTouch を実装しています。

// js/main.js より抜粋
function addClickAndTouch(element, handler) {
    if (!element) return;
    let touchStartX = 0;
    let touchStartY = 0;
    let touchMoved = false;
    const TOUCH_MOVE_THRESHOLD = 10;

    element.addEventListener('touchstart', (e) => {
        if (e.touches && e.touches.length === 1) {
            touchStartX = e.touches[0].clientX;
            touchStartY = e.touches[0].clientY;
            touchMoved = false;
        }
    }, { passive: true });

    element.addEventListener('touchmove', (e) => {
        if (e.touches && e.touches.length === 1) {
            const dx = Math.abs(e.touches[0].clientX - touchStartX);
            const dy = Math.abs(e.touches[0].clientY - touchStartY);
            if (dx > TOUCH_MOVE_THRESHOLD || dy > TOUCH_MOVE_THRESHOLD) {
                touchMoved = true; // スクロール操作と判定してタップをキャンセル
            }
        }
    }, { passive: true });

    element.addEventListener('touchend', (e) => {
        if (!touchMoved) {
            e.preventDefault();
            handler(e);
        }
    });

    element.addEventListener('click', (e) => {
        handler(e);
    });
}

このユーティリティにより、スクロール目的のタッチとボタンタップを正確に識別し、モバイルでの快適な操作感を提供しています。

4. 個別ゲーム実装パターン(HTML5 Canvas / JavaScript)

100以上のゲームは、主に HTML5 Canvas API や DOM+CSS3 で描画されています。

例えば games/2048-puzzle/ などのパズルゲームやアクションゲームでは、requestAnimationFrame を利用した軽量なゲームループ構造をとっています。

// ゲームループの基本構造パターン
function gameLoop(timestamp) {
    updateLogic();  // 状態・物理演算の更新
    renderCanvas(); // Canvas描画

    if (!isGameOver) {
        requestAnimationFrame(gameLoop);
    }
}

// 描画処理
function renderCanvas() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    // オブジェクトの描画処理...
}

ライブラリを使わずCanvasのネイティブAPIのみで描画を行うことで、メモリ消費量を数十MB程度に抑え、低スペックなスマートフォンでも60fpsでスムーズに動作させています。

5. まとめ

大規模なWebアプリケーションであっても、要件によってはJavaScriptとWeb標準技術を工夫して組み合わせることで、以下のような多くのメリットが得られます。

  1. ビルドプロセス不要: リポジトリにコミットしたコードがそのまま即時実行・デプロイ可能。
  2. 圧倒的なパフォーマンス: 不要なJSランタイムが読み込まれないため、爆速な初期表示。
  3. 長期的なメンテナンス性: フレームワークのバージョンアップや非推奨化に伴う破壊的変更の影響を受けない。

今後もゲームラインナップの追加と、Web standard API(Web Audio, Web Workers等)を活用した機能拡張を続けていきます。

暇つぶしゲーム (collect777.com)

0
0
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
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?