From d36ea69066a4ac5a44e8030a08f645bef745d3eb Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Tue, 22 Sep 2026 21:11:21 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E8=B4=B4=E5=9B=BE=E5=AD=98?= =?UTF-8?q?=E5=82=A8=E6=96=B9=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages-user/client-base/src/index.ts | 6 - .../client-base/src/material/autotile.ts | 595 +++++++++--------- .../client-base/src/material/index.ts | 6 - .../client-base/src/material/manager.ts | 350 ++++++----- .../client-base/src/material/types.ts | 218 +++---- packages-user/client-base/src/types.ts | 4 +- packages-user/client-modules/src/client.ts | 11 +- packages-user/client-modules/src/core.ts | 7 +- .../client-modules/src/render/map/renderer.ts | 2 +- packages-user/client-modules/src/types.ts | 4 + packages-user/entry-client/src/create.ts | 1 - packages/common/src/logger.json | 8 + 12 files changed, 627 insertions(+), 585 deletions(-) diff --git a/packages-user/client-base/src/index.ts b/packages-user/client-base/src/index.ts index e72f62d..8be5ffa 100644 --- a/packages-user/client-base/src/index.ts +++ b/packages-user/client-base/src/index.ts @@ -1,9 +1,3 @@ -import { createMaterial } from './material'; - -export function create() { - createMaterial(); -} - export * from './load'; export * from './material'; export * from './save'; diff --git a/packages-user/client-base/src/material/autotile.ts b/packages-user/client-base/src/material/autotile.ts index a0e9358..fb17d4b 100644 --- a/packages-user/client-base/src/material/autotile.ts +++ b/packages-user/client-base/src/material/autotile.ts @@ -6,65 +6,317 @@ import { import { AutotileConnection, AutotileType, - BlockCls, IAutotileConnection, IAutotileProcessor, - IMaterialFramedData, - ITextureManager + IMaterialFramedData } from './types'; import { ICoreState } from '@user/data-state'; import { isNil } from 'lodash-es'; +import { logger } from '@motajs/common'; -interface ConnectedAutotile { +// 3x4 自动元件索引图 +// +// | <- 当上下左右都没有连接时,会使用左上角的内容,其实等同于使用 [12, 17, 42, 47] +// |-------|-------|-------| <- 当上左、上右、下左、下右有连接时,会使用右上角的内容 +// | 00 01 | 02 03 | 04 05 | +// | 06 07 | 08 09 | 10 11 | +// |-------|-------|-------| <- 分割线,上面用于控制无连接(左)以及十字连接(右),中间用于判断父子关系(特殊连接) +// | 12 13 | 14 15 | 16 17 | +// | 18 19 | 20 21 | 22 23 | +// |-------|-------|-------| <- 当仅下方有连接时,会用第二行的内容 +// | 24 25 | 26 27 | 28 29 | +// | 30 31 | 32 33 | 34 35 | +// |-------|-------|-------| <- 当上下都有连接时,会用第三行的内容 +// | 36 37 | 38 39 | 40 41 | +// | 42 43 | 44 45 | 46 47 | +// |-------|-------|-------| <- 当仅上方有连接时,会用第四行的内容 +// | | | | +// | | | | <- 左右和上下的连接会相互干扰,具体干扰方式间右上角内容的描述 +// | | | <- 当仅左方有连接时,会使用第三列的内容 +// | | <- 当左右都有连接时,会使用第二列的内容 +// | <- 当仅右方有连接时,会用第一列的内容 + +// 2x3 自动元件索引图 +// +// |-------|-------| +// | 00 01 | 02 03 | +// | 04 05 | 06 07 | +// |-------|-------| <- 分割线,上面用于控制无连接(左)以及十字连接(右),这种自动元件无法从图片获取父子关系 +// | 08 09 | 10 11 | +// | 12 13 | 14 15 | +// |-------|-------| +// | 16 17 | 18 19 | +// | 20 21 | 22 23 | +// |-------|-------| +// 此自动元件本质上是把 3x4 自动元件中间的 4x4 区域合并为了这里的 [13, 14, 17, 18] + +interface IConnectedAutotile { + /** 左上角 */ readonly lt: Readonly; + /** 右上角 */ readonly rt: Readonly; + /** 右下角 */ readonly rb: Readonly; + /** 左下角 */ readonly lb: Readonly; } -export interface IAutotileData { - /** 图像源 */ - readonly source: SizedCanvasImageSource; - /** 自动元件帧数 */ - readonly frames: number; -} - -/** 3x4 自动元件的连接映射,元组表示将对应大小的自动元件按照格子 1/4 大小切分后对应的索引位置 */ -const connectionMap3x4 = new Map(); -/** 2x3 自动元件的连接映射,元组表示将对应大小的自动元件按照格子 1/4 大小切分后对应的索引位置 */ -const connectionMap2x3 = new Map(); -/** 3x4 自动元件各方向连接的矩形映射 */ -const rectMap3x4 = new Map(); -/** 2x3 自动元件各方向连接的矩形映射 */ -const rectMap2x3 = new Map(); -/** 不重复连接映射,用于平铺自动元件,一共 48 种 */ -const distinctConnectionMap = new Map(); - export class AutotileProcessor implements IAutotileProcessor { - /** 自动元件父子关系映射,子元件 -> 父元件 */ - readonly parentMap: Map = new Map(); - /** 自动元件父子关系映射,父元件 -> 子元件列表 */ - readonly childMap: Map> = new Map(); + /** 自动元件特殊连接方式映射 */ + private readonly spec: Map> = new Map(); - constructor( - readonly manager: ITextureManager, - readonly state: ICoreState - ) {} + /** 3x4 自动元件的各方向连接索引 */ + readonly conn3x4: Map = new Map(); + /** 2x3 自动元件的各方向连接索引 */ + readonly conn2x3: Map = new Map(); + /** 不重复连接映射,用于平铺自动元件,一共 48 种 */ + readonly distinct: Map = new Map(); - private ensureChildSet(num: number) { - const set = this.childMap.get(num); - if (set) return set; - const ensure = new Set(); - this.childMap.set(num, ensure); - return ensure; + constructor(readonly state: ICoreState) { + this.conn3x4 = this.mapAutotile(AutotileType.Big3x4); + this.conn2x3 = this.mapAutotile(AutotileType.Small2x3); + this.deduplicateConnection(); } - setConnection(autotile: number, parent: number): void { - this.parentMap.set(autotile, parent); - const child = this.ensureChildSet(parent); - child.add(autotile); + /** + * 映射自动元件连接 + * @param type 自动元件类型 + */ + private mapAutotile(type: AutotileType) { + // 这些常量非常 magic,可以参考文件开头的索引注释来理解 + const h = type === AutotileType.Big3x4 ? 2 : 1; // 横向偏移因子 + const v = type === AutotileType.Big3x4 ? 12 : 4; // 纵向偏移因子 + const luo = type === AutotileType.Big3x4 ? 12 : 8; // leftup origin + const ruo = type === AutotileType.Big3x4 ? 17 : 11; // rightup origin + const ldo = type === AutotileType.Big3x4 ? 42 : 20; // leftdown origin + const rdo = type === AutotileType.Big3x4 ? 47 : 23; // rightdown origin + const luc = type === AutotileType.Big3x4 ? 4 : 2; // leftup corner + const ruc = type === AutotileType.Big3x4 ? 5 : 3; // rightup corner + const rdc = type === AutotileType.Big3x4 ? 11 : 7; // rightdown corner + const ldc = type === AutotileType.Big3x4 ? 10 : 6; // leftdown corner + + const result = new Map(); + + for (let i = 0; i <= 0b1111_1111; i++) { + // 自动元件由四个更小的矩形组合而成 + // 初始状态下,四个矩形分别处在四个角的位置 + // 而且对应角落的矩形只可能出现在每个大区块的对应角落 + + let lu = luo; // leftup + let ru = ruo; // rightup + let ld = ldo; // leftdown + let rd = rdo; // rightdown + + // 先看四个方向,最后看斜角方向 + if (i & 0b0000_0001) { + // 左侧有连接,左侧两个矩形向右偏移两个因子 + lu += h * 2; + ld += h * 2; + // 如果右侧还有连接,那么右侧矩形和左侧矩形需要移动至中间 + // 但是由于后面还处理了先右侧再左侧的情况,因此需要先向右偏移一个因子 + // 结果就是先向右移动了一个因子,在后面又向左移动了两个因子,因此相当于向左移动了一个因子 + if (i & 0b0001_0000) { + ru += h; + rd += h; + } + } + if (i & 0b0000_0100) { + // 下侧有连接,下侧两个矩形向上偏移两个因子 + ld -= v * 2; + rd -= v * 2; + if (i & 0b0100_0000) { + lu -= v; + ru -= v; + } + } + if (i & 0b0001_0000) { + // 右侧有连接,右侧矩形向左移动两个因子 + ru -= h * 2; + rd -= h * 2; + if (i & 0b0000_0001) { + lu -= h; + ld -= h; + } + } + if (i & 0b0100_0000) { + // 上侧有链接,上侧矩形向下移动两个因子 + lu += v * 2; + ru += v * 2; + if (i & 0b0000_0100) { + ld += v; + rd += v; + } + } + // 斜角 + // 如果左上仅与上和左连接 + if ((i & 0b1100_0001) === 0b0100_0001) { + lu = luc; + } + // 如果右上仅与上和右连接 + if ((i & 0b0111_0000) === 0b0101_0000) { + ru = ruc; + } + // 如果右下仅与右和下连接 + if ((i & 0b0001_1100) === 0b0001_0100) { + rd = rdc; + } + // 如果左下仅与左和下连接 + if ((i & 0b0000_0111) === 0b0000_0101) { + ld = ldc; + } + result.set(i, [lu, ru, rd, ld]); + } + + return result; } + /** + * 初始化自动元件连接配置 + */ + private deduplicateConnection() { + const usedRect: [number, number, number, number][] = []; + let flag = 0; + // 2x3 和 3x4 的自动元件连接方式一样,因此没必要映射两次 + this.conn2x3.forEach((conn, num) => { + const index = usedRect.findIndex( + used => + used[0] === conn[0] && + used[1] === conn[1] && + used[2] === conn[2] && + used[3] === conn[3] + ); + if (index === -1) { + this.distinct.set(num, flag); + usedRect.push(conn.slice() as [number, number, number, number]); + flag++; + } else { + this.distinct.set(num, index); + } + }); + } + + /** + * 获取自动元件指定连接方式在原贴图上的裁剪位置 + * @param connection 连接方式,八位二进制数字 + * @param type 自动元件类型 + * @param cw 自动元件的 tile 宽度的一半 + * @param ch 自动元件的 tile 高度的一半 + */ + private getSliceRect( + connection: number, + type: AutotileType, + hw: number, + hh: number + ): IConnectedAutotile | null { + const map = type === AutotileType.Big3x4 ? this.conn3x4 : this.conn2x3; + const data = map.get(connection); + if (!data) return null; + // 每行的切块数量,可以参考开头的注释理解其含义 + const n = type === AutotileType.Big3x4 ? 6 : 4; + const [ltd, rtd, rbd, lbd] = data; + const ltx = (ltd % n) * hw; + const lty = Math.floor(ltd / n) * hh; + const rtx = (rtd % n) * hw; + const rty = Math.floor(rtd / n) * hh; + const rbx = (rbd % n) * hw; + const rby = Math.floor(rbd / n) * hh; + const lbx = (lbd % n) * hw; + const lby = Math.floor(lbd / n) * hh; + const rect: IConnectedAutotile = { + lt: { x: ltx, y: lty, w: hw, h: hh }, + rt: { x: rtx, y: rty, w: hw, h: hh }, + rb: { x: rbx, y: rby, w: hw, h: hh }, + lb: { x: lbx, y: lby, w: hw, h: hh } + }; + return rect; + } + + /** + * 获取自动元件的单个图块尺寸 + * @param frameWidth 自动元件每帧的宽度 + * @param height 自动元件的高度 + * @param type 自动元件的类型 + */ + private getAutotileCellSize( + frameWidth: number, + height: number, + type: AutotileType + ): [width: number, height: number] { + if (type === AutotileType.Big3x4) { + if (frameWidth % 3 !== 0 || height % 4 !== 0) { + logger.warn(190, frameWidth.toString(), height.toString()); + return [0, 0]; + } + return [frameWidth / 3, height / 4]; + } else { + if (frameWidth % 2 !== 0 || height % 3 !== 0) { + logger.warn(190, frameWidth.toString(), height.toString()); + return [0, 0]; + } + return [frameWidth / 2, height / 3]; + } + } + + flatten( + source: SizedCanvasImageSource, + type: AutotileType, + frames: number + ): SizedCanvasImageSource | null { + if (source.width % frames !== 0) { + logger.warn(189, source.width.toString(), frames.toString()); + return null; + } + const { width, height } = source; + // frame width + const fw = width / frames; + // cell width, cell height + const [cw, ch] = this.getAutotileCellSize(fw, height, type); + if (cw % 2 !== 0 || ch % 2 !== 0) { + logger.warn(190, fw.toString(), height.toString()); + return null; + } + // 画到画布上 + const canvas = document.createElement('canvas'); + canvas.width = cw * frames; + canvas.height = ch * 48; + const ctx = canvas.getContext('2d'); + if (!ctx) return null; + // half width, half height + const hw = cw / 2; + const hh = ch / 2; + // 遍历每个组合 + this.distinct.forEach((index, conn) => { + const rect = this.getSliceRect(conn, type, hw, hh)!; + const { lt, rt, rb, lb } = rect; + const y = index * ch; + for (let i = 0; i < frames; i++) { + const x = i * cw; + const ox = i * fw; + // prettier-ignore + ctx.drawImage(source, lt.x + ox, lt.y, lt.w, lt.h, x, y, hw, hh); + // prettier-ignore + ctx.drawImage(source, rt.x + ox, rt.y, rt.w, rt.h, x + hw, y, hw, hh); + // prettier-ignore + ctx.drawImage(source, rb.x + ox, rb.y, rb.w, rb.h, x + hw, y + hh, hw, hh); + // prettier-ignore + ctx.drawImage(source, lb.x + ox, lb.y, lb.w, lb.h, x, y + hh, hw, hh); + } + }); + + return canvas; + } + + setConnection(autotile: number, target: number): void { + const set = this.spec.getOrInsertComputed(autotile, () => new Set()); + set.add(target); + } + + /** + * 判断地图边缘连接点 + * @param length 地图面积,也就是地图数组的总长度 + * @param index 目标位置索引 + * @param width 地图宽度 + */ private connectEdge(length: number, index: number, width: number): number { // 最高位表示左上,低位依次顺时针旋转 @@ -136,10 +388,12 @@ export class AutotileProcessor implements IAutotileProcessor { center: 0 }; } - let res: number = this.connectEdge(array.length, index, width); - const childList = this.childMap.get(block); + let res = this.connectEdge(array.length, index, width); + const spec = this.spec.get(block); // 最高位表示左上,低位依次顺时针旋转 + // 在边缘时,在地图外的部分一定会连接上,所以哪怕索引可能导致串行,也对结果没有任何影响 + // 例如在右边缘时,右侧一定连接,此时不论其与下一行的首个图块是否一个连接,都不会影响需要连接的结果 const a7 = array[index - width - 1] ?? 0; const a6 = array[index - width] ?? 0; const a5 = array[index - width + 1] ?? 0; @@ -151,7 +405,7 @@ export class AutotileProcessor implements IAutotileProcessor { // Benchmark https://www.measurethat.net/Benchmarks/Show/35271/0/convert-boolean-to-number - if (!childList || childList.size === 0) { + if (!spec || spec.size === 0) { // 不包含子元件,那么直接跟相同的连接 res |= +(a0 === block) | @@ -164,14 +418,14 @@ export class AutotileProcessor implements IAutotileProcessor { (+(a7 === block) << 7); } else { res |= - +childList.has(a0) | - (+childList.has(a1) << 1) | - (+childList.has(a2) << 2) | - (+childList.has(a3) << 3) | - (+childList.has(a4) << 4) | - (+childList.has(a5) << 5) | - (+childList.has(a6) << 6) | - (+childList.has(a7) << 7); + +spec.has(a0) | + (+spec.has(a1) << 1) | + (+spec.has(a2) << 2) | + (+spec.has(a3) << 3) | + (+spec.has(a4) << 4) | + (+spec.has(a5) << 5) | + (+spec.has(a6) << 6) | + (+spec.has(a7) << 7); } return { @@ -186,7 +440,7 @@ export class AutotileProcessor implements IAutotileProcessor { target: number, direction: AutotileConnection ): number { - const childList = this.childMap.get(center); + const childList = this.spec.get(center); if (!childList || !childList.has(target)) { return connection & ~direction; } else { @@ -194,40 +448,13 @@ export class AutotileProcessor implements IAutotileProcessor { } } - /** - * 检查贴图是否是一个自动元件 - * @param tile 贴图数据 - */ - private checkAutotile(tile: IMaterialFramedData) { - if (tile.cls !== BlockCls.Autotile) return false; - const { texture, frames } = tile; - if (texture.width !== 96 * frames) return false; - if (texture.height === 128 || texture.height === 144) return true; - else return false; - } - - render(autotile: number, connection: number): ITextureRenderable | null { - const tile = this.manager.getTile(autotile); - if (!tile) return null; - if (!this.checkAutotile(tile)) return null; - return this.renderWithoutCheck(tile, connection); - } - - renderWith( - tile: IMaterialFramedData, - connection: number - ): ITextureRenderable | null { - if (!this.checkAutotile(tile)) return null; - return this.renderWithoutCheck(tile, connection); - } - - renderWithoutCheck( + render( tile: IMaterialFramedData, connection: number ): ITextureRenderable | null { const { texture } = tile; const size = texture.height === 32 * 48 ? 32 : 48; - const index = distinctConnectionMap.get(connection); + const index = this.distinct.get(connection); if (isNil(index)) return null; const { rect } = texture.render(); return { @@ -237,22 +464,12 @@ export class AutotileProcessor implements IAutotileProcessor { } *renderAnimated( - autotile: number, - connection: number - ): Generator { - const tile = this.manager.getTile(autotile); - if (!tile) return; - yield* this.renderAnimatedWith(tile, connection); - } - - *renderAnimatedWith( tile: IMaterialFramedData, connection: number ): Generator { - if (!this.checkAutotile(tile)) return; const { texture, frames } = tile; const size = texture.height === 128 ? 32 : 48; - const index = distinctConnectionMap.get(connection); + const index = this.distinct.get(connection); if (isNil(index)) return; for (let i = 0; i < frames; i++) { yield { @@ -261,198 +478,4 @@ export class AutotileProcessor implements IAutotileProcessor { }; } } - - /** - * 将自动元件图片展平,平铺存储 48 种样式,此时可以只通过一次绘制来绘制出自动元件,不需要四次绘制 - * @param image 原始自动元件图片 - */ - static flatten(image: IAutotileData): SizedCanvasImageSource | null { - const { source, frames } = image; - if (source.width !== frames * 96) return null; - if (source.height !== 128 && source.height !== 144) return null; - const type = - source.height === 128 ? AutotileType.Big3x4 : AutotileType.Small2x3; - const size = type === AutotileType.Big3x4 ? 32 : 48; - const width = frames * size; - const height = 48 * size; - // 画到画布上 - const canvas = document.createElement('canvas'); - canvas.width = width; - canvas.height = height; - const ctx = canvas.getContext('2d'); - if (!ctx) return null; - const half = size / 2; - const map = type === AutotileType.Big3x4 ? rectMap3x4 : rectMap2x3; - const used = new Set(); - // 遍历每个组合 - distinctConnectionMap.forEach((index, conn) => { - if (used.has(conn)) return; - used.add(conn); - const { lt, rt, rb, lb } = map.get(conn)!; - const y = index * size; - for (let i = 0; i < frames; i++) { - const x = i * size; - // prettier-ignore - ctx.drawImage(source, lt.x + i * 96, lt.y, lt.w, lt.h, x, y, half, half); - // prettier-ignore - ctx.drawImage(source, rt.x + i * 96, rt.y, rt.w, rt.h, x + half, y, half, half); - // prettier-ignore - ctx.drawImage(source, rb.x + i * 96, rb.y, rb.w, rb.h, x + half, y + half, half, half); - // prettier-ignore - ctx.drawImage(source, lb.x + i * 96, lb.y, lb.w, lb.h, x, y + half, half, half); - } - }); - - return canvas; - } -} - -/** - * 映射自动元件连接 - * @param target 输出映射对象 - * @param mode 自动元件类型,1 表示 3x4,2 表示 2x3 - */ -function mapAutotile( - target: Map, - mode: 1 | 2 -) { - const h = mode === 1 ? 2 : 1; // 横向偏移因子 - const v = mode === 1 ? 12 : 4; // 纵向偏移因子 - const luo = mode === 1 ? 12 : 8; // leftup origin - const ruo = mode === 1 ? 17 : 11; // rightup origin - const ldo = mode === 1 ? 42 : 20; // leftdown origin - const rdo = mode === 1 ? 47 : 23; // rightdown origin - const luc = mode === 1 ? 4 : 2; // leftup corner - const ruc = mode === 1 ? 5 : 3; // rightup corner - const rdc = mode === 1 ? 11 : 7; // rightdown corner - const ldc = mode === 1 ? 10 : 6; // leftdown corner - - for (let i = 0; i <= 0b1111_1111; i++) { - // 自动元件由四个更小的矩形组合而成 - // 初始状态下,四个矩形分别处在四个角的位置 - // 而且对应角落的矩形只可能出现在每个大区块的对应角落 - - let lu = luo; // leftup - let ru = ruo; // rightup - let ld = ldo; // leftdown - let rd = rdo; // rightdown - - // 先看四个方向,最后看斜角方向 - if (i & 0b0000_0001) { - // 左侧有连接,左侧两个矩形向右偏移两个因子 - lu += h * 2; - ld += h * 2; - // 如果右侧还有连接,那么右侧矩形和左侧矩形需要移动至中间 - // 但是由于后面还处理了先右侧再左侧的情况,因此需要先向右偏移一个因子 - // 结果就是先向右移动了一个因子,在后面又向左移动了两个因子,因此相当于向左移动了一个因子 - if (i & 0b0001_0000) { - ru += h; - rd += h; - } - } - if (i & 0b0000_0100) { - // 下侧有连接,下侧两个矩形向上偏移两个因子 - ld -= v * 2; - rd -= v * 2; - if (i & 0b0100_0000) { - lu -= v; - ru -= v; - } - } - if (i & 0b0001_0000) { - // 右侧有连接,右侧矩形向左移动两个因子 - ru -= h * 2; - rd -= h * 2; - if (i & 0b0000_0001) { - lu -= h; - ld -= h; - } - } - if (i & 0b0100_0000) { - // 上侧有链接,上侧矩形向下移动两个因子 - lu += v * 2; - ru += v * 2; - if (i & 0b0000_0100) { - ld += v; - rd += v; - } - } - // 斜角 - // 如果左上仅与上和左连接 - if ((i & 0b1100_0001) === 0b0100_0001) { - lu = luc; - } - // 如果右上仅与上和右连接 - if ((i & 0b0111_0000) === 0b0101_0000) { - ru = ruc; - } - // 如果右下仅与右和下连接 - if ((i & 0b0001_1100) === 0b0001_0100) { - rd = rdc; - } - // 如果左下仅与左和下连接 - if ((i & 0b0000_0111) === 0b0000_0101) { - ld = ldc; - } - target.set(i, [lu, ru, rd, ld]); - } -} - -export function createAutotile() { - mapAutotile(connectionMap3x4, 1); - mapAutotile(connectionMap2x3, 2); - - connectionMap3x4.forEach((data, connection) => { - const [ltd, rtd, rbd, lbd] = data; - const ltx = (ltd % 6) * 16; - const lty = Math.floor(ltd / 6) * 16; - const rtx = (rtd % 6) * 16; - const rty = Math.floor(rtd / 6) * 16; - const rbx = (rbd % 6) * 16; - const rby = Math.floor(rbd / 6) * 16; - const lbx = (lbd % 6) * 16; - const lby = Math.floor(lbd / 6) * 16; - rectMap3x4.set(connection, { - lt: { x: ltx, y: lty, w: 16, h: 16 }, - rt: { x: rtx, y: rty, w: 16, h: 16 }, - rb: { x: rbx, y: rby, w: 16, h: 16 }, - lb: { x: lbx, y: lby, w: 16, h: 16 } - }); - }); - connectionMap2x3.forEach((data, connection) => { - const [ltd, rtd, rbd, lbd] = data; - const ltx = (ltd % 4) * 24; - const lty = Math.floor(ltd / 4) * 24; - const rtx = (rtd % 4) * 24; - const rty = Math.floor(rtd / 4) * 24; - const rbx = (rbd % 4) * 24; - const rby = Math.floor(rbd / 4) * 24; - const lbx = (lbd % 4) * 24; - const lby = Math.floor(lbd / 4) * 24; - rectMap2x3.set(connection, { - lt: { x: ltx, y: lty, w: 24, h: 24 }, - rt: { x: rtx, y: rty, w: 24, h: 24 }, - rb: { x: rbx, y: rby, w: 24, h: 24 }, - lb: { x: lbx, y: lby, w: 24, h: 24 } - }); - }); - const usedRect: [number, number, number, number][] = []; - let flag = 0; - // 2x3 和 3x4 的自动元件连接方式一样,因此没必要映射两次 - connectionMap2x3.forEach((conn, num) => { - const index = usedRect.findIndex( - used => - used[0] === conn[0] && - used[1] === conn[1] && - used[2] === conn[2] && - used[3] === conn[3] - ); - if (index === -1) { - distinctConnectionMap.set(num, flag); - usedRect.push(conn.slice() as [number, number, number, number]); - flag++; - } else { - distinctConnectionMap.set(num, index); - } - }); } diff --git a/packages-user/client-base/src/material/index.ts b/packages-user/client-base/src/material/index.ts index a5dd6a5..628014c 100644 --- a/packages-user/client-base/src/material/index.ts +++ b/packages-user/client-base/src/material/index.ts @@ -1,9 +1,3 @@ -import { createAutotile } from './autotile'; - -export function createMaterial() { - createAutotile(); -} - export * from './autotile'; export * from './builder'; export * from './manager'; diff --git a/packages-user/client-base/src/material/manager.ts b/packages-user/client-base/src/material/manager.ts index f49aaf4..468c44b 100644 --- a/packages-user/client-base/src/material/manager.ts +++ b/packages-user/client-base/src/material/manager.ts @@ -2,7 +2,6 @@ import { ITexture, ITextureComposedData, ITextureRenderable, - ITextureSplitter, ITextureStore, SizedCanvasImageSource, Texture, @@ -11,36 +10,38 @@ import { TextureStore } from '@motajs/render'; import { - IBlockIdentifier, IMaterialData, ITextureManager, IIndexedIdentifier, IMaterialAssetData, - BlockCls, IAssetBuilder, IMaterialFramedData, - ITrackedAssetData + ITrackedAssetData, + IAutotileProcessor, + AutotileType } from './types'; import { ICoreState } from '@user/data-state'; import { logger } from '@motajs/common'; -import { getClsByString, getTextureFrame } from './utils'; import { isNil } from 'lodash-es'; import { AssetBuilder } from './builder'; import { AutotileProcessor } from './autotile'; +import { ITileStore, TileType } from '@user/data-common'; -interface TilesetCache { +interface ITilesetCache { /** 是否已经在贴图库中存在 */ readonly existed: boolean; /** 贴图对象 */ readonly texture: ITexture; } -export class MaterialManager implements ITextureManager { +export class TextureManager implements ITextureManager { + readonly autotile: IAutotileProcessor; + + readonly textures: ITextureStore = new TextureStore(); readonly tileStore: ITextureStore = new TextureStore(); readonly tilesetStore: ITextureStore = new TextureStore(); readonly imageStore: ITextureStore = new TextureStore(); readonly assetStore: ITextureStore = new TextureStore(); - readonly textures: ITextureStore = new TextureStore(); /** 自动元件图像源映射 */ readonly autotileSource: Map = new Map(); @@ -52,19 +53,21 @@ export class MaterialManager implements ITextureManager { /** 带有脏标记追踪的图集对象 */ readonly trackedAsset: ITrackedAssetData; - /** tileset 中 `Math.floor(id / 10000) + 1` 映射到 tileset 对应索引的映射,用于处理图块超出 10000 的 tileset */ + /** tileset 中的偏移索引映射,可以处理超出 Tileset 图块数量单元的 Tileset */ readonly tilesetOffsetMap: Map = new Map(); /** 图集打包器 */ readonly assetBuilder: IAssetBuilder; - /** 图块 id 到图块数字的映射 */ - readonly idNumMap: Map = new Map(); - /** 图块数字到图块 id 的映射 */ - readonly numIdMap: Map = new Map(); - /** 图块数字到图块类型的映射 */ - readonly clsMap: Map = new Map(); /** 图块的默认帧数 */ readonly defaultFrames: Map = new Map(); + /** 每个图块的总帧数 */ + readonly frames: Map = new Map(); + /** 每个图块的帧偏移量 */ + readonly tileOffsets: Map = new Map(); + /** 每个 Tileset 的 tile 尺寸 */ + readonly tilesetCells: Map = new Map(); + /** 自动元件类型映射 */ + readonly autotileType: Map = new Map(); /** 网格切分器 */ readonly gridSplitter: TextureGridSplitter = new TextureGridSplitter(); @@ -78,120 +81,151 @@ export class MaterialManager implements ITextureManager { /** 是否已经构建过素材 */ private built: boolean = false; - constructor(readonly state: ICoreState) { + constructor( + readonly state: ICoreState, + readonly tiles: ITileStore, + readonly tilesetReserve: number, + readonly tilesetUnit: number + ) { + this.autotile = new AutotileProcessor(state); this.assetBuilder = new AssetBuilder(this, state); this.assetBuilder.pipe(this.assetStore); this.trackedAsset = this.assetBuilder.tracked(); } /** - * 添加由分割器和图块映射组成的图像源贴图 - * @param source 图像源 - * @param map 图块 id 与图块数字映射 - * @param store 要添加至的贴图存储对象 - * @param splitter 使用的分割器 - * @param splitterData 传递给分割器的数据 - * @param processTexture 对每个纹理进行处理 + * 添加经由分割器分割的贴图素材 + * @param splitted 分割器分割后的图像列表 + * @param map 图块的图块数字映射数组 + * @param frames 此素材中每一个贴图使用的帧率 + * @param offsets 此素材中每一个贴图的帧偏移量 */ - private addMappedSource( - source: SizedCanvasImageSource, - map: ArrayLike, - store: ITextureStore, - splitter: ITextureSplitter, - splitterData: T, - processTexture?: (tex: ITexture) => void + private addSplittedSource( + splitted: Iterable, + map: ArrayLike, + frames: number, + offsets: number ): Iterable { - const tex = new Texture(source); - const textures = [...splitter.split(tex, splitterData)]; - if (textures.length !== map.length) { - logger.warn(75, textures.length.toString(), map.length.toString()); - } - const res: IMaterialData[] = textures.map((v, i) => { - if (!map[i]) { + return [...splitted].map((v, i) => { + if (isNil(map[i])) { return { - store, + store: this.tileStore, texture: v, identifier: -1, alias: '@internal-unknown' }; } - const { id, num, cls } = map[i]; - store.addTexture(num, v); - store.alias(num, id); - this.clsMap.set(num, getClsByString(cls)); - processTexture?.(v); + const num = map[i]; + const id = this.tiles.id(num); + if (isNil(id)) { + logger.warn(184, num.toString()); + return { + store: this.tileStore, + texture: v, + identifier: -1, + alias: '@internal-unknown' + }; + } + this.tileStore.addTexture(num, v); + this.tileStore.alias(num, id); + this.frames.set(num, frames); + this.tileOffsets.set(num, offsets); const data: IMaterialData = { - store, + store: this.tileStore, texture: v, identifier: num, alias: id }; return data; }); - return res; } addGrid( source: SizedCanvasImageSource, - map: ArrayLike + map: ArrayLike, + width: number, + height: number ): Iterable { - return this.addMappedSource( - source, - map, - this.tileStore, - this.gridSplitter, - [32, 32] - ); + const tex = new Texture(source); + const textures = [...this.gridSplitter.split(tex, [width, height])]; + if (textures.length !== map.length) { + logger.warn(75, textures.length.toString(), map.length.toString()); + } + return this.addSplittedSource(textures, map, 1, width); } addRowAnimate( source: SizedCanvasImageSource, - map: ArrayLike, + map: ArrayLike, + width: number, height: number ): Iterable { - return this.addMappedSource( - source, - map, - this.tileStore, - this.rowSplitter, - height - ); + if (source.width % width !== 0) { + logger.warn(185, source.width.toString(), width.toString()); + return []; + } + const tex = new Texture(source); + const textures = [...this.rowSplitter.split(tex, height)]; + if (textures.length !== map.length) { + logger.warn(75, textures.length.toString(), map.length.toString()); + } + const frames = source.width / width; + return this.addSplittedSource(textures, map, frames, width); } addAutotile( source: SizedCanvasImageSource, - identifier: IBlockIdentifier + num: number, + type: AutotileType ): void { - this.autotileSource.set(identifier.num, source); - this.tileStore.alias(identifier.num, identifier.id); - this.clsMap.set(identifier.num, BlockCls.Autotile); + const id = this.tiles.id(num); + if (isNil(id)) { + logger.warn(184, num.toString()); + return; + } + this.autotileSource.set(num, source); + this.tileStore.alias(num, id); + this.autotileType.set(num, type); } addTileset( source: SizedCanvasImageSource, - identifier: IIndexedIdentifier + identifier: IIndexedIdentifier, + cellWidth: number, + cellHeight: number ): IMaterialData | null { const tex = new Texture(source); this.tilesetStore.addTexture(identifier.index, tex); this.tilesetStore.alias(identifier.index, identifier.alias); - const width = Math.floor(source.width / 32); - const height = Math.floor(source.height / 32); + if ( + source.width % cellWidth !== 0 || + source.height % cellHeight !== 0 + ) { + logger.warn( + 186, + source.width.toString(), + source.height.toString(), + cellWidth.toString(), + cellHeight.toString() + ); + } + // 计算 tile 数量,溢出部分不算 + const width = Math.floor(source.width / cellWidth); + const height = Math.floor(source.height / cellHeight); const count = width * height; - const offset = Math.ceil(count / 10000); + // 一个 tileset 可能不止 tilesetUnit 个图块,需要计算偏移 + const offset = Math.ceil(count / this.tilesetUnit); if (identifier.index === 0) { this.tilesetOffsetMap.set(0, 0); this.nowTilesetIndex = 0; this.nowTilesetOffset = offset; } else { + // 不允许不按顺序追加,因为这会导致图块数字难以维护 if (identifier.index - 1 !== this.nowTilesetIndex) { logger.warn(78); return null; } - // 一个 tileset 可能不止 10000 个图块,需要计算偏移 - const width = Math.floor(source.width / 32); - const height = Math.floor(source.height / 32); - const count = width * height; - const offset = Math.ceil(count / 10000); + // 这个 tileset 所包含的所有单位,都应该映射至当前 tileset,所以循环设置这期间的所有映射 const end = this.nowTilesetOffset + offset; for (let i = this.nowTilesetOffset; i < end; i++) { this.tilesetOffsetMap.set(i, identifier.index); @@ -205,6 +239,7 @@ export class MaterialManager implements ITextureManager { identifier: identifier.index, alias: identifier.alias }; + this.tilesetCells.set(identifier.index, [cellWidth, cellHeight]); return data; } @@ -232,31 +267,43 @@ export class MaterialManager implements ITextureManager { return this.defaultFrames.get(identifier) ?? -1; } - getTile(identifier: number): Readonly | null { - if (identifier < 10000) { - const cls = this.clsMap.get(identifier) ?? BlockCls.Unknown; - if ( - cls === BlockCls.Autotile && - this.autotileSource.has(identifier) - ) { - this.cacheAutotile(identifier); + getFrameCount(num: number): number { + return this.frames.get(num) ?? 0; + } + + getTile(num: number): Readonly | null { + if (num < this.tilesetReserve) { + const type = this.tiles.getType(num); + if (type === TileType.Autotile && this.autotileSource.has(num)) { + this.cacheAutotile(num); } - const texture = this.tileStore.getTexture(identifier); + + const texture = this.tileStore.getTexture(num); if (!texture) return null; + + const frames = this.frames.get(num); + const offset = this.tileOffsets.get(num); + + if (isNil(frames) || isNil(offset)) { + logger.warn(187); + return null; + } + return { texture, - cls, - offset: 32, - frames: getTextureFrame(cls, texture), - defaultFrame: this.defaultFrames.get(identifier) ?? -1 + tileType: type, + offset, + frames, + defaultFrame: this.defaultFrames.get(num) ?? -1 }; } else { - const texture = this.cacheTileset(identifier); + const texture = this.cacheTileset(num); if (!texture) return null; return { texture, - cls: BlockCls.Tileset, - offset: 32, + tileType: TileType.Tileset, + // Tileset 不需要偏移量 + offset: 0, frames: 1, defaultFrame: -1 }; @@ -289,28 +336,34 @@ export class MaterialManager implements ITextureManager { return this.imageStore.fromAlias(alias); } - private getTilesetOwnTexture(identifier: number): TilesetCache | null { - const texture = this.tileStore.getTexture(identifier); + private getTilesetOwnTexture(num: number): ITilesetCache | null { + const texture = this.tileStore.getTexture(num); if (texture) return { existed: true, texture }; // 如果 tileset 不存在,那么执行缓存操作 - const offset = Math.floor(identifier / 10000); - const index = this.tilesetOffsetMap.get(offset - 1); + const adjustedNum = num - this.tilesetReserve; + const offset = Math.floor(adjustedNum / this.tilesetUnit); + const index = this.tilesetOffsetMap.get(offset); if (isNil(index)) return null; // 获取对应的 tileset 贴图 const tileset = this.tilesetStore.getTexture(index); - if (!tileset) return null; - // 计算图块位置 - const rest = identifier - offset * 10000; + const cell = this.tilesetCells.get(index); + if (!tileset || !cell) return null; + // 计算图块位置,首先计算没有偏移时图块的索引,即假如 Tileset 的左上角是 0,计算 num 的索引应该是什么 + const unoffset = adjustedNum - offset * this.tilesetUnit; const { width, height } = tileset; - const tileWidth = Math.floor(width / 32); - const tileHeight = Math.floor(height / 32); + const [cellWidth, cellHeight] = cell; + const tileWidth = Math.floor(width / cellWidth); + const tileHeight = Math.floor(height / cellHeight); // 如果图块位置超出了贴图范围 - if (rest > tileWidth * tileHeight) return null; + if (unoffset > tileWidth * tileHeight) { + logger.warn(188, num.toString()); + return null; + } // 裁剪 tileset,生成贴图 - const x = rest % tileWidth; - const y = Math.floor(rest / tileWidth); + const x = unoffset % tileWidth; + const y = Math.floor(unoffset / tileWidth); const newTexture = new Texture(tileset.source); - newTexture.clip(x * 32, y * 32, 32, 32); + newTexture.clip(x * cellWidth, y * cellHeight, cellWidth, cellHeight); return { existed: false, texture: newTexture }; } @@ -356,8 +409,6 @@ export class MaterialManager implements ITextureManager { if (existed) return texture; // 缓存贴图 this.tileStore.addTexture(identifier, texture); - this.idNumMap.set(`X${identifier}`, identifier); - this.numIdMap.set(identifier, `X${identifier}`); const data = this.assetBuilder.addTexture(texture); texture.toAsset(data); this.checkAssetDirty(data); @@ -377,8 +428,6 @@ export class MaterialManager implements ITextureManager { if (existed) return; toAdd.push(texture); this.tileStore.addTexture(v, texture); - this.idNumMap.set(`X${v}`, v); - this.numIdMap.set(v, `X${v}`); }); const data = this.assetBuilder.addTextureList(toAdd); @@ -390,38 +439,41 @@ export class MaterialManager implements ITextureManager { /** * 获取自动元件展开后的图片,如果图片不存在,或是已经展开并存储至了 `tileStore`,那么返回 `null` - * @param identifier 自动元件标识符 + * @param num 自动元件数字 */ - private getFlattenedAutotile( - identifier: number - ): SizedCanvasImageSource | null { - const cls = this.clsMap.get(identifier); - if (cls !== BlockCls.Autotile) return null; - if (this.tileStore.getTexture(identifier)) return null; - const source = this.autotileSource.get(identifier); + private getFlattenedAutotile(num: number): SizedCanvasImageSource | null { + const type = this.tiles.getType(num); + if (type !== TileType.Autotile) return null; + if (this.tileStore.getTexture(num)) return null; + const source = this.autotileSource.get(num); if (!source) return null; - const frames = source.width === 96 ? 1 : 4; - const flattened = AutotileProcessor.flatten({ source, frames }); + const autotileType = this.autotileType.get(num); + const frames = this.frames.get(num); + if (isNil(autotileType) || isNil(frames)) { + logger.warn(191); + return null; + } + const flattened = this.autotile.flatten(source, autotileType, frames); if (!flattened) return null; return flattened; } - cacheAutotile(identifier: number): ITexture | null { - const flattened = this.getFlattenedAutotile(identifier); + cacheAutotile(num: number): ITexture | null { + const existed = this.tileStore.getTexture(num); + if (existed) return existed; + const flattened = this.getFlattenedAutotile(num); if (!flattened) return null; const tex = new Texture(flattened); - this.tileStore.addTexture(identifier, tex); + this.tileStore.addTexture(num, tex); const data = this.assetBuilder.addTexture(tex); tex.toAsset(data); - this.autotileSource.delete(identifier); + this.autotileSource.delete(num); this.checkAssetDirty(data); return tex; } - cacheAutotileList( - identifierList: Iterable - ): Iterable { - const arr = [...identifierList]; + cacheAutotileList(list: Iterable): Iterable { + const arr = [...list]; const toAdd: ITexture[] = []; arr.forEach(v => { @@ -500,45 +552,31 @@ export class MaterialManager implements ITextureManager { return this.assetDataStore.get(id) ?? null; } - private getTextureOf(identifier: number, cls: BlockCls): ITexture | null { - if (cls === BlockCls.Unknown) return null; - if (cls !== BlockCls.Tileset) { - return this.tileStore.getTexture(identifier); + private getTextureOf(num: number): ITexture | null { + const existed = this.tileStore.getTexture(num); + if (existed) return existed; + if (num >= this.tilesetReserve) { + return this.cacheTileset(num); + } else { + const type = this.tiles.getType(num); + if (type === TileType.Autotile) { + return this.cacheAutotile(num); + } else { + return null; + } } - if (identifier < 10000) return null; - return this.cacheTileset(identifier); } - getRenderable(identifier: number): ITextureRenderable | null { - const cls = this.clsMap.get(identifier); - if (isNil(cls)) return null; - const texture = this.getTextureOf(identifier, cls); + getRenderable(num: number): ITextureRenderable | null { + const texture = this.getTextureOf(num); if (!texture) return null; return texture.render(); } getRenderableByAlias(alias: string): ITextureRenderable | null { - const identifier = this.idNumMap.get(alias); - if (isNil(identifier)) return null; - return this.getRenderable(identifier); - } - - getBlockCls(identifier: number): BlockCls { - return this.clsMap.get(identifier) ?? BlockCls.Unknown; - } - - getBlockClsByAlias(alias: string): BlockCls { - const id = this.idNumMap.get(alias); - if (isNil(id)) return BlockCls.Unknown; - return this.clsMap.get(id) ?? BlockCls.Unknown; - } - - getIdentifierByAlias(alias: string): number | undefined { - return this.idNumMap.get(alias); - } - - getAliasByIdentifier(identifier: number): string | undefined { - return this.numIdMap.get(identifier); + const num = this.tiles.num(alias); + if (isNil(num)) return null; + return this.getRenderable(num); } assetContainsTexture(texture: ITexture): boolean { diff --git a/packages-user/client-base/src/material/types.ts b/packages-user/client-base/src/material/types.ts index 7c8f0e1..0f47428 100644 --- a/packages-user/client-base/src/material/types.ts +++ b/packages-user/client-base/src/material/types.ts @@ -6,23 +6,13 @@ import { ITextureStore, SizedCanvasImageSource } from '@motajs/render'; +import { ITileStore, TileType } from '@user/data-common'; import { ICoreStateExtended } from '@user/data-state'; -export const enum BlockCls { - Unknown, - Terrains, - Animates, - Enemys, - Npcs, - Items, - Enemy48, - Npc48, - Tileset, - Autotile -} - export const enum AutotileType { + /** 2x3 大小的自动元件 */ Small2x3, + /** 3x4 大小的自动元件 */ Big3x4 } @@ -85,8 +75,8 @@ export interface IAutotileConnection { export interface IMaterialFramedData { /** 贴图对象 */ texture: ITexture; - /** 图块类型 */ - cls: BlockCls; + /** 贴图对应图块的图块类型 */ + tileType: TileType; /** 贴图总帧数 */ frames: number; /** 每帧的横向偏移量 */ @@ -102,12 +92,21 @@ export interface IMaterialAsset } export interface IAutotileProcessor extends ICoreStateExtended { - /** 该自动元件处理器使用的素材管理器 */ - readonly manager: ITextureManager; + /** + * 展平自动元件,纵向排列 48 种组合 + * @param source 图像源 + * @param type 自动元件的类型 + * @param frames 自动元件的帧数 + */ + flatten( + source: SizedCanvasImageSource, + type: AutotileType, + frames: number + ): SizedCanvasImageSource | null; /** - * 设置一个自动元件的特殊连接方式,设置后当前自动元件将会单方面与目标元件连接, - * 一个自动元件可以与多个自动元件有特殊连接 + * 设置一个自动元件的特殊连接方式,设置后当前自动元件将会单方面与目标图块连接, + * 一个自动元件可以与多个图块有特殊连接 * @param autotile 自动元件 * @param target 当前自动元件将会连接至的自动元件 */ @@ -142,52 +141,22 @@ export interface IAutotileProcessor extends ICoreStateExtended { /** * 根据图块数字,获取指定自动元件经过连接的可渲染对象 - * @param autotile 自动元件的图块数字 + * @param tile 自动元件的图块信息 * @param connection 连接方式,上方连接是第一位,顺时针旋转位次依次升高 * @returns 连接方式的可渲染对象,可以通过偏移量依次获取其他帧 */ - render(autotile: number, connection: number): ITextureRenderable | null; - - /** - * 根据图块贴图对象,获取指定自动元件经过连接的可渲染对象 - * @param tile 自动元件的图块贴图数据 - * @param connection 连接方式,上方连接是第一位,顺时针旋转位次依次升高 - * @returns 连接方式的可渲染对象,可以通过偏移量依次获取其他帧 - */ - renderWith( - tile: Readonly, + render( + tile: IMaterialFramedData, connection: number ): ITextureRenderable | null; /** - * 根据图块贴图对象,获取指定自动元件经过连接的可渲染对象,但是会假设传入的图块就是自动元件,不做不必要的判断 - * @param tile 自动元件的图块贴图数据 - * @param connection 连接方式,上方连接是第一位,顺时针旋转位次依次升高 - * @returns 连接方式的可渲染对象,可以通过偏移量依次获取其他帧 - */ - renderWithoutCheck( - tile: Readonly, - connection: number - ): ITextureRenderable | null; - - /** - * 根据图块数字,获取指定自动元件经过链接的动态可渲染对象 - * @param autotile 自动元件的图块数字 + * 根据图块贴图对象,获取指定自动元件经过链接的动态可渲染对象 + * @param autotile 自动元件的图块信息 * @param connection 自动元件的连接方式 * @returns 生成器,每一个输出代表每一帧的渲染对象,不同自动元件的帧数可能不同 */ renderAnimated( - autotile: number, - connection: number - ): Generator; - - /** - * 根据图块贴图对象,获取指定自动元件经过链接的动态可渲染对象 - * @param autotile 自动元件的图块数字 - * @param connection 自动元件的连接方式 - * @returns 生成器,每一个输出代表每一帧的渲染对象,不同自动元件的帧数可能不同 - */ - renderAnimatedWith( tile: Readonly, connection: number ): Generator; @@ -196,33 +165,27 @@ export interface IAutotileProcessor extends ICoreStateExtended { export interface ITextureGetter { /** * 根据图块数字获取图块,可以获取额外素材,会自动将未缓存的额外素材缓存 - * @param identifier 图块的图块数字 + * @param num 图块的图块数字 */ - getTile(identifier: number): Readonly | null; - - /** - * 根据图块标识符获取图块类型 - * @param identifier 图块标识符,即图块数字 - */ - getBlockCls(identifier: number): BlockCls; + getTile(num: number): Readonly | null; /** * 根据标识符获取图集信息 - * @param identifier 图集的标识符 + * @param num 图集的标识符 */ - getAsset(identifier: number): ITextureComposedData | null; + getAsset(num: number): ITextureComposedData | null; /** * 根据额外素材索引获取额外素材 - * @param identifier 额外素材的索引 + * @param num 额外素材的索引 */ - getTileset(identifier: number): ITexture | null; + getTileset(num: number): ITexture | null; /** * 根据图片的索引获取图片 - * @param identifier 图片的索引 + * @param num 图片的索引 */ - getImage(identifier: number): ITexture | null; + getImage(num: number): ITexture | null; } export interface ITextureAliasGetter { @@ -249,16 +212,15 @@ export interface ITextureAliasGetter { * @param alias 图集的别名 */ getAssetByAlias(alias: string): ITextureComposedData | null; - - /** - * 根据图块别名获取图块类型 - * @param alias 图块别名,即图块的 id - */ - getBlockClsByAlias(alias: string): BlockCls; } export interface ITextureManager extends ITextureGetter, ITextureAliasGetter, ICoreStateExtended { + /** 使用的图块数据存储对象 */ + readonly tiles: ITileStore; + /** 自动元件处理器 */ + readonly autotile: IAutotileProcessor; + /** 通过加载获取的所有纹理贴图存储,包括图块、普通图片等 */ readonly textures: ITextureStore; /** 贴图存储,把 terrains 等内容单独分开存储 */ @@ -275,51 +237,73 @@ export interface ITextureManager /** 带有脏标记追踪的图集信息 */ readonly trackedAsset: ITrackedAssetData; - /** 图块类型映射 */ - readonly clsMap: Map; + /** Tileset 额外素材预留多少数字,即从多少数字开始,素材变为额外素材 */ + readonly tilesetReserve: number; + /** + * Tileset 额外素材的素材数量单位,例如 10000 就表示每个额外素材最少使用 10000 个数字位, + * 如果超出则变为 20000, 30000,以此类推。 + */ + readonly tilesetUnit: number; /** - * 添加网格类型的贴图,包括 terrains 和 items 类型 + * 添加网格类型的贴图,此方式添加的贴图仅有一帧,不包含动画。 * @param source 图像源 - * @param map 贴图字符串 id 与图块数字映射,按照先从左到右,再从上到下的顺序映射 + * @param width 每个贴图的像素宽度 + * @param height 每个贴图的像素高度 + * @param map 贴图的图块数字映射,按照先从左到右,再从上到下的顺序映射,如下图所示 + * + * ```txt + * 0 1 2 ... n-1 + * n n+1 n+2 ... 2n-1 + * . ... ... ... ... + * ``` */ addGrid( source: SizedCanvasImageSource, - map: ArrayLike + map: ArrayLike, + width: number, + height: number ): Iterable; /** - * 添加行动画的贴图,包括 animates enemys npcs enemy48 npc48 类型 + * 添加行动画的贴图,要求每一行的帧数一致,如不一致请拆分成多次调用 * @param source 图像源 - * @param map 贴图字符串 id 与图块数字映射,按从上到下的顺序映射 - * @param frames 每一行的帧数 - * @param height 每一行的高度 + * @param map 贴图的图块数字映射,按从上到下的顺序映射 + * @param width 每一帧的像素宽度 + * @param height 每一帧的像素高度,也就是每一行的高度 */ addRowAnimate( source: SizedCanvasImageSource, - map: ArrayLike, + map: ArrayLike, + width: number, height: number ): Iterable; /** * 添加自动元件 * @param source 图像源 - * @param identifier 自动元件的字符串 id 及图块数字 + * @param identifier 自动元件的图块数字 + * @param type 自动元件类型,是 3x4 还是 2x3 * @returns 由于自动元件是懒加载的,因此不会返回任何东西 */ addAutotile( source: SizedCanvasImageSource, - identifier: IBlockIdentifier + identifier: number, + type: AutotileType ): void; /** * 添加一个 tileset 类型的素材 * @param source 图像源 - * @param alias tileset 的标识符,包含其在 tilesets 列表中的索引和图片名称 + * @param identifier tileset 的标识符对象 + * @param cellWidth Tileset 中每个 tile 的宽度 + * @param cellHeight Tileset 中每个 tile 的高度 */ addTileset( source: SizedCanvasImageSource, - identifier: IIndexedIdentifier + identifier: IIndexedIdentifier, + cellWidth: number, + cellHeight: number ): IMaterialData | null; /** @@ -334,44 +318,48 @@ export interface ITextureManager /** * 设置指定图块默认显示第几帧 - * @param identifier 图块标识符,即图块数字 + * @param num 图块标识符,即图块数字 * @param defaultFrame 图块的默认帧数 */ - setDefaultFrame(identifier: number, defaultFrame: number): void; + setDefaultFrame(num: number, defaultFrame: number): void; /** * 获取图块的默认帧数,-1 表示正常动画,非负整数表示默认使用指定帧数,除非单独指定 - * @param identifier 图块标识符,即图块数字 + * @param num 图块标识符,即图块数字 */ - getDefaultFrame(identifier: number): number; + getDefaultFrame(num: number): number; /** - * 缓存某个 tileset,当需要缓存多个时,请使用 {@link cacheTilesetList} 方法 - * @param identifier tileset 的标识符,即图块数字 + * 获取指定图块的总帧数,若图块不存在,返回 0 + * @param num 图块标识符,即图块数字 */ - cacheTileset(identifier: number): ITexture | null; + getFrameCount(num: number): number; /** - * 缓存一系列 tileset - * @param identifierList 标识符列表,即图块数字列表 + * 缓存某个 tileset 上的图块,作用是生成贴图并打包入图集, + * 当需要缓存多个时,请使用 {@link cacheTilesetList} 方法 + * @param num tileset 图块的标识符,即图块数字 */ - cacheTilesetList( - identifierList: Iterable - ): Iterable; + cacheTileset(num: number): ITexture | null; /** - * 缓存某个自动元件,当需要缓存多个时,请使用 {@link cacheAutotileList} 方法 - * @param identifier 自动元件标识符,即图块数字 + * 缓存一系列 tileset 上的图块 + * @param list 标识符列表,即图块数字列表 */ - cacheAutotile(identifier: number): ITexture | null; + cacheTilesetList(list: Iterable): Iterable; + + /** + * 缓存某个自动元件,作用是生成展平贴图并打包入图集,展平贴图指将自动元件拆分成 48 种连接方式纵向堆叠组成的贴图。 + * 当需要缓存多个时,请使用 {@link cacheAutotileList} 方法 + * @param num 自动元件标识符,即图块数字 + */ + cacheAutotile(num: number): ITexture | null; /** * 缓存一系列自动元件 - * @param identifierList 自动元件标识符列表,即图块数字列表 + * @param list 自动元件标识符列表,即图块数字列表 */ - cacheAutotileList( - identifierList: Iterable - ): Iterable; + cacheAutotileList(list: Iterable): Iterable; /** * 把常用素材打包成为图集形式供后续使用 @@ -392,9 +380,9 @@ export interface ITextureManager /** * 根据图块标识符在图集中获取对应的可渲染对象 - * @param identifier 图块标识符,即图块数字 + * @param num 图块标识符,即图块数字 */ - getRenderable(identifier: number): ITextureRenderable | null; + getRenderable(num: number): ITextureRenderable | null; /** * 根据图块别名在图集中获取对应的可渲染对象 @@ -402,18 +390,6 @@ export interface ITextureManager */ getRenderableByAlias(alias: string): ITextureRenderable | null; - /** - * 根据图块别名获取图块标识符,即图块数字 - * @param alias 图块别名,即图块的 id - */ - getIdentifierByAlias(alias: string): number | undefined; - - /** - * 根据图块标识符获取图块别名,即图块的 id - * @param identifier 图块标识符,即图块数字 - */ - getAliasByIdentifier(identifier: number): string | undefined; - /** * 当前的所有图集中是否包含指定的贴图对象 * @param texture 贴图对象 diff --git a/packages-user/client-base/src/types.ts b/packages-user/client-base/src/types.ts index 8119921..2ba5691 100644 --- a/packages-user/client-base/src/types.ts +++ b/packages-user/client-base/src/types.ts @@ -1,7 +1,7 @@ import { IBGMPlayer, IMotaAudioContext, ISoundPlayer } from '@motajs/audio'; import { ISaveSystem } from './save'; import { ICoreState } from '@user/data-state'; -import { IAutotileProcessor, ITextureManager } from './material'; +import { ITextureManager } from './material'; import { IRenderTreeRoot } from '@motajs/render'; import { IExcitation, IExcitationDivider } from '@motajs/animate'; @@ -16,8 +16,6 @@ export interface IClientBase extends ICoreState { readonly bgmPlayer: IBGMPlayer; /** 素材管理器 */ readonly materials: ITextureManager; - /** 自动元件处理器 */ - readonly autotile: IAutotileProcessor; /** 渲染画面的根元素 */ readonly renderer: IRenderTreeRoot; /** 用于渲染系统的 Raf 激励源 */ diff --git a/packages-user/client-modules/src/client.ts b/packages-user/client-modules/src/client.ts index b70bdfe..0e49898 100644 --- a/packages-user/client-modules/src/client.ts +++ b/packages-user/client-modules/src/client.ts @@ -12,7 +12,7 @@ import { IRenderTreeRoot, MotaRenderer } from '@motajs/render'; import { ITextureManager, IAutotileProcessor, - MaterialManager, + TextureManager, AutotileProcessor, ISaveSystem, SaveSystem @@ -48,7 +48,6 @@ export class ClientCore extends CoreState implements IClientCore { // Layer 5 渲染顶层 readonly materials: ITextureManager; - readonly autotile: IAutotileProcessor; readonly rafExcitation: IExcitation; readonly excitationDivider: IExcitationDivider; @@ -81,8 +80,12 @@ export class ClientCore extends CoreState implements IClientCore { //#region 素材系统 - this.materials = new MaterialManager(this); - this.autotile = new AutotileProcessor(this.materials, this); + this.materials = new TextureManager( + this, + this.tileStore, + config.tilesetReserve, + config.tilesetUnit + ); //#endregion diff --git a/packages-user/client-modules/src/core.ts b/packages-user/client-modules/src/core.ts index 8cc1770..067906a 100644 --- a/packages-user/client-modules/src/core.ts +++ b/packages-user/client-modules/src/core.ts @@ -3,4 +3,9 @@ import { ClientCore } from './client'; // TODO: 逐渐弱化 ClientCore 的单例概念,每个接口都通过参数传入 IClientCore 对象 /** 客户端实例 */ -export const client = new ClientCore(); +export const client = new ClientCore({ + clientURL: 'placeholder', + // 这两个值不要随意调整,若需要调整,务必先在编辑器中调整“额外素材预留”与“额外素材单位”,否则必定会导致游戏出错 + tilesetReserve: 100000, + tilesetUnit: 5000 +}); diff --git a/packages-user/client-modules/src/render/map/renderer.ts b/packages-user/client-modules/src/render/map/renderer.ts index 4a879b6..e3f6ae1 100644 --- a/packages-user/client-modules/src/render/map/renderer.ts +++ b/packages-user/client-modules/src/render/map/renderer.ts @@ -1261,7 +1261,7 @@ export class MapRenderer } else { // 多帧图块 if (tex.cls === BlockCls.Autotile) { - const gen = this.autotile.renderAnimatedWith(tex, 0b1111_1111); + const gen = this.autotile.renderAnimated(tex, 0b1111_1111); return this.useDynamicBackground(gl, data, [...gen]); } else { const gen = this.tileAnimater.once(tex.texture, tex.frames); diff --git a/packages-user/client-modules/src/types.ts b/packages-user/client-modules/src/types.ts index 1a910ac..9629211 100644 --- a/packages-user/client-modules/src/types.ts +++ b/packages-user/client-modules/src/types.ts @@ -4,6 +4,10 @@ import { IMapExtensionManager, IMapRenderer } from './render/map'; export interface IClientCoreConfig { /** 渲染端数据配置文件路径,相对于 `src/content` */ readonly clientURL: string; + /** Tileset 预留数量 */ + readonly tilesetReserve: number; + /** Tileset 单元数量 */ + readonly tilesetUnit: number; } export interface IClientCore extends IClientSystem { diff --git a/packages-user/entry-client/src/create.ts b/packages-user/entry-client/src/create.ts index 10f9219..8156b04 100644 --- a/packages-user/entry-client/src/create.ts +++ b/packages-user/entry-client/src/create.ts @@ -33,7 +33,6 @@ export function create() { } async function createModule() { - UserClientBase.create(); ClientModules.create(); LegacyUI.create(); diff --git a/packages/common/src/logger.json b/packages/common/src/logger.json index 716bc36..7c5d8a0 100644 --- a/packages/common/src/logger.json +++ b/packages/common/src/logger.json @@ -262,6 +262,14 @@ "181": "Number $1 has been registered to $2 store, old one will be overridden.", "182": "Cannot bind '$1' instance to '$2' since they belong to different CoreState instance.", "183": "An in-map location is expected to begin map graph building, but got outside.", + "184": "Cannot add tile texture $1 since no registered id for it in tile store.", + "185": "Cannot add row animated texture since it cannot be splitted averagely by specified width. Texture width: $1, split width: $2.", + "186": "Tileset has a size that cannot be splitted evenly by specified cell size, overflowed content will be ignored. Tileset size: [$1, $2], cell size: [$1, $2].", + "187": "Cannot get tile that indeed has a texture but no frame and offset infomation, which seems like a internal bug of mota-js engine, please report a bug.", + "188": "Tileset tile $1 exceeds tileset itself. You may check the num indeed corresponding to a tileset tile, or may a internal bug of mota-js engine.", + "189": "Cannot flatten autotile since it cannot be splitted evenly. Autotile width: $1, frames: $2.", + "190": "Cannot flatten autotile since its width or height cannot be divided evenly. Autotile frame size: [$1,$2].", + "191": "Cannot cache autotile since no its frame or type data stored.", "1001": "Event(setBlock): Unknown tile '$1' when setting block." } }