【Cocos Creator 3.8】AutoSaverの実装:アタッチするだけで「一定間隔 or 手動トリガーでバックグラウンドセーブ」を実現する汎用スクリプト
この記事では、AutoSaver という汎用コンポーネントを実装します。
シーン内の任意のノードにアタッチするだけで、
- 一定時間ごとの自動セーブ
- 任意のタイミング(エリア移動直後・重要イベント直後など)での手動トリガーセーブ
を簡単に実現できます。
セーブ処理の本体は「JSON文字列をローカルストレージに保存する」という形で完結しており、外部のGameManagerやシングルトンに一切依存しません。
ゲーム側では「現在のゲーム状態をJSON文字列に変換してAutoSaverに渡すだけ」で利用できます。
コンポーネントの設計方針
要件整理
今回の AutoSaver コンポーネントに求める要件は次の通りです。
- 外部依存なし:GameManager などのカスタムスクリプトに依存しない。
- 汎用セーブ先:Cocos Creator の
sys.localStorageを利用し、任意のキー名で JSON 文字列を保存する。 - 自動セーブ機能:
- 一定時間間隔で自動的にセーブを実行できる。
- 自動セーブの ON/OFF をインスペクタで切り替え可能。
- 手動トリガー機能:
- スクリプトから
triggerSave()を呼ぶだけでセーブが走る。 - 「エリア移動直後」「イベント完了直後」など任意のタイミングで呼び出せる。
- スクリプトから
- セーブ内容の指定:
- ゲーム側で JSON 文字列を渡して保存する
saveJsonString()を提供。 - 「最後に渡された JSON を自動セーブに再利用」できるように内部にキャッシュする。
- ゲーム側で 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を更新。
- ゲーム側から渡された JSON を即座に保存し、同時に
- updateLatestJson(json)
- 即時セーブは行わず、
_latestJsonだけ更新。 - 「重要イベント中はセーブしたくないが、終了後にまとめてセーブしたい」といったケースに使えます。
- 即時セーブは行わず、
- triggerSave()
_latestJsonが空でなければ、直近の状態を即時セーブ。- エリア移動完了時などに他のスクリプトから呼び出して使います。
- _performSave(json, isAuto)
sys.localStorage.setItem(saveKey, json)を実行する共通処理。simulateHeavySaveとsimulatedDelayMsによって、疑似遅延付きのセーブをシミュレート可能。- ログ出力は
logOnSave/logWarningsのフラグによって制御。
使用手順と動作確認
1. スクリプトファイルの作成
- エディタ左下の Assets パネルで、保存したいフォルダ(例:
assets/scripts)を右クリックします。 - Create > TypeScript を選択し、ファイル名を
AutoSaver.tsにします。 - 自動生成されたテンプレートコードをすべて削除し、本記事の
AutoSaverコードを丸ごと貼り付けて保存します。
2. テスト用ノードの作成とコンポーネントのアタッチ
- Hierarchy パネルで右クリックし、Create > Empty Node を選択して空ノードを作成します。
- 作成されたノードの名前を
AutoSaverNodeなど、分かりやすい名前に変更します。 - Inspector パネルで、そのノードが選択されていることを確認します。
- 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. ゲーム側からの呼び出し例(テスト用スクリプト)
実際に「エリア移動直後」「重要イベント直後」にセーブするイメージを、簡単なテストスクリプトで確認してみます。
- Assets パネルで右クリック → Create > TypeScript を選択し、
AutoSaverTester.tsを作成します。 - 以下のようなテストコードを貼り付けます。
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);
}
}
テストスクリプトの接続手順
- Hierarchy で、AutoSaver をアタッチした
AutoSaverNodeを確認します。 - 同じく Hierarchy で右クリック → Create > Empty Node を選択し、
AutoSaverTesterNodeを作成します。 AutoSaverTesterNodeを選択し、Inspector の Add Component から AutoSaverTester を追加します。- AutoSaverTester コンポーネントの Auto Saver プロパティ欄に、
AutoSaverNodeをドラッグ&ドロップして参照を設定します。
プレイモードでの動作確認
- エディタ上部の Play ボタンを押してゲームを実行します。
- コンソールログを確認し、起動時に
[AutoSaver]のログが出力されていることを確認します。 - 実行中に、
AutoSaverTesterのsimulateAreaMove()やsimulateImportantEvent()を- デバッグ用の UI ボタンから呼ぶ
- または Component Preview 機能やスクリプト内の
scheduleOnceなどで呼ぶ
ことで、セーブログが出ることを確認します。
- ゲームを停止したあと、再度 Play して
start()内で既存セーブが検出されるログが出るか確認します。
Web ビルドであれば、ブラウザの開発者ツール → Application → Local Storage から、saveKey に設定したキー名で JSON が保存されていることも確認できます。
まとめ
この AutoSaver コンポーネントは、
- 任意ノードにアタッチするだけで動作し、外部 GameManager やシングルトンに依存しません。
- 自動セーブ間隔やログ出力をインスペクタから調整できるため、プロジェクトごとに柔軟にカスタマイズできます。
- saveJsonString / updateLatestJson / triggerSave / loadSavedJson といった明確な API を提供しているので、
- エリア移動直後に
triggerSave() - 重要イベント中は
updateLatestJson()だけ呼んでおき、終了時にtriggerSave() - 定期バックアップとして
enableAutoSaveを ON
といった運用が簡単に行えます。
- エリア移動直後に
プロジェクト内で「セーブ処理」を一箇所に閉じ込めておくことで、
- 複数シーンにまたがるセーブ処理の重複を避けられる
- セーブ仕様の変更(例: キー名の変更、圧縮、暗号化の追加など)を AutoSaver 内だけで完結できる
といったメリットも得られます。
このコンポーネントをベースに、暗号化やスロット管理、クラウド同期などを拡張していくことで、より高度なセーブシステムへ発展させることも容易です。




