近年のライブ型オープンワールドRPG(例:原神)では、
- ストーリーは基本ソロ進行
- 宝箱や探索は個別管理
- マルチでは世界が部分同期
という設計が一般的です。
しかし構造を分解すると、
技術的に同期が難しいのではなく
「永続状態を持ちすぎている」ことが問題
本記事では、
体験を損なわず、開発コストを抑えた同期フラグシステムの最小構成テンプレ
を提示します。
設計原則
原則1:世界は1本線にする
int story_progress_id;
永続分岐を持たない
NPCの生死フラグを持たない
世界状態はprogressから導出
bool isNpcVisible(int progress_id) {
return progress_id >= 3020;
}
保存しない。導出する。
原則2:宝箱は「点」でなく「面」で持つ
❌ NG(フラグ爆発)
bool chest_100234_opened;
bool chest_100235_opened;
✅ 推奨(エリア単位ビットマスク)
struct RegionChestData {
uint64_t bitmask[2]; // 128個まで
};
- エリア単位管理
- ONのみ(OFFに戻さない)
- データサイズ固定
原則3:存在と取得を分離せよ(重要)
宝箱同期で最も多いバグ:
マルチ後に宝箱が復活する問題
原因は
「存在」と「取得」を同一視していること。
🔹 宝箱の存在(セッション依存)
if (host_region_bitmask[target_bit] == 0)
spawn_chest();
→ ホスト基準
🔹 宝箱の取得(プレイヤー依存)
if (player_region_bitmask[target_bit] == 0)
allow_loot();
else
disable_loot();
→ 個人基準
なぜ復活しないのか?
- ビットはONのみ
- OFFに戻らない
- 永続データはプレイヤー単位
セッション終了後は
自分のbitmaskから世界を再構築するだけ。
クラス設計(最小構成)
永続層(保存対象)
story_progress_id
region_bitmask[]
boss_kill_time[]
以上のみ。
セッション層(揮発)
SessionWorld
visible_objects
セッションシーケンス
1. 参加
PlayerA → Server : Join
Server : buildFrom(host.progress)
2. 宝箱インタラクト
PlayerA → Server : ChestInteract(region, bit)
Server:
if A.bitmask == 0:
giveReward()
setBit(A)
セッション世界は変更しない。
3.セッション終了
PlayerA:
world = rebuildFrom(A.PlayerWorldData)
宝箱は復活しない。
「参加者未取得宝箱」問題
ケース:
- ホスト:開封済み
- 参加者:未取得
解決策は3パターン。
A:取得不可(低コスト)
仕様:
ホストが開封済みなら不可。
B:個別可視(推奨)
if (player.bitmask == 0)
showChestToPlayerOnly();
- 未取得者にだけ表示
- UX自然
- 永続増加なし
C:完全個別インスタンス(高コスト)
MMO型。今回は不要。
ワールドボス管理
撃破フラグを持たない。
int64_t boss_last_kill_time;
if (now - boss_last_kill_time > respawn_time)
spawn();
データ肥大対策
- NPC状態は保存しない
- ワールドはprogressから導出
- 宝箱はビット圧縮
- ボスはタイムスタンプ
保存は最小限。
セーブ改竄対策(最低限)
-
bitmaskはサーバー保持
-
報酬判定は必ずサーバー側
-
クライアントは参照のみ
この設計のメリット
✔ 管理フラグの増殖を防ぐ
✔ QAコストを抑えられる
✔ ライブ拡張に強い
✔ データサイズが固定化できる
適用可能なゲーム
✔ 直線型ライブRPG
✔ 分岐が少ない探索型
✔ ホスト制マルチ
✖ 分岐型ADV
✖ 大規模MMO
結論
同期が難しいのではない。
永続状態を持ちすぎていることが問題。
保存は最小限に。
あとは導出する。
それだけで
原神型ライブRPGの同期はテンプレ化できる。