基于你的需求对比三种统一持久化路径的方案
+ +
+// ChunkRecord 扩展
+{
+ blockData: { ... },
+ staticEntities: [...],
+ runtimeSeedData: { ... },
+ runtimeEntities: {
+ turrets: [...],
+ zombieNests: [...],
+ minecarts: [...]
+ }
+}
+
+// 修改点(7个文件):
+1. WorldStore.js - ChunkRecord 格式 + migration
+2. WorldRuntime.js - flushBeforeUnload 写 entities
+3. PersistenceWorker - 新 action: saveEntitiesToChunk
+4. Chunk.js - loadFromRecord 读 runtimeEntities
+5. Game.js - collectSnapshot 从 worldStore 读
+6. PersistenceService - cache.entities 桥接
+7. ManualSaveWorker - 统一读取路径
+
+最小改动:扩展 ChunkRecord + 修改 flushBeforeUnload 写入 entities + 渐进迁移
+
+// 新增独立存储层
+RuntimeEntityStore {
+ saveChunkEntities(cx, cz, entities) → write IDB
+ loadChunkEntities(cx, cz) → read IDB
+ migrateFromDeltas() → progressive
+}
+
+// 修改点(9个文件):
+1. RuntimeEntityStore - 新类,独立 IDB store
+2. WorldStore.js - 委托读取 runtimeEntities
+3. WorldRuntime.js - flush 时调用 RuntimeEntityStore
+4. Chunk.js - load 时从 RuntimeEntityStore 读
+5. PersistenceService - cache → RuntimeEntityStore 桥接
+6. PersistenceWorker - 新增 entity actions
+7. Game.js - collectSnapshot 合并两处数据
+8. ManualSaveWorker - 统一读取
+9. 三个 Manager - 改为调用 RuntimeEntityStore
+
+职责分离:runtime entities 单独存储在独立 IDB store,WorldStore 仅做委托
+
+// 将 entities 作为 worldStore 的一部分
+// 通过 RegionRecord 统一写入
+{
+ region: "rx,rz",
+ chunks: {
+ "cx,cz": {
+ blockData: {...},
+ staticEntities: [...],
+ runtimeSeedData: {...},
+ runtimeEntities: {...}
+ }
+ },
+ runtimeEntities: { // region 级汇总
+ "cx,cz": { turrets: [...], ... }
+ }
+}
+
+// 修改点(6个文件):
+1. WorldStore.js - RegionRecord 格式扩展
+2. WorldRuntime.js - flush 时汇总到 region
+3. Chunk.js - load 时从 region 读取
+4. Game.js - collectSnapshot 改为 region 级
+5. ManualSaveWorker - 改为 region 级读取
+6. PersistenceService - 渐进迁移逻辑
+
+数据聚合:将 entities 汇总到 RegionRecord 层级,按 region 批量读写
+| 维度 | +A: 直接扩展 | +B: 独立抽象 | +C: Region 级 | +
|---|---|---|---|
| 修改文件数 | +7 个 | +9 个 | +6 个 | +
| 新增代码量 | +~300 行 | +~500 行 | +~250 行 | +
| 概念复杂度 | +低(扩展已有格式) | +中(新增抽象层) | +中(region 级聚合) | +
| 渐进迁移难度 | +低(chunk 级自动迁移) | +低(同上) | +中(需 region 级迁移) | +
| 向后兼容 | +好(runtimeEntities 可选) | +好 | +一般(region 格式变更) | +
| 性能影响 | +无额外 I/O | +额外一次 I/O | +region 级批量更优 | +
从一阶段到二阶段:特殊实体跨重启持久化改造
+ +解决了单次 runtime 会话内 chunk 卸载/重载的正确性问题,通过 cache 快照桥接层实现了:
+将特殊实体正式迁入 worldStore / IndexedDB,实现跨页面重启的持久化恢复。
+ +// 当前 cache.entities 结构
+{
+ "turrets": [...],
+ "zombieNests": [...],
+ "minecarts": [...]
+}
+// 仅存在于内存
+// 页面关闭即丢失
+
+扩展 ChunkRecord 格式,runtime entities 纳入 worldStore 统一管理
+// 设想的 Repository 抽象
+RuntimeEntityRepository {
+ getEntities(chunkKey, type)
+ saveEntity(chunkKey, entity)
+ removeEntity(chunkKey, id)
+ flushToStore(chunkKey)
+}
+// 统一三个 manager 的读写
+
+统一炮塔/巢穴/矿车的快照读写抽象,消除重复逻辑
+// chunk 级生命周期 +chunk.activate() → 实体加载、启动逻辑 +chunk.deactivate() → 实体停用、写入快照 + +// 当前:manager 全局持有 +// 目标:显式生命周期 ++
显式生命周期管理,评估性能收益与复杂度
+第 2/3 节:关键组件的接口变更
+ +
+flushBeforeUnload(cx, cz, blockDataSnapshot) {
+ const chunk = this.world.chunks.get(`${cx},${cz}`);
+ const record = {
+ blockData: blockDataSnapshot || chunk.blockData,
+ staticEntities: [...chunk.staticEntities],
+ runtimeSeedData: { structureCenters: chunk.structureCenters }
+ };
+ this.worldStore.putChunkRecord(cx, cz, record);
+}
+
+
+flushBeforeUnload(cx, cz, blockDataSnapshot, entitiesSnapshot) {
+ const chunk = this.world.chunks.get(`${cx},${cz}`);
+ const record = {
+ blockData: blockDataSnapshot || chunk.blockData,
+ staticEntities: [...chunk.staticEntities],
+ runtimeSeedData: { structureCenters: chunk.structureCenters },
+ runtimeEntities: entitiesSnapshot || this._collectEntitiesForChunk(cx, cz)
+ };
+ this.worldStore.putChunkRecord(cx, cz, record);
+}
+
+_collectEntitiesForChunk(cx, cz) {
+ return {
+ turrets: this.game.turretManager.getEntitiesForChunk(cx, cz),
+ zombieNests: this.game.zombieNestManager.getEntitiesForChunk(cx, cz),
+ minecarts: this.game.minecartManager.getEntitiesForChunk(cx, cz)
+ };
+}
+
+
+loadFromRecord(chunkRecord) {
+ // ... blockData 处理 ...
+
+ // 从 cache 读取 runtime entities
+ const cacheEntry = persistence.cache.get(chunkKey);
+ const entities = cacheEntry?.entities || {};
+ this.pendingRuntimeEntities = {
+ zombieNests: entities.zombieNests || [],
+ turrets: entities.turrets || [],
+ minecarts: entities.minecarts || []
+ };
+}
+
+
+loadFromRecord(chunkRecord) {
+ // ... blockData 处理 ...
+
+ // 优先从 chunkRecord 读取 runtime entities(新格式)
+ if (chunkRecord.runtimeEntities) {
+ this.pendingRuntimeEntities = {
+ zombieNests: chunkRecord.runtimeEntities.zombieNests || [],
+ turrets: chunkRecord.runtimeEntities.turrets || [],
+ minecarts: chunkRecord.runtimeEntities.minecarts || []
+ };
+ } else {
+ // 旧格式兼容:从 cache.entities 读取(渐进迁移路径)
+ const cacheEntry = persistence.cache.get(chunkKey);
+ this.pendingRuntimeEntities = {
+ zombieNests: cacheEntry?.entities?.zombieNests || [],
+ turrets: cacheEntry?.entities?.turrets || [],
+ minecarts: cacheEntry?.entities?.minecarts || []
+ };
+ // 标记需要渐进迁移
+ this._needsEntityMigration = true;
+ }
+}
+
+
+async _ensureChunkEntitiesMigrated(cx, cz, chunkRecord) {
+ if (chunkRecord.runtimeEntities) return;
+
+ const chunkKey = `${cx},${cz}`;
+ const legacyData = await persistence.workerGetChunkData(cx, cz);
+
+ if (legacyData?.entities) {
+ chunkRecord.runtimeEntities = legacyData.entities;
+
+ // 回填到 worldStore
+ this.worldStore.putChunkRecord(cx, cz, chunkRecord);
+
+ // 清理已迁移的数据(可选)
+ // await persistence.workerRemoveChunkData(cx, cz);
+
+ console.log(`[WorldRuntime] migrated runtime entities for ${chunkKey}`);
+ } else {
+ // world_deltas 中也没有,创建空结构
+ chunkRecord.runtimeEntities = {
+ turrets: [],
+ zombieNests: [],
+ minecarts: []
+ };
+ }
+}
+
+
+collectSnapshot() {
+ const worldDeltas = [];
+ for (const [key, data] of persistenceService.cache.entries()) {
+ worldDeltas.push({ key, ...data });
+ }
+ return {
+ player: { ... },
+ worldDeltas,
+ seed: this.seed,
+ settings: { ... }
+ };
+}
+
+
+collectSnapshot() {
+ const worldDeltas = [];
+ // 从 worldStore 读取已加载 chunk 的完整数据
+ for (const [key, chunk] of this.world.chunks.entries()) {
+ const record = this.world.worldStore.getChunkRecord(chunk.cx, chunk.cz);
+ worldDeltas.push({
+ key,
+ blocks: record.blockData,
+ entities: record.runtimeEntities || {}
+ });
+ }
+ return {
+ player: { ... },
+ worldDeltas,
+ seed: this.seed,
+ settings: { ... }
+ };
+}
+
+第 1/3 节:数据流设计
+ +
+Chunk unload:
+┌─────────────────────────────────────────────────┐
+│ World.update() │
+│ 1. snapshotChunkBlocks(key, blockData) │── 同步
+│ 2. stopMinecartsForChunk(cx, cz) │── 停止移动矿车
+│ 3. flushBeforeUnload(cx, cz, blockData, entities)│── 传入 entities
+│ │ │
+│ ▼ │
+│ WorldRuntime.flushBeforeUnload() │
+│ → 构建 ChunkRecord: │
+│ { │
+│ blockData: snapshot, │
+│ staticEntities: [...], │
+│ runtimeSeedData: {...}, │
+│ runtimeEntities: { ◄── 新增字段 │
+│ turrets: [...], │
+│ zombieNests: [...], │
+│ minecarts: [...] │
+│ } │
+│ } │
+│ → putChunkRecord(cx, cz, record) │
+│ │ │
+│ ▼ │
+│ WorldStore.putChunkRecord() │
+│ → RegionRecord.chunks[key] = record │
+│ → saveRegionRecord(rx, rz, region) │── IndexedDB
+└─────────────────────────────────────────────────┘
+
++Chunk load: +┌─────────────────────────────────────────────────┐ +│ Chunk.loadFromRecord(chunkRecord) │ +│ 1. ensureChunkSnapshot() │── 确保 cache 存在 +│ 2. hydrateChunkBlocks(blockData) │── cache.blocks 优先 +│ 3. _injectBlockData(effectiveBlockData) │ +│ 4. _injectStaticEntities(staticEntities) │ +│ 5. structureCenters = runtimeSeedData │ +│ │ +│ 6. 读取 runtimeEntities: │── 新增逻辑 +│ if chunkRecord.runtimeEntities: │ +│ 使用 chunkRecord.runtimeEntities │── 新格式 +│ else: │ +│ 从 cache.entities 读取 │── 旧格式兼容 +│ 渐进迁移 → 写入 chunkRecord.runtimeEntities │ +│ │ +│ 7. pendingRuntimeEntities = entities │ +│ 8. finalizeNonDeferredPhase() │ +│ → restoreNestsForChunk() │ +│ → restoreTurretsForChunk() │ +│ → restoreMinecartsForChunk() │ +└─────────────────────────────────────────────────┘ ++
+旧存档加载时: +┌─────────────────────────────────────────────────┐ +│ WorldRuntime.ensureChunkData(cx, cz) │ +│ → getChunkRecord(cx, cz) │ +│ → 从 world_regions 读取 │ +│ → 若 chunkRecord.runtimeEntities 不存在: │ +│ 1. 从 world_deltas 读取对应 chunk 的 entities │ +│ 2. chunkRecord.runtimeEntities = entities │ +│ 3. putChunkRecord(cx, cz, chunkRecord) │── 回填 +│ 4. 可选: 从 world_deltas 删除已迁移的数据 │ +│ → return chunkRecord │ +└─────────────────────────────────────────────────┘ ++
第 3/3 节:影响范围、兼容性、风险
+ +| 文件 | +变更类型 | +说明 | +
|---|---|---|
| WorldStore.js | +扩展 | +ChunkRecord 格式注释更新,不改变读取逻辑 | +
| WorldRuntime.js | +修改 | +flushBeforeUnload 新增 entities 参数 + _collectEntitiesForChunk + _ensureChunkEntitiesMigrated | +
| World.js | +修改 | +unload 路径传入 entities 参数到 flushBeforeUnload | +
| Chunk.js | +修改 | +loadFromRecord 读取 runtimeEntities + _needsEntityMigration 标记 | +
| Game.js | +修改 | +collectSnapshot 改为从 worldStore 读取 + restoreSnapshot 处理 runtimeEntities | +
| PersistenceService.js | +修改 | +cache.entities 保留为临时层,渐进迁移后不再作为权威源 | +
| ManualSaveWorker.js | +修改 | +load 路径兼容 runtimeEntities 格式的 worldDeltas | +
当前实现 vs 帧分散方案对比
+ +一帧内发生的事情(红色 = 瓶颈点,无预算限制)
+ +每帧只做少量工作,保持稳定的 16ms 帧率
+ +