【Cocos Creator】アタッチするだけ!VersionChecker (バージョン確認)の実装方法【TypeScript】

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

【Cocos Creator 3.8】VersionChecker の実装:アタッチするだけで HTTP 経由の最新版チェックを自動実行する汎用スクリプト

このガイドでは、ゲーム起動時に HTTP リクエストを投げて「最新版があるか」を自動チェックする、汎用コンポーネント VersionChecker を実装します。任意のノードにアタッチするだけで動作し、外部の GameManager やシングルトンに依存しない完全独立型です。

アプリ起動時やタイトル画面での「バージョン確認」「アップデート通知」などにそのまま使える設計とし、インスペクタから URL やタイムアウト、現在バージョンなどを柔軟に設定できるようにします。


コンポーネントの設計方針

1. 機能要件の整理

  • ゲーム起動(またはノード有効化)時に HTTP リクエストを 1 回送信する。
  • サーバーから「最新版のバージョン情報」を JSON で受け取り、現在バージョンと比較する。
  • 最新版が存在する場合はコンソールログで通知し、任意でコールバック(イベントディスパッチ)も行う。
  • HTTP エラー・JSON パースエラー・タイムアウトなどは、すべてコンソールに分かりやすくログを出す。
  • 外部のカスタムスクリプトには一切依存しない(完全独立)。
  • 挙動のほとんどをインスペクタの @property から制御できるようにする。

2. サーバー側レスポンスの想定フォーマット

ここでは、サーバーが以下のような JSON を返すことを前提にします(もちろん実際の仕様に合わせて変更可能です)。

{
  "latestVersion": "1.2.0",
  "forceUpdate": false,
  "downloadUrl": "https://example.com/download",
  "message": "新しいバージョンが利用可能です。"
}

このうち必須とみなすのは latestVersion のみで、その他のフィールドは存在すればログ表示に利用します。

3. インスペクタで設定可能なプロパティ

VersionChecker コンポーネントで設定できるプロパティと、その役割を整理します。

  • enabledOnStart: boolean
    – デフォルト: true
    – ノードが有効化されたとき(onEnable)に自動でバージョンチェックを実行するかどうか。
  • checkUrl: string
    – 例: https://example.com/api/version
    – バージョン情報を取得する HTTP(S) エンドポイント URL。空の場合は警告を出して処理を中断します。
  • currentVersion: string
    – 例: 1.0.0
    – クライアント側(このビルド)のバージョン文字列。サーバーから返ってきた latestVersion と比較します。
  • useGET: boolean
    – デフォルト: true
    true の場合は GET リクエスト、false の場合は POST リクエストを使用します。
  • requestBodyJson: string
    – デフォルト: 空文字(GET の場合は無視)
    – POST 時に送信する JSON 文字列。例: {"platform":"android","channel":"google"}
    – 不正な JSON でも送信自体は行いますが、エディタ上で分かりやすいようにログで警告します。
  • timeoutSeconds: number
    – デフォルト: 10
    – リクエストのタイムアウト秒数。0 以下の場合は「タイムアウトなし」と見なします。
  • autoLogResult: boolean
    – デフォルト: true
    – バージョンチェックの結果を自動的に console.log / console.warn / console.error に出力するかどうか。
  • dispatchNodeEvent: boolean
    – デフォルト: true
    – チェック完了時に、このノードから Cocos のイベントをディスパッチするかどうか。
  • eventNameOnUpToDate: string
    – デフォルト: version-up-to-date
    – 最新版と同じか、それより新しい場合にディスパッチするイベント名。
  • eventNameOnUpdateAvailable: string
    – デフォルト: version-update-available
    – 新しいバージョンが存在すると判定された場合にディスパッチするイベント名。
  • eventNameOnError: string
    – デフォルト: version-check-error
    – HTTP エラーや JSON パースエラーが発生した場合にディスパッチするイベント名。

4. バージョン比較ロジック

