【Cocos Creator】アタッチするだけ!AutoSaver (オートセーブ)の実装方法【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】AutoSaverの実装:アタッチするだけで「一定間隔 or 手動トリガーでバックグラウンドセーブ」を実現する汎用スクリプト

この記事では、AutoSaver という汎用コンポーネントを実装します。
シーン内の任意のノードにアタッチするだけで、

  • 一定時間ごとの自動セーブ
  • 任意のタイミング(エリア移動直後・重要イベント直後など)での手動トリガーセーブ

を簡単に実現できます。
セーブ処理の本体は「JSON文字列をローカルストレージに保存する」という形で完結しており、外部のGameManagerやシングルトンに一切依存しません
ゲーム側では「現在のゲーム状態をJSON文字列に変換してAutoSaverに渡すだけ」で利用できます。


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

要件整理

今回の AutoSaver コンポーネントに求める要件は次の通りです。

  • 外部依存なし:GameManager などのカスタムスクリプトに依存しない。
  • 汎用セーブ先:Cocos Creator の sys.localStorage を利用し、任意のキー名で JSON 文字列を保存する。
  • 自動セーブ機能
    • 一定時間間隔で自動的にセーブを実行できる。
    • 自動セーブの ON/OFF をインスペクタで切り替え可能。
  • 手動トリガー機能
    • スクリプトから triggerSave() を呼ぶだけでセーブが走る。
    • 「エリア移動直後」「イベント完了直後」など任意のタイミングで呼び出せる。
  • セーブ内容の指定
    • ゲーム側で JSON 文字列を渡して保存する saveJsonString() を提供。
    • 「最後に渡された JSON を自動セーブに再利用」できるように内部にキャッシュする。
  • 防御的実装
    • 自動セーブ間隔が短すぎる/0以下などの不正値を防ぐ。
    • 保存対象 JSON が空の場合は警告を出し、セーブをスキップする。
    • Web / ネイティブ / エディタで sys.localStorage が使える前提だが、念のためのエラーハンドリングを行う。

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

AutoSaver コンポーネントのプロパティ設計は以下の通りです。

  • saveKey: string
    • 保存に使用するローカルストレージのキー名。
    • 例: "autosave_slot_1"
    • 未設定(空文字)の場合、デフォルトで "AUTO_SAVE" を使用。
  • enableAutoSave: boolean
    • 自動セーブ機能の ON/OFF。
    • ON の場合、autoSaveInterval 秒ごとにセーブ処理が走る。
  • autoSaveInterval: number
    • 自動セーブの間隔(秒)。
    • 最小値を 1 秒に制限し、それより小さい値が設定された場合は 1 秒に補正。
    • 例: 30 にすると 30 秒ごとに自動セーブ。
  • logOnSave: boolean
    • セーブ成功時にコンソールログを出すかどうか。
    • 開発中は ON、本番ビルドでは OFF にする想定。
  • logWarnings: boolean
    • セーブ対象 JSON が空など、注意すべき状況で警告ログを出すかどうか。
  • simulateHeavySave: boolean
    • セーブ処理に疑似的な遅延を入れるかどうか(デバッグ用)。
    • ON の場合、指定時間だけ setTimeout による非同期遅延を入れる。
  • simulatedDelayMs: number
    • simulateHeavySave が ON のときに使われる、疑似遅延時間(ミリ秒)。
    • 例: 200 なら 0.2 秒だけ遅延してから保存。

さらに、ゲーム側から呼び出す公開メソッドを用意します。

  • saveJsonString(json: string): void
    • ゲーム側で用意した JSON 文字列を渡して即時セーブする。
    • 同時に、その JSON を「次回の自動セーブで使うための最新データ」として内部に保存する。
  • updateLatestJson(json: string): void
    • 「今はセーブしたくないが、最新状態だけ更新しておきたい」場合に使用。
    • 内部の最新 JSON だけ更新し、即時セーブは行わない。
  • triggerSave(): void
    • 内部に保持している最新 JSON を使って即時セーブを行う。
    • エリア移動直後や重要イベント直後などに呼び出す。
  • loadSavedJson(): string | null
    • ローカルストレージから現在の saveKey に保存されている JSON 文字列を取得する。
    • 存在しない場合は null を返す。

TypeScriptコードの実装


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

/**
 * AutoSaver
 * 
 * - 任意のノードにアタッチして使用する汎用オートセーブコンポーネント。
 * - JSON文字列を sys.localStorage に保存します。
 * - 外部GameManager等には依存せず、このコンポーネント単体で完結します。
 */
@ccclass('AutoSaver')
export class AutoSaver extends Component {

