From bbdea043936b32da1075d2ae55803513aeecd21f Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Mon, 28 Sep 2026 23:36:54 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E5=9C=B0=E5=9B=BE=E5=89=8D?= =?UTF-8?q?=E5=8D=8A=E9=83=A8=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dev.md | 2 +- .../client-modules/src/loader/hook.ts | 6 - .../data-base/src/map/dynamicTile.ts | 17 ++- packages-user/data-base/src/map/eventView.ts | 18 ++- packages-user/data-base/src/map/gameMap.ts | 44 ++----- packages-user/data-base/src/map/mapLayer.ts | 124 ++++++++---------- packages-user/data-base/src/map/types.ts | 42 +++--- packages-user/data-state/src/loader/hook.ts | 4 - packages/common/src/types.ts | 4 +- 9 files changed, 109 insertions(+), 152 deletions(-) diff --git a/dev.md b/dev.md index 4c5f32b..ac86178 100644 --- a/dev.md +++ b/dev.md @@ -112,7 +112,7 @@ - 语句尽量不换行,除非必要,尤其注意三元运算符与 `private readonly` 类成员。 - 极度不建议写 `getter` 和 `setter`,但在极少数场景下还是可以使用的,一般是必须要提供运算符操作方法且需要监听 `get` 和 `set` 时才允许使用。 - 方法实现中,未使用的后置参数直接不填,不得写以下划线开头的参数名。 -- 使用对象解构语法时,除非明确后续可能新增其他变量,否则不允许出现单个的对象解构,如 `const { value } = obj`,必须写成 `const value = obj.value`。 +- 使用对象解构语法时,除非明确后续可能新增其他变量,否则不允许出现单个的对象解构,如 `const { value } = obj`,必须写成 `const value = obj.value`。但对于超过一个的情况,建议使用对象解构语法,但不得出现换行。 - 换行使用 `CRLF` 格式。 - 不得将一个对象上的函数声明为一个临时变量/常量,哪怕其没有 `this` 指向问题。 - 通常情况下,一个文件应只包含一个类,但如果一个类仅是另一个类的依赖,或一个类的长度远远小于另一个类等特殊情况时,允许将多个类放在同一个文件中。 diff --git a/packages-user/client-modules/src/loader/hook.ts b/packages-user/client-modules/src/loader/hook.ts index cc3ae18..9d7da4e 100644 --- a/packages-user/client-modules/src/loader/hook.ts +++ b/packages-user/client-modules/src/loader/hook.ts @@ -3,12 +3,6 @@ import { IMotaDataLoader, IMotaDataLoaderHooks } from '@user/data-state'; import { IClientConfig } from '../common'; export class RenderLoaderHooks implements IMotaDataLoaderHooks { - constructor() {} - - awake(): void {} - - destroy(): void {} - onCoreConfigLoaded?(loader: IMotaDataLoader): Promise { const client = loader.getConfig('client'); if (!client) { diff --git a/packages-user/data-base/src/map/dynamicTile.ts b/packages-user/data-base/src/map/dynamicTile.ts index fa9cd5f..d35a05e 100644 --- a/packages-user/data-base/src/map/dynamicTile.ts +++ b/packages-user/data-base/src/map/dynamicTile.ts @@ -114,18 +114,23 @@ export class DynamicTile return save; } - /** - * 从存档恢复动态图块:先经 `set` 还原图块数字(同时重取原始图块并重建默认事件), - * 再按存档逐条覆盖事件;`num` 与 `events` 即 `IDynamicBlockSave` 的全部字段。 - */ loadState(save: Readonly): void { this.set(save.num); + const eventView = this.tileEvent(); + eventView.clear(); if (save.events) { - const eventView = this.tileEvent(); - eventView.clear(); + // 包含事件存档,那么需要读取 for (const [priority, id] of save.events) { eventView.set(priority, id); } + } else { + // 不包含事件存档,那么需要从原始数据中重建 + const raw = this.raw(); + if (!raw) return; + for (const [priority, id] of Object.entries(raw.events)) { + const p = Number(priority); + eventView.set(p, id); + } } } } diff --git a/packages-user/data-base/src/map/eventView.ts b/packages-user/data-base/src/map/eventView.ts index cd6fbc0..f2df536 100644 --- a/packages-user/data-base/src/map/eventView.ts +++ b/packages-user/data-base/src/map/eventView.ts @@ -23,13 +23,9 @@ export class LayerEventView implements ILayerEventView { * @param priority 事件优先级 */ private isEntryDirty(priority: number): boolean { - const storeHas = this.store.has(priority); - const referenceHas = this.reference.has(priority); - return ( - storeHas !== referenceHas || - (storeHas && - this.store.get(priority) !== this.reference.get(priority)) - ); + const stored = this.store.get(priority); + const ref = this.reference.get(priority); + return stored !== ref; } /** @@ -61,6 +57,14 @@ export class LayerEventView implements ILayerEventView { this.updateDirtyEntry(priority, before); } + @shouldReplay('Reseting layer event view should be replayed.') + reset(): void { + this.store.clear(); + for (const [priority, id] of this.reference) { + this.store.set(priority, id); + } + } + clear(): void { this.store.clear(); this.dirtyEntries = this.reference.size; diff --git a/packages-user/data-base/src/map/gameMap.ts b/packages-user/data-base/src/map/gameMap.ts index 6b85008..8163775 100644 --- a/packages-user/data-base/src/map/gameMap.ts +++ b/packages-user/data-base/src/map/gameMap.ts @@ -26,10 +26,10 @@ import { MapLayer } from './mapLayer'; export class GameMap extends Hookable implements IGameMap { readonly layerList: Set = new Set(); - /** 图层到图层别名映射 */ - readonly layerAliasMap: WeakMap = new WeakMap(); + readonly tileStore: ITileStore; + /** 图层别名到图层的映射 */ - readonly aliasLayerMap: Map = new Map(); + readonly aliasLayerMap: Map = new Map(); /** 背景图块 */ private backgroundTile: number = 0; @@ -48,19 +48,19 @@ export class GameMap extends Hookable implements IGameMap { constructor( public readonly state: IDataCommon, - public readonly tileStore: ITileStore, public readonly floorId: string, public width: number, public height: number ) { super(); + this.tileStore = state.tileStore; this.indexer.setWidth(width); } @shouldReplay('Adding game map layer should be replayed.') - addLayer(): IMapLayer { + addLayer(alias: string): IMapLayer { const array = new Uint32Array(this.width * this.height); - const layer = new MapLayer(array, this.width, this.height, this); + const layer = new MapLayer(array, this.width, this.height, this, alias); this.layerList.add(layer); this.forEachHook(hook => { hook.onUpdateLayer?.(this.layerList); @@ -68,18 +68,14 @@ export class GameMap extends Hookable implements IGameMap { const controller = layer.addHook(new StateMapLayerHook(this, layer)); this.layerHookMap.set(layer, controller); controller.load(); + this.aliasLayerMap.set(alias, layer); return layer; } @shouldReplay('Removing game map layer should be replayed.') removeLayer(layer: IMapLayer): void { this.layerList.delete(layer as IResizableMapLayer); - const alias = this.layerAliasMap.get(layer); - if (alias) { - const symbol = Symbol.for(alias); - this.aliasLayerMap.delete(symbol); - this.layerAliasMap.delete(layer); - } + this.aliasLayerMap.delete(layer.alias); this.forEachHook(hook => { hook.onUpdateLayer?.(this.layerList); }); @@ -93,25 +89,6 @@ export class GameMap extends Hookable implements IGameMap { return this.layerList.has(layer as IResizableMapLayer); } - setLayerAlias(layer: IMapLayer, alias: string): void { - const symbol = Symbol.for(alias); - if (this.aliasLayerMap.has(symbol)) { - logger.warn(84, alias); - return; - } - this.layerAliasMap.set(layer, alias); - this.aliasLayerMap.set(symbol, layer); - } - - getLayerByAlias(alias: string): IMapLayer | null { - const symbol = Symbol.for(alias); - return this.aliasLayerMap.get(symbol) ?? null; - } - - getLayerAlias(layer: IMapLayer): string | undefined { - return this.layerAliasMap.get(layer); - } - @shouldReplay('Resizing game map layer should be replayed.') resizeLayer( width: number, @@ -198,8 +175,9 @@ export class GameMap extends Hookable implements IGameMap { const layers = new Map(); for (const layer of this.layerList) { const save = layer.saveState(compression); - if (this.isEmptyLayerSave(save)) continue; - layers.set(layer.zIndex, save); + if (!this.isEmptyLayerSave(save)) { + layers.set(layer.zIndex, save); + } } return { background: this.backgroundTile, diff --git a/packages-user/data-base/src/map/mapLayer.ts b/packages-user/data-base/src/map/mapLayer.ts index 86e5342..28c1529 100644 --- a/packages-user/data-base/src/map/mapLayer.ts +++ b/packages-user/data-base/src/map/mapLayer.ts @@ -47,8 +47,9 @@ export class MapLayer /** 坐标到动态图块集合的映射,外层 key = y,内层 key = x */ private readonly tilePosMap: Map>> = new Map(); - /** 动态图块到其当前坐标的映射 */ - private readonly posTileMap: Map = new Map(); + /** 图层的所有已缓存位置的动态图块,目的是能够持有更新前的位置,从而在图块更新时能够更新内部存储 */ + private readonly cachedDynamics: Map = + new Map(); /** 图层脏标记 */ private layerDirty: boolean = false; /** 图层参考基准,用于存档压缩对比 */ @@ -61,7 +62,8 @@ export class MapLayer array: Uint32Array, width: number, height: number, - public readonly map: IGameMap + readonly map: IGameMap, + readonly alias: string ) { super(); this.state = map.state; @@ -78,52 +80,31 @@ export class MapLayer } /** - * 将动态图块登记到指定坐标的索引表中 + * 将动态图块登记到索引表中 * @param tile 动态图块 - * @param x 横坐标 - * @param y 纵坐标 */ - private addTileToPosMap(tile: IDynamicTile, x: number, y: number): void { - let xMap = this.tilePosMap.get(y); - if (!xMap) { - xMap = new Map(); - this.tilePosMap.set(y, xMap); - } - let set = xMap.get(x); - if (!set) { - set = new Set(); - xMap.set(x, set); - } + private addDynamicToPosMap(tile: IDynamicTile): void { + const { x, y } = tile; + const xMap = this.tilePosMap.getOrInsertComputed(y, () => new Map()); + const set = xMap.getOrInsertComputed(x, () => new Set()); set.add(tile); + // 需要使用不同引用的 locator,避免与 tile 本身的 locator 同引用导致缓存位置失效 + // 缓存位置的目的是能在动态图块位置更新时还持有旧位置,能够执行内部存储的移动 + this.cachedDynamics.set(tile, { x, y }); } /** - * 将动态图块从指定坐标的索引表中移除 + * 将动态图块从指定缓存坐标的索引表中移除 * @param tile 动态图块 * @param x 横坐标 * @param y 纵坐标 */ - private removeTileFromPosMap( - tile: IDynamicTile, - x: number, - y: number - ): void { + private removeDynamic(tile: IDynamicTile, x: number, y: number): void { + this.cachedDynamics.delete(tile); this.tilePosMap.get(y)?.get(x)?.delete(tile); } - /** - * 从两个内部映射中移除图块记录 - * @param tile 动态图块 - */ - private removeTile(tile: IDynamicTile): void { - const pos = this.posTileMap.get(tile); - if (pos) { - this.removeTileFromPosMap(tile, pos.x, pos.y); - } - this.posTileMap.delete(tile); - } - - //#region 点事件操作 + //#region 事件操作 /** * 将动态图块的事件同步回当前静态格点 @@ -431,8 +412,7 @@ export class MapLayer tileEvent.set(priority, id); } } - this.addTileToPosMap(tile, x, y); - this.posTileMap.set(tile, { x, y }); + this.addDynamicToPosMap(tile); this.forEachHook(hook => hook.onCreateDynamic?.(tile)); return tile; } @@ -463,25 +443,45 @@ export class MapLayer return tile; } + /** + * 真正执行动态到静态的转换 + * @param tile 动态图块实例 + * @param keepEvent 是否保持图块事件至静态图层 + */ + private toStatic( + tile: IDynamicTile, + keepEvent: boolean = true + ): IStaticTile | null { + this.setBlock(tile.num(), tile.x, tile.y); + const staticTile = this.getTile(tile.x, tile.y); + if (!staticTile) return null; + if (keepEvent) { + const staticEvent = staticTile.tileEvent(); + const dynamicEvent = tile.tileEvent(); + staticEvent.clear(); + for (const [priority, id] of dynamicEvent.get()) { + staticEvent.set(priority, id); + } + } + this.removeDynamic(tile, tile.x, tile.y); + this.forEachHook(hook => hook.onDeleteDynamic?.(tile)); + return staticTile; + } + @shouldReplay('Transfering dynamic tile to static should be replayed.') transferToStatic( tile: IDynamicTile, keepEvent: boolean = true ): IStaticTile | null { - const x = tile.x; - const y = tile.y; - if (x < 0 || y < 0 || x >= this.width || y >= this.height) { + const { x, y } = tile; + if (!this.inMap(x, y)) { logger.warn(128, x.toString(), y.toString()); return null; } if (this.getBlock(x, y) !== 0) { logger.warn(129, x.toString(), y.toString()); } - this.setBlock(tile.num(), x, y); - this.syncStaticEvent(tile, keepEvent); - this.removeTile(tile); - this.forEachHook(hook => hook.onDeleteDynamic?.(tile)); - return this.getTile(x, y); + return this.toStatic(tile, keepEvent); } @shouldReplay('Transfering dynamic tile to static should be replayed.') @@ -489,27 +489,17 @@ export class MapLayer tile: IDynamicTile, keepEvent: boolean = true ): IStaticTile | null { - const x = tile.x; - const y = tile.y; - if (x < 0 || y < 0 || x >= this.width || y >= this.height) { - logger.warn(128, x.toString(), y.toString()); + if (!this.inMap(tile.x, tile.y)) { + logger.warn(128, tile.x.toString(), tile.y.toString()); return null; } if (this.getBlock(tile.x, tile.y) !== 0) return null; - this.setBlock(tile.num(), x, y); - this.syncStaticEvent(tile, keepEvent); - this.removeTile(tile); - this.forEachHook(hook => hook.onDeleteDynamic?.(tile)); - return this.getTile(x, y); + return this.toStatic(tile, keepEvent); } @shouldReplay('Deleting dynamic tile should be replayed.') async deleteDynamic(tile: IDynamicTile): Promise { - if (!this.posTileMap.has(tile)) { - logger.warn(130); - return; - } - this.removeTile(tile); + this.removeDynamic(tile, tile.x, tile.y); const hooks = this.forEachHook(hook => hook.onDeleteDynamic?.(tile)); await Promise.all(hooks); } @@ -519,7 +509,7 @@ export class MapLayer } iterateDynamicTiles(): Iterable { - return this.posTileMap.keys(); + return this.cachedDynamics.keys(); } @shouldReplay('Setting map layer block direction should be replayed.') @@ -529,15 +519,15 @@ export class MapLayer @shouldReplay('Updating dynamic tile position should be replayed.') updateDynamicTile(tile: IDynamicTile): void { - const oldPos = this.posTileMap.get(tile); + const oldPos = this.cachedDynamics.get(tile); if (oldPos) { - this.removeTileFromPosMap(tile, oldPos.x, oldPos.y); + this.removeDynamic(tile, oldPos.x, oldPos.y); oldPos.x = tile.x; oldPos.y = tile.y; - this.addTileToPosMap(tile, tile.x, tile.y); + this.addDynamicToPosMap(tile); } else { - this.addTileToPosMap(tile, tile.x, tile.y); - this.posTileMap.set(tile, { x: tile.x, y: tile.y }); + this.addDynamicToPosMap(tile); + this.cachedDynamics.set(tile, { x: tile.x, y: tile.y }); } this.forEachHook(hook => hook.onUpdateDynamicPosition?.(tile)); } @@ -866,7 +856,7 @@ export class MapLayer const tiles = [...this.iterateDynamicTiles()]; for (const tile of tiles) { this.syncStaticEvent(tile, false); - this.removeTile(tile); + this.removeDynamic(tile, tile.x, tile.y); this.forEachHook(hook => hook.onDeleteDynamic?.(tile)); } } diff --git a/packages-user/data-base/src/map/types.ts b/packages-user/data-base/src/map/types.ts index 4dda8c2..88ecb47 100644 --- a/packages-user/data-base/src/map/types.ts +++ b/packages-user/data-base/src/map/types.ts @@ -66,6 +66,11 @@ export interface ILayerEventView extends IReadonlyEventView { */ delete(priority: number): void; + /** + * 将点事件重置为参考基准 + */ + reset(): void; + /** * 清空所有事件 */ @@ -152,7 +157,7 @@ export interface IDynamicTile ISaveableContent> { /** 该动态图块的移动器 */ readonly mover: IObjectMover; - /** 该动态图块所属的动态图层 */ + /** 该动态图块所属的图层 */ readonly layer: IMapLayer; /** @@ -210,7 +215,7 @@ export interface IMapLayerHooks extends IHookBase { * @param width 地图宽度 * @param height 地图高度 */ - onResize(width: number, height: number): void; + onResize?(width: number, height: number): void; /** * 当更新某个区域的图块时执行,对应于 `putMapData` 方法 @@ -219,7 +224,7 @@ export interface IMapLayerHooks extends IHookBase { * @param width 更新区域宽度 * @param height 更新区域高度 */ - onUpdateArea(x: number, y: number, width: number, height: number): void; + onUpdateArea?(x: number, y: number, width: number, height: number): void; /** * 当更新某个点的图块时执行,如果设置的图块与原先一样,则不会触发此方法,对应于 `setBlock` 方法 @@ -227,14 +232,14 @@ export interface IMapLayerHooks extends IHookBase { * @param x 更新点横坐标 * @param y 更新点纵坐标 */ - onUpdateBlock(block: number, x: number, y: number): void; + onUpdateBlock?(block: number, x: number, y: number): void; /** * 当开门时触发,返回一个 `Promise`,当相关动画执行完毕后兑现 * @param x 门横坐标 * @param y 门纵坐标 */ - onOpenDoor(x: number, y: number): Promise; + onOpenDoor?(x: number, y: number): Promise; /** * 当关门时触发,返回一个 `Promise`,当相关动画执行完毕后兑现 @@ -242,7 +247,7 @@ export interface IMapLayerHooks extends IHookBase { * @param x 门横坐标 * @param y 门纵坐标 */ - onCloseDoor(num: number, x: number, y: number): Promise; + onCloseDoor?(num: number, x: number, y: number): Promise; /** * 当动态图块被创建时触发,包括从静态图块转换为动态图块 @@ -489,6 +494,9 @@ export interface IMapLayer /** 该图层使用的朝向绑定器 */ readonly faceBinder: IRoleFaceBinder; + /** 图层别名,可以用于从 `IGameMap` 获取图层实例 */ + readonly alias: string; + /** * 设置图层的参考基准,存档时会根据参考基准进行必要的压缩处理,仅可调用一次,多次调用无效 * @param data 图层的静态地图矩阵 @@ -638,8 +646,9 @@ export interface IGameMap /** * 添加图层,使用楼层预设的宽高 + * @param alias 图层别名,用于获取图层实例 */ - addLayer(): IMapLayer; + addLayer(alias: string): IMapLayer; /** * 移除指定图层 @@ -653,25 +662,6 @@ export interface IGameMap */ hasLayer(layer: IMapLayer): boolean; - /** - * 设置图层别名 - * @param layer 图层对象 - * @param alias 图层别名 - */ - setLayerAlias(layer: IMapLayer, alias: string): void; - - /** - * 根据图层别名获取图层对象 - * @param alias 图层别名 - */ - getLayerByAlias(alias: string): IMapLayer | null; - - /** - * 获取图层对象的别名 - * @param layer 图层对象 - */ - getLayerAlias(layer: IMapLayer): string | undefined; - /** * 设置地图的参考基准,存档时会根据参考基准进行必要的压缩,仅可调用一次,多次调用无效 * @param data 地图的静态地图矩阵,键表示纵深,值表示对应图层的矩阵 diff --git a/packages-user/data-state/src/loader/hook.ts b/packages-user/data-state/src/loader/hook.ts index 4e1acac..5b2ba5e 100644 --- a/packages-user/data-state/src/loader/hook.ts +++ b/packages-user/data-state/src/loader/hook.ts @@ -3,10 +3,6 @@ import { ICoreStateCoreConfig } from '../common'; import { IMotaDataLoader, IMotaDataLoaderHooks } from './types'; export class DefaultDataLoaderHook implements IMotaDataLoaderHooks { - awake(): void {} - - destroy(): void {} - onCoreConfigLoaded(loader: IMotaDataLoader): Promise { const coreConfig = loader.getConfig('core'); if (!coreConfig) { diff --git a/packages/common/src/types.ts b/packages/common/src/types.ts index dd56daf..aa400be 100644 --- a/packages/common/src/types.ts +++ b/packages/common/src/types.ts @@ -69,13 +69,13 @@ export interface IHookBase { * 加载此钩子对象 * @param controller 钩子控制器对象 */ - awake(controller: IHookController): void; + awake?(controller: IHookController): void; /** * 摧毁此钩子对象 * @param controller 钩子控制器对象 */ - destroy(controller: IHookController): void; + destroy?(controller: IHookController): void; } export interface IHookable<