バージョン文字列(例: 1.2.3)を「ドット区切りの数値配列」として比較します。

  • 1.2.101.2.3 より新しい。
  • 長さが異なる場合は足りない部分を 0 として扱う(例: 1.21.2.0 は同じ)。
  • 数値以外が混ざっている場合は安全のため「同じ」とみなし、更新なしとして扱う。

これにより、「最新かどうか」の判定をシンプルかつ汎用的に行えます。


TypeScriptコードの実装

import { _decorator, Component, sys, JsonAsset, EventTarget } from 'cc';
const { ccclass, property } = _decorator;

interface VersionCheckResponse {
    latestVersion?: string;
    forceUpdate?: boolean;
    downloadUrl?: string;
    message?: string;
    // 追加のフィールドがあっても無視されるように緩く定義
}

type VersionCompareResult = 'NEWER' | 'OLDER' | 'EQUAL' | 'UNKNOWN';

@ccclass('VersionChecker')
export class VersionChecker extends Component {

    @property({
        tooltip: 'ノード有効化時(onEnable)に自動でバージョンチェックを実行するかどうか。',
    })
    public enabledOnStart: boolean = true;

    @property({
        tooltip: 'バージョン情報を取得する HTTP(S) エンドポイント URL。例: https://example.com/api/version',
    })
    public checkUrl: string = '';

    @property({
        tooltip: 'このクライアント(ビルド)の現在バージョン文字列。例: 1.0.0',
    })
    public currentVersion: string = '1.0.0';

    @property({
        tooltip: 'true なら GET、false なら POST リクエストで送信します。',
    })
    public useGET: boolean = true;

    @property({
        tooltip: 'POST 時に送信する JSON 文字列。GET の場合は無視されます。',
        multiline: true,
    })
    public requestBodyJson: string = '';

    @property({
        tooltip: 'リクエストのタイムアウト秒数。0 以下ならタイムアウトなし。',
        min: 0,
    })
    public timeoutSeconds: number = 10;

    @property({
        tooltip: 'チェック結果やエラーを自動的にコンソールへログ出力するかどうか。',
    })
    public autoLogResult: boolean = true;

    @property({
        tooltip: 'チェック完了時にこのノードからイベントをディスパッチするかどうか。',
    })
    public dispatchNodeEvent: boolean = true;

    @property({
        tooltip: '最新版と同じ、またはそれより新しいと判定された場合にディスパッチするイベント名。',
    })
    public eventNameOnUpToDate: string = 'version-up-to-date';

    @property({
        tooltip: '新しいバージョンが存在すると判定された場合にディスパッチするイベント名。',
    })
    public eventNameOnUpdateAvailable: string = 'version-update-available';

    @property({
        tooltip: 'HTTP エラーや JSON パースエラーなど、チェックに失敗した場合にディスパッチするイベント名。',
    })
    public eventNameOnError: string = 'version-check-error';

    /**
     * 内部状態: 直近のレスポンスデータを保持(任意で他スクリプトから参照可能)。
     */
    private _lastResponse: VersionCheckResponse | null = null;

    /**
     * 内部状態: 直近の比較結果を保持。
     */
    private _lastCompareResult: VersionCompareResult = 'UNKNOWN';

    onEnable() {
        if (this.enabledOnStart) {
            this.checkVersion();
        }
    }