    @property({
        tooltip: 'ローカルストレージに保存する際のキー名。\n空の場合は "AUTO_SAVE" が使用されます。'
    })
    public saveKey: string = 'AUTO_SAVE';

    @property({
        tooltip: '自動セーブ機能を有効にするかどうか。\nON の場合、指定秒数ごとにセーブが実行されます。'
    })
    public enableAutoSave: boolean = true;

    @property({
        tooltip: '自動セーブの間隔(秒)。\n1秒未満の値が設定された場合は1秒に補正されます。',
        min: 0.1
    })
    public autoSaveInterval: number = 30;

    @property({
        tooltip: 'セーブ成功時にログを出力するかどうか(開発用)。'
    })
    public logOnSave: boolean = true;

    @property({
        tooltip: '警告ログ(空データのセーブ試行など)を出力するかどうか。'
    })
    public logWarnings: boolean = true;

    @property({
        tooltip: 'セーブ処理に疑似的な遅延を入れるかどうか(デバッグ用)。\nON の場合、下の遅延ミリ秒だけ待ってから保存します。'
    })
    public simulateHeavySave: boolean = false;

    @property({
        tooltip: '疑似セーブ遅延時間(ミリ秒)。\nsimulateHeavySave が ON のときのみ有効です。',
        min: 0
    })
    public simulatedDelayMs: number = 0;

    // 内部で保持する「最新のセーブ対象JSON文字列」
    private _latestJson: string = '';

    // 自動セーブ用の経過時間カウンタ
    private _elapsed: number = 0;

    onLoad() {
        // saveKey が空の場合はデフォルト値を使用
        if (!this.saveKey || this.saveKey.trim().length === 0) {
            if (this.logWarnings) {
                console.warn('[AutoSaver] saveKey が空のため、"AUTO_SAVE" を使用します。');
            }
            this.saveKey = 'AUTO_SAVE';
        }

        // 自動セーブ間隔の防御的補正
        if (this.autoSaveInterval < 1) {
            if (this.logWarnings) {
                console.warn(`[AutoSaver] autoSaveInterval が ${this.autoSaveInterval} 秒と短すぎるため、1秒に補正します。`);
            }
            this.autoSaveInterval = 1;
        }

        // シミュレート遅延の防御的補正
        if (this.simulatedDelayMs < 0) {
            if (this.logWarnings) {
                console.warn(`[AutoSaver] simulatedDelayMs が負の値 (${this.simulatedDelayMs}) のため、0 に補正します。`);
            }
            this.simulatedDelayMs = 0;
        }
    }

    start() {
        // 特に必須コンポーネントはないため、ここでは何も行いません。
        // 必要であれば、ここで過去のセーブデータをロードして
        // _latestJson に反映することもできます。
        const existing = this.loadSavedJson();
        if (existing && existing.length > 0) {
            this._latestJson = existing;
            if (this.logOnSave) {
                console.log('[AutoSaver] 既存のセーブデータを検出しました。_latestJson を初期化します。');
            }
        }
    }

    update(deltaTime: number) {
        if (!this.enableAutoSave) {
            return;
        }

        this._elapsed += deltaTime;

        if (this._elapsed >= this.autoSaveInterval) {
            this._elapsed = 0;
            this._autoSaveInternal();
        }
    }

    /**
     * ゲーム側から直接 JSON 文字列を渡してセーブを実行するメソッド。
     * 
     * @param json セーブしたいゲーム状態を表す JSON 文字列
     */
    public saveJsonString(json: string): void {
        this._latestJson = json;
        this._performSave(json);
    }

    /**
     * 最新状態の JSON 文字列だけ更新し、即時セーブは行わないメソッド。
     * 自動セーブや後からの triggerSave で使用されます。
     * 
     * @param json 最新ゲーム状態を表す JSON 文字列
     */
    public updateLatestJson(json: string): void {
        this._latestJson = json;
    }

    /**
     * 内部に保持している最新 JSON を使って即時セーブを行うメソッド。
     * エリア移動直後や重要イベント直後などに呼び出してください。
     */
    public triggerSave(): void {
        if (!this._latestJson || this._latestJson.length === 0) {
            if (this.logWarnings) {
                console.warn('[AutoSaver] triggerSave が呼ばれましたが、_latestJson が空のためセーブをスキップします。');
            }
            return;
        }
        this._performSave(this._latestJson);
    }

    /**
     * 現在の saveKey に保存されている JSON 文字列を取得します。
     * セーブデータが存在しない場合は null を返します。
     */
    public loadSavedJson(): string | null {
        try {
            const value = sys.localStorage.getItem(this.saveKey);
            return value !== null ? value : null;
        } catch (e) {
            console.error('[AutoSaver] ローカルストレージからの読み込みに失敗しました。', e);
            return null;
        }
    }

