【Rockman2 hack】C++でゲームのセーブステートを作る――壊れたデータで現在のゲームを壊さない設計

AIで作成 C/C++
AIで作成

(2026/09/12 更新)
一部文言を修正。ローカルパスの情報をそのまま書き込んでしまっていたため


こんにちわぁ(^o^)/

先日、OpenAIから新しいAIモデルが公開されましたね。
GPT-6 Astra。

何やら巷ではこの話題について盛り上がっているようです。

先日から開発を進めているファミコンロックマンのようなものも数回スクリプトを渡すだけでモドキレベルで作ってくれるとの事で、ちょっと興味深々 ( ゚Д゚)ナーーーニーーー!?

一部界隈では、課金できる人とできない人との間でAI格差が生まれていると指摘する方もいらっしゃるみたいで、
それについては確かにごもっともだなぁ、と思いました。

それについては私もここで申してマウント取ってる思われても癪なので言いませんが、
AIに限らずどこの業界にもその問題は根付いていると思う。

世の中、お金で回っている以上、綺麗事は言えないです。
私もお金配れるわけでもないですし、どうこう言う権利もないので、あまり触れないでおきます(;・∀・)

なんかこういう話ってデリカシーがどうとかって言った人間が沙汰される風潮がありますけど、それ言ってたらまあはなからお金目当てで長年努力して成功した人とか、資産家の端くれな方々は何て言うんだろうね。

金持ちでも悩みは持ってるわけで、それ言ったらキリがないと思う。
人間はないものねだりな生物なので。

知らんけど ( -‘д-)y-~


ステートセーブ

エミュレーターとか知ってる人は多分すぐピンとくる単語だと思う (´・ω・`)

ゲームを実行していて途中でブツ切りにして、また後日同じ箇所から続きをプレイしたい、って時にこの機能を使います。

つまり、ゲームの進行状態をそっくりそのまま復元できるようにするセーブ機能の事です。

例えばですけど、ゲームを普通にプレイしていて、

用事があり退出したい時。
本来のバックアップ機能は特定のシーンに達するまで使えないため、ここでシャットダウンすると進行状態は通常失われてしまいます。

そんな時に、

これを押します(・∀・)
「ステートセーブ」。

セーブデータが作成される。

別の日にゲームをプレイして、前回の部分から再開したいなぁ、となった時は、
次の「ステートロード」を選択する。

すると前回のセーブした箇所から即座にゲームを再開できる ( ノ゚∀゚)ノ
こんな感じです。

その折にこだわった点は、全ての状態を復元する事をします。

それはつまり再生中のBGMの曲の状態やSFX(効果音)の状態、敵キャラの位置や放った攻撃技の状態や数、位置など全てを即座に復元します。

Nintendo Switchとかのバーチャルコンソール(って今もあるんでしょうか・・・)でもこういった機能があると思います。

もしかしたら知ってる人は多いのかな c(-∀-*)


実装について

最初は Scene ID だけ復元されて、ステージ先頭から復帰するのみでした。
Scene ID ってのは「オープニングシーン」とか、「ステージ選択シーン」とかそういう区分の事です。

それをDTOという仕組みを使って、あらゆるオブジェクトを一か所に集約して内容を記録するように改訂。

writer.WriteBytes(kSaveMagic) &&
writer.WriteU32(config::SystemConfig::kCurrentSaveVersion) &&
writer.WriteU64(data.gameContentCompatibilityId) &&
writer.WriteI32(data.sequenceID) &&
writer.WriteI32(data.sceneID) &&
writer.WriteU32(static_cast<std::uint32_t>(data.scenePayload.size())) &&
writer.WriteU32(checksum) &&
writer.WriteBytes(data.scenePayload);

DTOってなんぞ??って感じかも分かりませんが、これはデータの容器みたいなものであると思ってくれてよいです。

これを使ってデータを保存します。

要は、ゲームの状態を復帰するための必要最低限な情報だけに絞って、それを決められたサイズの定型文にしてセーブデータの具材を作ってるわけです。

今度はそのデータを解凍して、ゲームのオブジェクトとして貼り付けて状態を形成していきます。

やがて、セーブした時の状態と瓜二つの状況が出来上がります。

そうしてゲームステート(状態)の復元が完了します。

まず、データの保存を行う時にC++オブジェクトをそのまま保存データとはせず、固定幅整数化する事でセーブデータの領域の分布を定着させる目的があり、
それらを全てに適用させる事でデータの容積を毎回図る必要もなくなります。

分かりやすい話をすると、コンパイラやアプリケーション実行bit環境に影響する int型 や size_t などを使わずに、std::uint8_t や std::int32_t などを使って明確なサイズの定義をしている、という事です。

std::uint8_t   // 1 byte
std::uint16_t  // 2 bytes
std::uint32_t  // 4 bytes
std::uint64_t  // 8 bytes
std::int32_t   // 4 bytes

これは設計上、取り扱うデータのサイズをしっかりと決めておかないといけない事を意味しています。

この情報はとても重要です(・∀・)/

例えばシーンIDを int のまま保存すると、読み書きする環境によって何バイトなのか曖昧になります。

writer.WriteI32(data.sceneID);

このように int32_t として定義すれば、

Scene IDは必ず4バイト

というファイル仕様を保証できます。

私もつい最近まで知らなかったんですけど、size_t なんかは32bitと64bitでサイズが変わるみたいです。

size_tは決まって8バイト・・・だと勝手に思ってたよf( ̄▽ ̄;)


固定長データを使っている理由

最終的に全てのデータは決まったサイズで定常的に実行されるオブジェクトとなり得ますが、そのデータはアプリを実行する中でサイズが誇大化する事を想定する必要があり、コンパイラが適切なサイズを決定しています。

しかしセーブデータとして値を保持するためには様々な問題を解決する必要があり、その中でも最も難しい障壁となっているのが、可変長配列などで取り扱っているデータの復元です。

いわゆる std::vector<T> などを例にみる可変長で定義されるデータを持ったクラスなどはそのまま記録しても復帰させる術がないため、セーブデータを固定長文字列に落とし込んで記録します。

そして問題になるのは、このデータをvector配列などに戻してやる部分です。
ここがポイントね┏( ^o^)┛

この後も技術的な話が続きます。゜(>人<)゜。

コメント

タイトルとURLをコピーしました