    /**
     * 外部から手動でバージョンチェックをトリガーするための公開メソッド。
     */
    public checkVersion() {
        if (!this.checkUrl) {
            console.warn('[VersionChecker] checkUrl が設定されていません。処理を中断します。');
            this._emitError('NO_URL');
            return;
        }

        if (!sys.isNative && typeof fetch !== 'function') {
            console.error('[VersionChecker] fetch API が利用できません。ターゲットプラットフォームを確認してください。');
            this._emitError('NO_FETCH');
            return;
        }

        if (this.autoLogResult) {
            console.log(`[VersionChecker] バージョンチェック開始: ${this.checkUrl}`);
        }

        const controller = (typeof AbortController !== 'undefined') ? new AbortController() : null;
        let timeoutId: any = null;

        if (controller && this.timeoutSeconds > 0) {
            timeoutId = setTimeout(() => {
                controller.abort();
            }, this.timeoutSeconds * 1000);
        }

        const options: RequestInit = {
            method: this.useGET ? 'GET' : 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            signal: controller ? controller.signal : undefined,
        };

        if (!this.useGET && this.requestBodyJson) {
            try {
                // 一度 parse して正当な JSON か軽く確認(失敗してもそのまま送る)
                JSON.parse(this.requestBodyJson);
            } catch (e) {
                console.warn('[VersionChecker] requestBodyJson は正しい JSON ではない可能性があります。', e);
            }
            (options as any).body = this.requestBodyJson;
        }

        fetch(this.checkUrl, options)
            .then(async (response) => {
                if (timeoutId) {
                    clearTimeout(timeoutId);
                }

                if (!response.ok) {
                    throw new Error(`HTTP status ${response.status}`);
                }

                const text = await response.text();
                let json: VersionCheckResponse;

                try {
                    json = JSON.parse(text);
                } catch (e) {
                    console.error('[VersionChecker] JSON パースに失敗しました。レスポンス:', text);
                    this._emitError('JSON_PARSE_ERROR', e);
                    return;
                }

                this._lastResponse = json;

                const latest = json.latestVersion;
                if (!latest) {
                    console.warn('[VersionChecker] latestVersion フィールドがレスポンスに含まれていません。', json);
                    this._emitError('NO_LATEST_VERSION_FIELD');
                    return;
                }

                const compareResult = this._compareVersion(this.currentVersion, latest);
                this._lastCompareResult = compareResult;

                switch (compareResult) {
                    case 'EQUAL':
                        if (this.autoLogResult) {
                            console.log(`[VersionChecker] バージョンは最新です。current=${this.currentVersion}, latest=${latest}`);
                        }
                        this._emitUpToDate(json);
                        break;
                    case 'OLDER':
                        if (this.autoLogResult) {
                            console.warn(`[VersionChecker] 新しいバージョンが利用可能です。current=${this.currentVersion}, latest=${latest}`);
                            if (json.message) {
                                console.warn(`[VersionChecker] サーバーメッセージ: ${json.message}`);
                            }
                            if (json.downloadUrl) {
                                console.warn(`[VersionChecker] ダウンロード URL: ${json.downloadUrl}`);
                            }
                            if (json.forceUpdate) {
                                console.warn('[VersionChecker] 強制アップデートフラグが有効です。');
                            }
                        }
                        this._emitUpdateAvailable(json);
                        break;
                    case 'NEWER':
                        if (this.autoLogResult) {
                            console.log(`[VersionChecker] クライアントのバージョンがサーバーより新しい可能性があります。current=${this.currentVersion}, latest=${latest}`);
                        }
                        this._emitUpToDate(json);
                        break;
                    case 'UNKNOWN':
                    default:
                        console.warn('[VersionChecker] バージョン比較に失敗しました。current=', this.currentVersion, 'latest=', latest);
                        this._emitError('COMPARE_FAILED');
                        break;
                }
            })
            .catch((error) => {
                if (timeoutId) {
                    clearTimeout(timeoutId);
                }

                if ((error as any).name === 'AbortError') {
                    console.error(`[VersionChecker] リクエストがタイムアウトしました(${this.timeoutSeconds} 秒)。`);
                    this._emitError('TIMEOUT', error);
                } else {
                    console.error('[VersionChecker] バージョンチェック中にエラーが発生しました。', error);
                    this._emitError('NETWORK_ERROR', error);
                }
            });
    }

    /**
     * 直近のレスポンスを取得するための getter。
     */
    public get lastResponse(): VersionCheckResponse | null {
        return this._lastResponse;
    }

    /**
     * 直近のバージョン比較結果を取得するための getter。
     */
    public get lastCompareResult(): VersionCompareResult {
        return this._lastCompareResult;
    }

