12 KiB
需求综述
当前 LayerState 只存储当前激活地图的数据,切换地图时原地图内容彻底丢失,
无法参与存档系统。为此引入 IMapStore,集中管理所有楼层的 LayerState,
并实现 ISaveableContent 接口以支持存档读档。
核心目标:
- 多楼层数据同时存在于内存中,通过 id 访问;
- 通过
active标记区分"玩家可能到达"与"无需关注"的楼层,节省存档开销; - 通过
compareWith提供参考基准,配合分级压缩大幅减少存档体积; IStateBase.layer类型由ILayerState改为IMapStore, 操作楼层必须先通过getLayerState(id)取得具体楼层。
实现思路
1. 给 LayerState 添加 active 成员
ILayerState 新增 readonly active: boolean 和 setActiveStatus(active: boolean): void
两个接口,LayerState 实现类中 active 默认为 false。
两者的关系:
ILayerState.setActiveStatus:直接操作楼层对象;IMapStore.setMapActiveStatus(id, active):通过 id 操作, 内部查找对应楼层后调用其setActiveStatus。
2. 脏数据追踪(dirty tracking)
为支持 LowCompression 和 HighCompression 的差分存档,
需要知道哪些楼层相对于参考基准是否发生了修改。
推荐方案:楼层级简单脏标记 + 存档时实际比较
在 LayerState 内部维护 private dirty: boolean = false:
- 当楼层内任意
MapLayer触发onUpdateBlock、onUpdateArea、onResize钩子时,将dirty置为true; dirty只在compareWith首次调用时根据实际数据对比结果初始化, 初始化后的 gameplay 过程中不再重置(仅置 true)。
在 saveState 时:
- 若
dirty = false,跳过该楼层(初始化后从未被触碰过); - 若
dirty = true:- LowCompression:与参考基准进行全量比较,若完全一致则跳过 (消除"改了又改回"场景下的误判),否则存储所有行;
- HighCompression:逐行与参考基准比较,只存储不一致的行。
在 loadState 时:若存档中此楼层没有任何数据(即未出现在 floors 中),
读档后将 dirty 置为 false(视为与参考基准一致)。
不在 MapLayer 内维护 dirtyRows,行级比较在 saveState 时直接对照参考基准进行。
这避免了每次 setBlock/putMapData 都更新行级标记的热路径开销,
且存档时实际比较已能消除误判,无需 probablyDirty + setInterval 或哈希方案。
存档时比较的开销:Uint32Array 内存连续,实际耗时极低,且保存操作本身是低频的, 若将来发现存档耗时问题,可考虑将比较逻辑移至 Web Worker。
3. compareWith 接口与参数类型
compareWith(ref: Map<string, Map<number, Uint32Array>>): void;
外层 Map 以楼层 id 为键,内层 Map 以图层 zIndex 为键,
值为对应图层的完整图块数组(Uint32Array,含所有行的扁平数据)。
使用此类型而非 IMapStore 的理由:接口更轻量,调用方可直接从游戏原始数据构建,
无需额外持有一个完整的 IMapStore 实例。
关于图层标识符:继续使用 zIndex,在单个楼层内 zIndex 是语义唯一的,
与已有 MapLayer.zIndex 接口保持一致。
compareWith 以首次调用为唯一基准,再次调用不更新参考(以游戏原始数据为基准,
避免存档之间产生依赖关系)。
实现步骤:
- 若
refData已存在,直接返回; - 保存
ref引用到private refData; - 遍历当前所有楼层,对每个楼层在
ref中查找对应 id:- 不存在:
dirty = true(新楼层,视为全脏); - 存在:对每个
MapLayer(按zIndex匹配)做全量比较, 若所有行与参考数据完全一致则dirty = false,否则dirty = true。
- 不存在:
4. 楼层的创建与管理
MapStore 内部以 Map<string, LayerState> 存储所有楼层。
getLayerState(id) 对不存在的 id 直接返回 null,不自动创建。
只提供一个创建接口:
createLayerState(id: string): ILayerState:创建并注册一个空白楼层 (无任何MapLayer,用户拿到后再调用addLayer配置图层结构),返回楼层对象。
注册时若 id 已存在,发出 logger 警告并覆盖。
若 compareWith 已调用后再通过上述接口新增楼层,新楼层直接视为全脏(dirty = true),
因为 refData 中不存在对应数据。
5. 存档数据格式
类型定义
/** 单个 MapLayer 的存档数据 */
interface IMapLayerSave {
readonly width: number;
readonly height: number;
/**
* key = 行索引,value = 该行完整的 Uint32Array 数据;
* NoCompression/LowCompression 时包含所有行(0 到 height - 1);
* HighCompression 时只包含与参考基准不同的行;
* 读档时,不在此 Map 中的行从参考基准还原。
*/
readonly rows: ReadonlyMap<number, Uint32Array>;
}
/** 单个楼层的存档数据 */
interface ILayerStateSave {
readonly background: number;
/**
* key = zIndex,value = 对应图层存档数据;
* 使用 Map 格式以支持图层的动态增删。
*/
readonly layers: ReadonlyMap<number, IMapLayerSave>;
}
/** 整个 MapStore 的存档数据 */
interface IMapStoreSave {
/**
* key = 楼层 id,只包含 active 的楼层;
* inactive 的楼层不写入,读档时无需处理。
*/
readonly floors: ReadonlyMap<string, ILayerStateSave>;
}
各压缩等级存储策略
| 压缩级别 | 楼层粒度 | 行粒度 |
|---|---|---|
NoCompression |
存储所有 active 楼层 | 存储该楼层所有行 |
LowCompression |
跳过 dirty = false 的楼层;dirty 楼层全量比较后仍一致的也跳过 |
存储该楼层所有行 |
HighCompression |
同 LowCompression | 只存储与参考基准不同的行 |
读档策略
读档时直接操作数组引用(通过 setMapRef),避免逐行拷贝的额外开销:
- 若参考基准(
refData)未设置,抛出 logger 错误,不进行任何读档操作; - 遍历
state.floors,对每个楼层 id:- 若当前
MapStore中不存在该 id,发出 logger 警告并跳过; - 对该楼层每个图层,先从参考基准取出对应
zIndex的数组, 将其深拷贝为新数组作为底层(确保未存档行使用参考基准值); - 再将
ILayerStateSave.layers中对应图层的rows数据写入该数组的对应行; - 调用
MapLayer.setMapRef(array)直接替换内部引用,无需额外拷贝;
- 若当前
- 对未出现在
state.floors中的 active 楼层, 从参考基准深拷贝完整数组后调用setMapRef还原,并将dirty置为false。
6. saveState / loadState 实现
根据压缩等级分别编写三个存档函数和三个读档函数,
saveState(compression) 和 loadState(state, compression) 根据 compression 分发,
无需在每个楼层的遍历循环内部判断等级:
private saveNoCompression(): IMapStoreSaveprivate saveLowCompression(): IMapStoreSaveprivate saveHighCompression(): IMapStoreSaveprivate loadNoCompression(state: IMapStoreSave): voidprivate loadLowCompression(state: IMapStoreSave): voidprivate loadHighCompression(state: IMapStoreSave): void
saveState 结果需通过 structuredClone 深拷贝后返回。
7. IMapStore 接口设计(新增到 map/types.ts)
interface IMapStore extends ISaveableContent<IMapStoreSave> {
/** 所有楼层的 id 集合 */
readonly maps: ReadonlySet<string>;
// --- 楼层访问 ---
/** 获取指定 id 的楼层状态,不存在则返回 null */
getLayerState(id: string): ILayerState | null;
/** 获取指定 id 的楼层状态,要求楼层必须是 active 的,否则返回 null */
getActiveMap(id: string): ILayerState | null;
// --- 楼层管理 ---
/** 创建并注册一个空白楼层,返回楼层状态对象 */
createLayerState(id: string): ILayerState;
// --- active 管理 ---
/** 获取指定 id 的楼层是否激活,不存在的 id 返回 false */
isMapActive(id: string): boolean;
/** 设置指定 id 楼层的激活状态 */
setMapActiveStatus(id: string, active: boolean): void;
/** 迭代所有 active 的楼层,yield [id, ILayerState] */
iterateActiveMaps(): Iterable<[string, ILayerState]>;
/** 迭代所有 inactive 的楼层,yield [id, ILayerState] */
iterateInactiveMaps(): Iterable<[string, ILayerState]>;
/** 迭代所有楼层,yield [id, ILayerState] */
iterateAllMaps(): Iterable<[string, ILayerState]>;
// --- 差分压缩基准 ---
/**
* 设置压缩参考基准,以首次调用为唯一基准,再次调用不更新。
* @param ref 外层 key = 楼层 id,内层 key = zIndex,value = 图层完整图块数据
*/
compareWith(ref: Map<string, Map<number, Uint32Array>>): void;
}
8. ILayerState 接口修改
在现有 ILayerState 上新增:
/** 此楼层是否处于激活状态 */
readonly active: boolean;
/** 设置楼层激活状态 */
setActiveStatus(active: boolean): void;
9. IStateBase 修改
将 IStateBase.layer: ILayerState 改为 IStateBase.layer: IMapStore。
涉及文件
需要引用的文件
@user/common/types.ts:ISaveableContent,SaveCompression@user/data-base/map/types.ts: 全部现有地图接口(IMapLayer,ILayerState, 等)
需要修改的文件
@user/data-base/src/map/types.ts
- 新增
IMapLayerSave接口:单个 MapLayer 存档数据格式 - 新增
ILayerStateSave接口:单个楼层存档数据格式 - 新增
IMapStoreSave接口:MapStore 整体存档数据格式 - 修改
ILayerState:新增readonly active: boolean和setActiveStatus(active: boolean): void - 修改
IMapLayer:新增setMapRef(array: Uint32Array): void - 新增
IMapStore接口:继承ISaveableContent<IMapStoreSave>, 含全部接口(见第 7 节)
@user/data-base/src/map/mapLayer.ts
@user/data-base/src/map/layerState.ts
- 新增
active: boolean = false成员:楼层激活状态 - 实现
setActiveStatus(active: boolean): void - 新增
private dirty: boolean = false成员:楼层级脏标记 - 修改
StateMapLayerHook.onUpdateArea、onUpdateBlock、onResize: 在转发钩子的同时,将state.dirty置true - 新增
isDirty(): boolean方法:返回this.dirty,供MapStore读取 - 新增
setDirty(dirty: boolean): void方法: 供MapStore.compareWith时根据实际比较结果设置
@user/data-base/src/map/mapLayer.ts
- 新增
setMapRef(array: Uint32Array): void方法: 直接替换内部图块数组引用,跳过拷贝,供MapStore读档时使用。 需确保传入数组长度与width × height匹配, 并触发必要的钩子通知(不触发onResize,应触发onUpdateArea通知全区域更新)。 在方法注释中明确标注:调用后不得再持有或修改传入的数组。
@user/data-base/src/map/mapStore.ts(新文件)
- 实现
MapStore类,实现IMapStore private mapData: Map<string, LayerState>:楼层 id 到状态对象的映射readonly maps: ReadonlySet<string>:所有楼层 id 的只读集合视图private refData: Map<string, Map<number, Uint32Array>> | null:参考基准- 实现
getLayerState、getActiveMap、createLayerState - 实现
isMapActive、setMapActiveStatus、iterateActiveMaps、iterateInactiveMaps、iterateAllMaps - 实现
compareWith - 实现
saveNoCompression、saveLowCompression、saveHighCompression - 实现
loadNoCompression、loadLowCompression、loadHighCompression - 实现
saveState(compression)和loadState(state, compression)分发
@user/data-base/src/map/index.ts
- 补充导出
mapStore.ts
@user/data-base/src/types.ts
- 将
IStateBase.layer类型由ILayerState改为IMapStore