    /**
     * 自動セーブ時に内部的に呼び出される処理。
     * _latestJson が空でない場合のみセーブを実行します。
     */
    private _autoSaveInternal(): void {
        if (!this._latestJson || this._latestJson.length === 0) {
            if (this.logWarnings) {
                console.warn('[AutoSaver] 自動セーブが発火しましたが、_latestJson が空のためセーブをスキップします。');
            }
            return;
        }
        this._performSave(this._latestJson, true);
    }

    /**
     * 実際に sys.localStorage に保存を行う共通メソッド。
     * 
     * @param json 保存する JSON 文字列
     * @param isAuto 自動セーブによる呼び出しかどうか(ログメッセージ用)
     */
    private _performSave(json: string, isAuto: boolean = false): void {
        if (!json || json.length === 0) {
            if (this.logWarnings) {
                console.warn('[AutoSaver] 空の JSON が渡されたため、セーブをスキップします。');
            }
            return;
        }

        const doSave = () => {
            try {
                sys.localStorage.setItem(this.saveKey, json);
                if (this.logOnSave) {
                    const mode = isAuto ? '自動セーブ' : '手動セーブ';
                    console.log(`[AutoSaver] ${mode} 成功。key="${this.saveKey}", length=${json.length}`);
                }
            } catch (e) {
                console.error('[AutoSaver] ローカルストレージへの保存に失敗しました。', e);
            }
        };

        if (this.simulateHeavySave && this.simulatedDelayMs > 0) {
            // 疑似的な重いセーブ処理(非同期)をシミュレート
            setTimeout(doSave, this.simulatedDelayMs);
        } else {
            doSave();
        }
    }
}

コードのポイント解説

  • onLoad()
    • saveKey が空の場合にデフォルト値 "AUTO_SAVE" を設定。
    • autoSaveInterval が 1 秒未満の場合に 1 秒へ補正。
    • simulatedDelayMs が負の場合に 0 へ補正。
  • start()
    • 既に保存済みのデータがあれば loadSavedJson() で取得し、_latestJson にセット。
    • これにより、ゲーム開始時から自動セーブが同じデータを使えるようになります。
  • update(deltaTime)
    • enableAutoSave が ON のときのみ、経過時間をカウント。
    • autoSaveInterval 秒を超えたら _autoSaveInternal() を呼び出し、自動セーブを行う。
  • saveJsonString(json)
    • ゲーム側から渡された JSON を即座に保存し、同時に _latestJson を更新。
  • updateLatestJson(json)
    • 即時セーブは行わず、_latestJson だけ更新。
    • 「重要イベント中はセーブしたくないが、終了後にまとめてセーブしたい」といったケースに使えます。
  • triggerSave()
    • _latestJson が空でなければ、直近の状態を即時セーブ。
    • エリア移動完了時などに他のスクリプトから呼び出して使います。
  • _performSave(json, isAuto)
    • sys.localStorage.setItem(saveKey, json) を実行する共通処理。
    • simulateHeavySavesimulatedDelayMs によって、疑似遅延付きのセーブをシミュレート可能。
    • ログ出力は logOnSave / logWarnings のフラグによって制御。

使用手順と動作確認

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

  1. エディタ左下の Assets パネルで、保存したいフォルダ(例: assets/scripts)を右クリックします。
  2. Create > TypeScript を選択し、ファイル名を AutoSaver.ts にします。
  3. 自動生成されたテンプレートコードをすべて削除し、本記事の AutoSaver コードを丸ごと貼り付けて保存します。

2. テスト用ノードの作成とコンポーネントのアタッチ

  1. Hierarchy パネルで右クリックし、Create > Empty Node を選択して空ノードを作成します。
  2. 作成されたノードの名前を AutoSaverNode など、分かりやすい名前に変更します。
  3. Inspector パネルで、そのノードが選択されていることを確認します。
  4. Add Component ボタンをクリックし、Custom カテゴリの中から AutoSaver を選択してアタッチします。

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

AutoSaver をアタッチすると、Inspector に以下のプロパティが表示されます。開発中の例として、次のように設定してみましょう。

  • Save Key(saveKey)
    • 例: autosave_slot_1
  • Enable Auto Save(enableAutoSave)
    • ON(チェックを入れる)
  • Auto Save Interval(autoSaveInterval)
    • 例: 10 (10秒ごとに自動セーブ)
  • Log On Save(logOnSave)
    • ON(セーブ時にログを確認したい場合)
  • Log Warnings(logWarnings)
    • ON(開発中はON推奨)
  • Simulate Heavy Save(simulateHeavySave)
    • 必要に応じて ON(セーブ遅延をテストしたい場合)
  • Simulated Delay Ms(simulatedDelayMs)
    • 例: 200 (0.2秒の疑似遅延)