    /**
     * バージョン文字列を比較するユーティリティ。
     * a が b より古い: 'OLDER'
     * a が b より新しい: 'NEWER'
     * 等しい: 'EQUAL'
     * 比較不能: 'UNKNOWN'
     */
    private _compareVersion(a: string, b: string): VersionCompareResult {
        if (!a || !b) {
            return 'UNKNOWN';
        }

        const aParts = a.split('.');
        const bParts = b.split('.');

        const len = Math.max(aParts.length, bParts.length);

        for (let i = 0; i < len; i++) {
            const aNum = parseInt(aParts[i] || '0', 10);
            const bNum = parseInt(bParts[i] || '0', 10);

            if (Number.isNaN(aNum) || Number.isNaN(bNum)) {
                return 'UNKNOWN';
            }

            if (aNum > bNum) {
                return 'NEWER';
            } else if (aNum < bNum) {
                return 'OLDER';
            }
        }

        return 'EQUAL';
    }

    /**
     * バージョンが最新(もしくはそれ以上)と判定されたときの処理。
     */
    private _emitUpToDate(payload: VersionCheckResponse) {
        if (this.dispatchNodeEvent && this.eventNameOnUpToDate) {
            this.node.emit(this.eventNameOnUpToDate, {
                response: payload,
                compareResult: this._lastCompareResult,
                currentVersion: this.currentVersion,
            });
        }
    }

    /**
     * 新しいバージョンが存在すると判定されたときの処理。
     */
    private _emitUpdateAvailable(payload: VersionCheckResponse) {
        if (this.dispatchNodeEvent && this.eventNameOnUpdateAvailable) {
            this.node.emit(this.eventNameOnUpdateAvailable, {
                response: payload,
                compareResult: this._lastCompareResult,
                currentVersion: this.currentVersion,
            });
        }
    }

    /**
     * エラー発生時の処理。
     */
    private _emitError(reason: string, error?: any) {
        if (this.dispatchNodeEvent && this.eventNameOnError) {
            this.node.emit(this.eventNameOnError, {
                reason,
                error,
                currentVersion: this.currentVersion,
                lastResponse: this._lastResponse,
            });
        }
    }
}

コードのポイント解説

  • onEnable()
    – ノードが有効になったタイミングで enabledOnStart を確認し、自動で checkVersion() を呼び出します。
  • checkVersion()
    – インスペクタで設定した checkUrluseGETrequestBodyJson を使って fetch を実行。
    timeoutSeconds が指定されていれば AbortController でタイムアウト制御。
    – レスポンスをテキストで受け取り、JSON.parseVersionCheckResponse 型として解釈します。
  • エラーハンドリング
    – URL 未設定、fetch 未サポート、HTTP ステータスエラー、JSON パースエラー、タイムアウトなど、すべてのケースで console にログを出しつつ _emitError() を呼び出します。
  • バージョン比較 (_compareVersion)
    – ドット区切りで分割し、各要素を整数として比較。
    – どちらかに数値以外が含まれていれば 'UNKNOWN' を返して安全側に倒します。
  • イベントディスパッチ
    dispatchNodeEventtrue の場合のみ、this.node.emit() でイベントを投げます。
    – イベント名はインスペクタから自由に変更できるため、プロジェクト側で柔軟にリスン可能です。
  • 外部依存なし
    – 他のカスタムスクリプトやシングルトンには一切依存せず、このコンポーネント単体で完結しています。

使用手順と動作確認

1. スクリプトファイルの作成

  1. Assets パネルで右クリックします。
  2. Create → TypeScript を選択します。
  3. ファイル名を VersionChecker.ts に変更します。
  4. 作成された VersionChecker.ts をダブルクリックしてエディタで開き、
    先ほどの TypeScript コード全文を貼り付けて保存します。

2. テスト用ノードの作成

  1. Hierarchy パネルで右クリックします。
  2. Create → Empty Node を選択し、名前を VersionCheckerNode などに変更します。
  3. このノードには特別なコンポーネントは不要です(VersionChecker 単体で動作します)。

3. VersionChecker コンポーネントのアタッチ

  1. Hierarchy で先ほど作成した VersionCheckerNode を選択します。
  2. Inspector パネルで Add Component をクリックします。
  3. Custom カテゴリから VersionChecker を選択して追加します。

4. インスペクタでのプロパティ設定例

Inspector 上で、以下のように設定してみてください。

  • Enabled On Start: true
    → シーン開始時に自動でバージョンチェックが走ります。
  • Check Url:
    例として、テスト用のモック API を使うか、自前のサーバー URL を指定します。
    例: https://example.com/api/version
  • Current Version: 1.0.0
  • Use GET: true(まずは GET で確認)
  • Request Body Json: 空のままで OK(GET の場合は無視されます)。
  • Timeout Seconds: 10
  • Auto Log Result: true
  • Dispatch Node Event: true
  • Event Name On Up To Date: version-up-to-date
  • Event Name On Update Available: version-update-available
  • Event Name On Error: version-check-error

5. 簡易的なテスト方法

  1. Cocos Creator 上部メニューから Project → Preview をクリックし、ブラウザまたはネイティブでプレビューを起動します。
  2. コンソールログ(ブラウザの DevTools / Cocos の Console)を開きます。
  3. シーンがロードされると、[VersionChecker] バージョンチェック開始: ... というログが出力されます。
  4. サーバーが適切な JSON を返していれば、以下のいずれかのログが表示されます。
    • バージョンが最新の場合:
      [VersionChecker] バージョンは最新です。current=1.0.0, latest=1.0.0
    • 新しいバージョンが存在する場合:
      [VersionChecker] 新しいバージョンが利用可能です。current=1.0.0, latest=1.1.0 など。
    • エラーが発生した場合:
      [VersionChecker] バージョンチェック中にエラーが発生しました。
      [VersionChecker] JSON パースに失敗しました。 など。

6. イベントを使った応用(任意)

他のスクリプトから結果を受け取りたい場合は、同じノード上に別のコンポーネントを追加し、onLoad などでイベントをリッスンします。

import { _decorator, Component, Node } from 'cc';
const { ccclass } = _decorator;

@ccclass('VersionListenerExample')
export class VersionListenerExample extends Component {
    onLoad() {
        // VersionChecker と同じノードにアタッチされている前提
        this.node.on('version-update-available', (eventData: any) => {
            console.log('[VersionListenerExample] アップデートあり:', eventData);
            // ここでポップアップを出したり、ダウンロードページを開いたりする
        }, this);

        this.node.on('version-up-to-date', (eventData: any) => {
            console.log('[VersionListenerExample] 最新版です:', eventData);
        }, this);

        this.node.on('version-check-error', (eventData: any) => {
            console.warn('[VersionListenerExample] バージョンチェックでエラー:', eventData);
        }, this);
    }
}

このように、VersionChecker 自体は完全に独立していながら、必要に応じて他のコンポーネントから結果を受け取ることができます。


まとめ

  • VersionChecker は、任意のノードにアタッチするだけで起動時のバージョン確認を自動化できる汎用コンポーネントです。
  • URL、現在バージョン、HTTP メソッド、タイムアウト、イベント名などをすべてインスペクタから設定できるため、プロジェクトごとの要件に合わせて容易に調整できます。
  • 外部の GameManager やシングルトンに依存せず、このスクリプト単体で完結しているため、他プロジェクトへの転用やテンプレート化にも向いています。
  • イベントディスパッチを利用することで、「アップデートが必要になったときだけポップアップを出す」「エラー時だけリトライボタンを表示する」といった UI 側の実装もシンプルに行えます。

このコンポーネントをベースに、アプリ内アップデートフローやメンテナンス情報の取得など、ネットワーク連携の起点として拡張していけば、Cocos Creator プロジェクトの起動処理をより堅牢かつ再利用可能な形で構築できます。

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

URLをコピーしました!