4. ゲーム側からの呼び出し例(テスト用スクリプト)

実際に「エリア移動直後」「重要イベント直後」にセーブするイメージを、簡単なテストスクリプトで確認してみます。

  1. Assets パネルで右クリック → Create > TypeScript を選択し、AutoSaverTester.ts を作成します。
  2. 以下のようなテストコードを貼り付けます。

import { _decorator, Component, Node } from 'cc';
import { AutoSaver } from './AutoSaver';
const { ccclass, property } = _decorator;

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

    @property({
        type: AutoSaver,
        tooltip: '同じシーン内の AutoSaver コンポーネントへの参照を設定します。'
    })
    public autoSaver: AutoSaver | null = null;

    private _areaId: number = 1;
    private _gold: number = 100;

    start() {
        if (!this.autoSaver) {
            console.error('[AutoSaverTester] autoSaver が設定されていません。Inspector から AutoSaver をアタッチしたノードを指定してください。');
            return;
        }

        // ゲーム開始時の状態を JSON にして保存
        this._saveCurrentStateImmediate();
    }

    /**
     * 「エリア移動」をシミュレートするメソッド。
     * 実際のゲームでは、エリア移動完了時のイベントなどで呼び出してください。
     */
    public simulateAreaMove() {
        this._areaId++;
        this._gold += 50;

        // 最新状態を JSON にして AutoSaver に渡し、即時セーブ
        this._saveCurrentStateImmediate();
    }

    /**
     * 「重要イベント完了」をシミュレートするメソッド。
     * 状態だけ更新し、自動セーブに任せる例。
     */
    public simulateImportantEvent() {
        this._gold += 500;

        // 最新状態だけ更新し、即時セーブは行わない
        const json = JSON.stringify({
            areaId: this._areaId,
            gold: this._gold,
            timestamp: Date.now(),
        });
        this.autoSaver?.updateLatestJson(json);
    }

    private _saveCurrentStateImmediate() {
        if (!this.autoSaver) {
            return;
        }

        const json = JSON.stringify({
            areaId: this._areaId,
            gold: this._gold,
            timestamp: Date.now(),
        });

        this.autoSaver.saveJsonString(json);
    }
}

テストスクリプトの接続手順

  1. Hierarchy で、AutoSaver をアタッチした AutoSaverNode を確認します。
  2. 同じく Hierarchy で右クリック → Create > Empty Node を選択し、AutoSaverTesterNode を作成します。
  3. AutoSaverTesterNode を選択し、Inspector の Add Component から AutoSaverTester を追加します。
  4. AutoSaverTester コンポーネントの Auto Saver プロパティ欄に、AutoSaverNode をドラッグ&ドロップして参照を設定します。

プレイモードでの動作確認

  1. エディタ上部の Play ボタンを押してゲームを実行します。
  2. コンソールログを確認し、起動時に [AutoSaver] のログが出力されていることを確認します。
  3. 実行中に、AutoSaverTestersimulateAreaMove()simulateImportantEvent()
    • デバッグ用の UI ボタンから呼ぶ
    • または Component Preview 機能やスクリプト内の scheduleOnce などで呼ぶ

    ことで、セーブログが出ることを確認します。

  4. ゲームを停止したあと、再度 Play して start() 内で既存セーブが検出されるログが出るか確認します。

Web ビルドであれば、ブラウザの開発者ツール → Application → Local Storage から、saveKey に設定したキー名で JSON が保存されていることも確認できます。


まとめ

この AutoSaver コンポーネントは、

  • 任意ノードにアタッチするだけで動作し、外部 GameManager やシングルトンに依存しません。
  • 自動セーブ間隔やログ出力をインスペクタから調整できるため、プロジェクトごとに柔軟にカスタマイズできます。
  • saveJsonString / updateLatestJson / triggerSave / loadSavedJson といった明確な API を提供しているので、
    • エリア移動直後に triggerSave()
    • 重要イベント中は updateLatestJson() だけ呼んでおき、終了時に triggerSave()
    • 定期バックアップとして enableAutoSave を ON

    といった運用が簡単に行えます。

プロジェクト内で「セーブ処理」を一箇所に閉じ込めておくことで、

  • 複数シーンにまたがるセーブ処理の重複を避けられる
  • セーブ仕様の変更(例: キー名の変更、圧縮、暗号化の追加など)を AutoSaver 内だけで完結できる

といったメリットも得られます。
このコンポーネントをベースに、暗号化やスロット管理、クラウド同期などを拡張していくことで、より高度なセーブシステムへ発展させることも容易です。

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をコピーしました!