From 8fe12634b0330003989c5535656237a4b2fa0811 Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Sat, 22 Nov 2025 23:48:59 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=8B=87=E5=A3=AB=E5=8F=8A=E8=B7=9F?= =?UTF-8?q?=E9=9A=8F=E8=80=85=E6=B8=B2=E6=9F=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- docs/guide/ui/ui.md | 2 +- .../client-modules/src/render/commonIns.ts | 8 + .../src/render/elements/index.ts | 26 +- .../src/render/elements/props.ts | 2 + .../client-modules/src/render/map/element.ts | 16 +- .../src/render/map/extension/hero.ts | 473 ++++++++++++++++++ .../src/render/map/extension/index.ts | 2 + .../src/render/map/extension/types.ts | 89 ++++ .../client-modules/src/render/map/index.ts | 8 +- .../client-modules/src/render/map/moving.ts | 30 +- .../client-modules/src/render/map/renderer.ts | 37 +- .../client-modules/src/render/map/types.ts | 27 + .../client-modules/src/render/ui/main.tsx | 3 + packages-user/data-state/src/common/face.ts | 63 +++ packages-user/data-state/src/common/index.ts | 3 + packages-user/data-state/src/common/types.ts | 54 ++ packages-user/data-state/src/common/utils.ts | 52 ++ packages-user/data-state/src/core.ts | 33 ++ packages-user/data-state/src/core/core.ts | 10 - packages-user/data-state/src/core/index.ts | 22 - packages-user/data-state/src/core/types.ts | 222 -------- packages-user/data-state/src/enemy/damage.ts | 2 +- packages-user/data-state/src/enemy/special.ts | 2 +- packages-user/data-state/src/hero/index.ts | 2 + packages-user/data-state/src/hero/state.ts | 121 +++++ packages-user/data-state/src/hero/types.ts | 187 +++++++ packages-user/data-state/src/index.ts | 66 ++- .../data-state/src/{state => legacy}/hero.ts | 0 .../data-state/src/{state => legacy}/index.ts | 0 .../src/{state => legacy}/interface.ts | 0 .../data-state/src/{state => legacy}/item.ts | 0 .../data-state/src/{state => legacy}/move.ts | 2 +- .../data-state/src/{state => legacy}/utils.ts | 0 packages-user/data-state/src/map/index.ts | 1 + .../src/{core => map}/layerState.ts | 33 +- packages-user/data-state/src/map/types.ts | 120 +++++ packages-user/data-state/src/types.ts | 25 + packages/common/src/hook.ts | 8 +- packages/common/src/logger.json | 10 +- packages/common/src/types.ts | 5 +- public/project/data.js | 1 - public/project/functions.js | 10 +- 43 files changed, 1472 insertions(+), 307 deletions(-) create mode 100644 packages-user/client-modules/src/render/commonIns.ts create mode 100644 packages-user/client-modules/src/render/map/extension/hero.ts create mode 100644 packages-user/client-modules/src/render/map/extension/index.ts create mode 100644 packages-user/client-modules/src/render/map/extension/types.ts create mode 100644 packages-user/data-state/src/common/face.ts create mode 100644 packages-user/data-state/src/common/index.ts create mode 100644 packages-user/data-state/src/common/types.ts create mode 100644 packages-user/data-state/src/common/utils.ts create mode 100644 packages-user/data-state/src/core.ts delete mode 100644 packages-user/data-state/src/core/core.ts delete mode 100644 packages-user/data-state/src/core/index.ts delete mode 100644 packages-user/data-state/src/core/types.ts create mode 100644 packages-user/data-state/src/hero/index.ts create mode 100644 packages-user/data-state/src/hero/state.ts create mode 100644 packages-user/data-state/src/hero/types.ts rename packages-user/data-state/src/{state => legacy}/hero.ts (100%) rename packages-user/data-state/src/{state => legacy}/index.ts (100%) rename packages-user/data-state/src/{state => legacy}/interface.ts (100%) rename packages-user/data-state/src/{state => legacy}/item.ts (100%) rename packages-user/data-state/src/{state => legacy}/move.ts (99%) rename packages-user/data-state/src/{state => legacy}/utils.ts (100%) rename packages-user/data-state/src/{core => map}/layerState.ts (81%) create mode 100644 packages-user/data-state/src/types.ts diff --git a/README.md b/README.md index aa416ef..9fe0524 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ - 不使用下划线命名法。 - 注释: - 常用属性成员、方法、接口、类型必须添加 `jsDoc` 注释。 - - 长文件可使用 `#region` 分段,非必要则不写 `#endregion`。 + - 长文件可使用 `#region` 分段,可以写上 `#endretion` 允许折叠。 - TODO 使用 `// TODO:` 或 `// todo:` 格式。 - 单行注释的双斜杠与注释内容之间添加一个空格,多行注释只允许出现 `jsDoc` 注释,如果需要多行非 `jsDoc` 注释,使用多个单行注释。 - 类型: diff --git a/docs/guide/ui/ui.md b/docs/guide/ui/ui.md index 4d53464..237b40f 100644 --- a/docs/guide/ui/ui.md +++ b/docs/guide/ui/ui.md @@ -633,7 +633,7 @@ export interface IWheelEvent extends IActionEvent { 1. 按下、抬起、点击**永远**保持为同一个 `identifier` 2. 移动过程中,使用最后一个按下的按键的 `identifier` 作为移动事件的 `identifier` -3. 如果移动过程中,最后一个按下的按键抬起,那么依然会维持**原先的** `identifer`,**不会**回退至上一个按下的按键 +3. 如果移动过程中,最后一个按下的按键抬起,那么依然会维持**原先的** `identifier`,**不会**回退至上一个按下的按键 除此之外,滚轮事件中的 `identifier` 永远为 -1。 diff --git a/packages-user/client-modules/src/render/commonIns.ts b/packages-user/client-modules/src/render/commonIns.ts new file mode 100644 index 0000000..def22fb --- /dev/null +++ b/packages-user/client-modules/src/render/commonIns.ts @@ -0,0 +1,8 @@ +import { state } from '@user/data-state'; +import { MapRenderer } from './map/renderer'; +import { materials } from '@user/client-base'; + +/** 主地图渲染器,用于渲染游戏画面 */ +export const mainMapRenderer = new MapRenderer(materials, state.layer); +/** 副地图渲染器,用于渲染缩略图、浏览地图等 */ +export const expandMapRenderer = new MapRenderer(materials, state.layer); diff --git a/packages-user/client-modules/src/render/elements/index.ts b/packages-user/client-modules/src/render/elements/index.ts index 0681631..4fd0cf5 100644 --- a/packages-user/client-modules/src/render/elements/index.ts +++ b/packages-user/client-modules/src/render/elements/index.ts @@ -7,8 +7,9 @@ import { Icon, Winskin } from './misc'; import { Animate } from './animate'; import { createItemDetail } from './itemDetail'; import { logger } from '@motajs/common'; -import { MapRender } from '../map/element'; +import { MapRender, MapRenderer } from '../map'; import { state } from '@user/data-state'; +import { materials } from '@user/client-base'; export function createElements() { createCache(); @@ -72,14 +73,27 @@ export function createElements() { tagMap.register('map-render', (_0, _1, props) => { if (!props) { logger.error(42); - return new MapRender(state.layer); + return new MapRender( + state.layer, + new MapRenderer(materials, state.layer) + ); } - const { layerState } = props; + const { layerState, renderer } = props; if (!layerState) { - logger.error(42); - return new MapRender(state.layer); + logger.error(42, 'layerState'); + return new MapRender( + state.layer, + new MapRenderer(materials, state.layer) + ); } - return new MapRender(layerState); + if (!renderer) { + logger.error(42, 'renderer'); + return new MapRender( + state.layer, + new MapRenderer(materials, state.layer) + ); + } + return new MapRender(layerState, renderer); }); } diff --git a/packages-user/client-modules/src/render/elements/props.ts b/packages-user/client-modules/src/render/elements/props.ts index f510f0a..fced0d4 100644 --- a/packages-user/client-modules/src/render/elements/props.ts +++ b/packages-user/client-modules/src/render/elements/props.ts @@ -12,6 +12,7 @@ import { EAnimateEvent } from './animate'; import { EIconEvent, EWinskinEvent } from './misc'; import { IEnemyCollection } from '@motajs/types'; import { ILayerState } from '@user/data-state'; +import { IMapRenderer } from '../map'; export interface AnimateProps extends BaseProps {} @@ -63,6 +64,7 @@ export interface LayerProps extends BaseProps { export interface MapRenderProps extends BaseProps { layerState: ILayerState; + renderer: IMapRenderer; } declare module 'vue/jsx-runtime' { diff --git a/packages-user/client-modules/src/render/map/element.ts b/packages-user/client-modules/src/render/map/element.ts index 91f1145..33357ae 100644 --- a/packages-user/client-modules/src/render/map/element.ts +++ b/packages-user/client-modules/src/render/map/element.ts @@ -1,19 +1,21 @@ import { MotaOffscreenCanvas2D, RenderItem } from '@motajs/render-core'; -import { ILayerState, state } from '@user/data-state'; +import { ILayerState } from '@user/data-state'; import { IMapRenderer } from './types'; -import { MapRenderer } from './renderer'; import { materials } from '@user/client-base'; import { ElementNamespace, ComponentInternalInstance } from 'vue'; import { CELL_HEIGHT, CELL_WIDTH, MAP_HEIGHT, MAP_WIDTH } from '../shared'; export class MapRender extends RenderItem { - /** 地图渲染器 */ - readonly renderer: IMapRenderer; - - constructor(readonly layerState: ILayerState) { + /** + * @param layerState 地图状态对象 + * @param renderer 地图渲染器对象 + */ + constructor( + readonly layerState: ILayerState, + readonly renderer: IMapRenderer + ) { super('static'); - this.renderer = new MapRenderer(materials, state.layer); this.renderer.setLayerState(layerState); this.renderer.useAsset(materials.trackedAsset); this.renderer.setCanvasSize(this.width, this.height); diff --git a/packages-user/client-modules/src/render/map/extension/hero.ts b/packages-user/client-modules/src/render/map/extension/hero.ts new file mode 100644 index 0000000..ef85b2b --- /dev/null +++ b/packages-user/client-modules/src/render/map/extension/hero.ts @@ -0,0 +1,473 @@ +import { + degradeFace, + FaceDirection, + getFaceMovement, + IHeroState, + IHeroStateHooks, + IMapLayer, + state +} from '@user/data-state'; +import { IMapRenderer, IMapRendererTicker, IMovingBlock } from '../types'; +import { isNil } from 'lodash-es'; +import { IHookController, logger } from '@motajs/common'; +import { BlockCls, IMaterialFramedData } from '@user/client-base'; +import { + ITexture, + ITextureSplitter, + TextureRowSplitter +} from '@motajs/render-assets'; +import { IMapHeroRenderer } from './types'; +import { TimingFn } from 'mutate-animate'; + +/** 默认的移动时长 */ +const DEFAULT_TIME = 100; + +interface HeroRenderEntity { + /** 移动图块对象 */ + readonly block: IMovingBlock; + /** 标识符,用于判定跟随者 */ + readonly identifier: string; + /** 目标横坐标,移动时有效 */ + targetX: number; + /** 目标纵坐标,移动时有效 */ + targetY: number; + /** 当前的移动朝向 */ + direction: FaceDirection; + /** 下一个跟随者的移动方向 */ + nextDirection: FaceDirection; + + /** 当前是否正在移动 */ + moving: boolean; + /** 当前是否正在动画,移动跟动画要分开,有的操作比如跳跃就是在移动中但是没动画 */ + animating: boolean; + /** 帧动画间隔 */ + animateInterval: number; + /** 当一次动画时刻 */ + lastAnimateTime: number; + /** 当前的动画帧数 */ + animateFrame: number; + /** 移动的 `Promise`,移动完成时兑现,如果停止,则一直是兑现状态 */ + promise: Promise; +} + +export class MapHeroRenderer implements IMapHeroRenderer { + private static readonly splitter: ITextureSplitter = + new TextureRowSplitter(); + + /** 勇士钩子 */ + readonly controller: IHookController; + /** 每个朝向的贴图对象 */ + readonly textureMap: Map = new Map(); + /** 勇士渲染实体,与 `entities[0]` 同引用 */ + readonly heroEntity: HeroRenderEntity; + + /** + * 渲染实体,索引 0 表示勇士,后续索引依次表示跟随的跟随者。 + * 整体是一个状态机,而且下一个跟随者只与上一个跟随者有关,下一个跟随者移动的方向就是上一个跟随者移动前指向的方向。 + */ + readonly entities: HeroRenderEntity[] = []; + + /** 每帧执行的帧动画对象 */ + readonly ticker: IMapRendererTicker; + + constructor( + readonly renderer: IMapRenderer, + readonly layer: IMapLayer, + readonly hero: IHeroState + ) { + this.controller = hero.addHook(new MapHeroHook(this)); + this.controller.load(); + const moving = this.addHeroMoving(renderer, layer, hero); + const heroEntity: HeroRenderEntity = { + block: moving, + identifier: '', + targetX: hero.x, + targetY: hero.y, + direction: hero.direction, + nextDirection: FaceDirection.Unknown, + moving: false, + animating: false, + animateInterval: 0, + lastAnimateTime: 0, + animateFrame: 0, + promise: Promise.resolve() + }; + this.heroEntity = heroEntity; + this.entities.push(heroEntity); + this.ticker = renderer.requestTicker(time => this.tick(time)); + } + + /** + * 添加勇士对应的移动图块 + * @param renderer 渲染器 + * @param layer 图块所属图层 + * @param hero 勇士状态对象 + */ + private addHeroMoving( + renderer: IMapRenderer, + layer: IMapLayer, + hero: IHeroState + ) { + if (isNil(hero.image)) { + logger.warn(88); + return renderer.addMovingBlock(layer, 0, hero.x, hero.y); + } + const image = this.renderer.manager.getImageByAlias(hero.image); + if (!image) { + logger.warn(89, hero.image); + return renderer.addMovingBlock(layer, 0, hero.x, hero.y); + } + this.updateHeroTexture(image); + const tex = this.textureMap.get(degradeFace(hero.direction)); + if (!tex) { + return renderer.addMovingBlock(layer, 0, hero.x, hero.y); + } + return renderer.addMovingBlock(layer, tex, hero.x, hero.y); + } + + /** + * 更新勇士贴图 + * @param image 勇士使用的贴图,包含四个方向 + */ + private updateHeroTexture(image: ITexture) { + const textures = [ + ...image.split(MapHeroRenderer.splitter, image.height / 4) + ]; + if (textures.length !== 4) { + logger.warn(90, hero.image); + return; + } + const faceList = [ + FaceDirection.Down, + FaceDirection.Left, + FaceDirection.Right, + FaceDirection.Up + ]; + faceList.forEach((v, i) => { + const dirImage = textures[i]; + const data: IMaterialFramedData = { + offset: dirImage.width / 4, + texture: dirImage, + cls: BlockCls.Unknown, + frames: 4 + }; + this.textureMap.set(v, data); + }); + } + + private tick(time: number) { + this.entities.forEach(v => { + if (!v.animating) { + v.animateFrame = 0; + return; + } + const dt = time - v.lastAnimateTime; + if (dt > v.animateInterval) { + v.animateFrame++; + v.lastAnimateTime = time; + v.block.useSpecifiedFrame(v.animateFrame); + } + }); + } + + setImage(image: ITexture): void { + this.updateHeroTexture(image); + const tex = this.textureMap.get(degradeFace(this.hero.direction)); + if (!tex) return; + this.heroEntity.block.setTexture(tex); + } + + setAlpha(alpha: number): void { + this.heroEntity.block.setAlpha(alpha); + } + + setPosition(x: number, y: number): void { + this.heroEntity.block.setPos(x, y); + } + + /** + * 移动指定渲染实体,不会影响其他渲染实体。多次调用时会按顺序依次移动 + * @param entity 渲染实体 + * @param direction 移动方向 + * @param time 移动时长 + */ + private moveEntity( + entity: HeroRenderEntity, + direction: FaceDirection, + time: number + ) { + const { x: dx, y: dy } = getFaceMovement(direction); + if (dx === 0 && dy === 0) return; + const block = entity.block; + const tx = block.x + dx; + const ty = block.y + dy; + const nextTile = state.roleFace.getFaceOf(block.tile, direction); + const nextTex = this.renderer.manager.getIfBigImage( + nextTile?.identifier ?? block.tile + ); + entity.promise = entity.promise.then(async () => { + entity.moving = true; + entity.animating = true; + entity.nextDirection = entity.direction; + entity.direction = direction; + if (nextTex) block.setTexture(nextTex); + await block.lineTo(tx, ty, time); + entity.moving = false; + entity.animating = false; + }); + } + + /** + * 生成跳跃曲线 + * @param dx 横向偏移量 + * @param dy 纵向偏移量 + */ + private generateJumpFn(dx: number, dy: number): TimingFn<2> { + const distance = Math.hypot(dx, dy); + const peak = 3 + distance; + + return (progress: number) => { + const x = dx * progress; + const y = progress * dy + (progress ** 2 - progress) * peak; + + return [x, y]; + }; + } + + /** + * 将指定渲染实体跳跃至目标点,多次调用时会按顺序依次执行,可以与 `moveEntity` 混用 + * @param entity 渲染实体 + * @param x 目标横坐标 + * @param y 目标纵坐标 + * @param time 跳跃时长 + */ + private jumpEntity( + entity: HeroRenderEntity, + x: number, + y: number, + time: number + ) { + const block = entity.block; + entity.promise = entity.promise.then(async () => { + const dx = block.x - x; + const dy = block.y - y; + const fn = this.generateJumpFn(dx, dy); + entity.moving = true; + entity.animating = false; + entity.animateFrame = 0; + await block.moveRelative(fn, time); + entity.moving = false; + entity.animating = false; + }); + } + + startMove(): void { + this.heroEntity.moving = true; + this.heroEntity.animating = true; + this.heroEntity.animateFrame = 1; + this.heroEntity.lastAnimateTime = this.ticker.timestamp; + this.heroEntity.block.useSpecifiedFrame(1); + } + + async waitMoveEnd(waitFollower: boolean): Promise { + if (waitFollower) { + await Promise.all(this.entities.map(v => v.promise)); + return; + } + return this.heroEntity.promise; + } + + stopMove(stopFollower: boolean): void { + if (stopFollower) { + this.entities.forEach(v => { + v.block.endMoving(); + }); + } else { + this.heroEntity.block.endMoving(); + } + } + + async move(direction: FaceDirection, time: number): Promise { + this.moveEntity(this.heroEntity, direction, time); + for (let i = 1; i < this.entities.length; i++) { + const last = this.entities[i - 1]; + this.moveEntity(this.entities[i], last.nextDirection, time); + } + await Promise.all(this.entities.map(v => v.promise)); + } + + async jumpTo( + x: number, + y: number, + time: number, + waitFollower: boolean + ): Promise { + // 首先要把所有的跟随者移动到勇士所在位置 + for (let i = 1; i < this.entities.length; i++) { + // 对于每一个跟随者,需要向前遍历每一个跟随者,然后朝向移动,这样就可以聚集在一起了 + const now = this.entities[i]; + for (let j = i - 1; j >= 0; j--) { + const last = this.entities[j]; + this.moveEntity(now, last.nextDirection, DEFAULT_TIME); + } + } + this.entities.forEach(v => { + this.jumpEntity(v, x, y, time); + }); + if (waitFollower) { + await Promise.all(this.entities.map(v => v.promise)); + } else { + return this.heroEntity.promise; + } + } + + addFollower(image: number, id: string): void { + const last = this.entities[this.entities.length - 1]; + if (last.moving) { + logger.warn(92); + return; + } + const nowFace = degradeFace(last.nextDirection, FaceDirection.Down); + const faced = state.roleFace.getFaceOf(image, nowFace); + const tex = this.renderer.manager.getIfBigImage(faced?.face ?? image); + if (!tex) { + logger.warn(91, image.toString()); + return; + } + const { x: dxn, y: dyn } = getFaceMovement(last.nextDirection); + const { x: dx, y: dy } = getFaceMovement(last.direction); + const x = last.block.x - dxn; + const y = last.block.y - dyn; + const moving = this.renderer.addMovingBlock(this.layer, tex, x, y); + const entity: HeroRenderEntity = { + block: moving, + identifier: id, + targetX: last.targetX - dx, + targetY: last.targetY - dy, + direction: nowFace, + nextDirection: FaceDirection.Unknown, + moving: false, + animating: false, + animateInterval: 0, + lastAnimateTime: 0, + animateFrame: 0, + promise: Promise.resolve() + }; + this.entities.push(entity); + } + + async removeFollower(follower: string, animate: boolean): Promise { + const index = this.entities.findIndex(v => v.identifier === follower); + if (index === -1) return; + if (this.entities[index].moving) { + logger.warn(93); + return; + } + if (index === this.entities.length - 1) { + this.entities.splice(index, 1); + return; + } + // 展示动画 + if (animate) { + for (let i = index + 1; i < this.entities.length; i++) { + const last = this.entities[i - 1]; + const moving = this.entities[i]; + this.moveEntity(moving, last.nextDirection, DEFAULT_TIME); + } + this.entities.splice(index, 1); + await Promise.all(this.entities.map(v => v.promise)); + return; + } + // 不展示动画 + for (let i = index + 1; i < this.entities.length; i++) { + const last = this.entities[i - 1]; + const moving = this.entities[i]; + moving.block.setPos(last.block.x, last.block.y); + const nextFace = state.roleFace.getFaceOf( + moving.block.tile, + last.nextDirection + ); + if (!nextFace) continue; + const tile = this.renderer.manager.getIfBigImage( + nextFace.identifier + ); + if (!tile) continue; + moving.block.setTexture(tile); + moving.nextDirection = moving.direction; + moving.direction = last.nextDirection; + } + this.entities.splice(index, 1); + } + + removeAllFollowers(): void { + this.entities.length = 1; + } + + setFollowerAlpha(identifier: string, alpha: number): void { + const follower = this.entities.find(v => v.identifier === identifier); + if (!follower) return; + follower.block.setAlpha(alpha); + } + + destroy() { + this.controller.unload(); + } +} + +class MapHeroHook implements Partial { + constructor(readonly hero: MapHeroRenderer) {} + + onSetImage(image: ImageIds): void { + const texture = this.hero.renderer.manager.getImageByAlias(image); + if (!texture) { + logger.warn(89, hero.image); + return; + } + this.hero.setImage(texture); + } + + onSetPosition(x: number, y: number): void { + this.hero.setPosition(x, y); + } + + onStartMove(): void { + this.hero.startMove(); + } + + onMoveHero(direction: FaceDirection, time: number): Promise { + return this.hero.move(direction, time); + } + + onEndMove(waitFollower: boolean): Promise { + return this.hero.waitMoveEnd(waitFollower); + } + + onJumpHero( + x: number, + y: number, + time: number, + waitFollower: boolean + ): Promise { + return this.hero.jumpTo(x, y, time, waitFollower); + } + + onSetAlpha(alpha: number): void { + this.hero.setAlpha(alpha); + } + + onAddFollower(follower: number, identifier: string): void { + this.hero.addFollower(follower, identifier); + } + + onRemoveFollower(identifier: string, animate: boolean): void { + this.hero.removeFollower(identifier, animate); + } + + onRemoveAllFollowers(): void { + this.hero.removeAllFollowers(); + } + + onSetFollowerAlpha(identifier: string, alpha: number): void { + this.hero.setFollowerAlpha(identifier, alpha); + } +} diff --git a/packages-user/client-modules/src/render/map/extension/index.ts b/packages-user/client-modules/src/render/map/extension/index.ts new file mode 100644 index 0000000..c494129 --- /dev/null +++ b/packages-user/client-modules/src/render/map/extension/index.ts @@ -0,0 +1,2 @@ +export * from './hero'; +export * from './types'; diff --git a/packages-user/client-modules/src/render/map/extension/types.ts b/packages-user/client-modules/src/render/map/extension/types.ts new file mode 100644 index 0000000..04691a0 --- /dev/null +++ b/packages-user/client-modules/src/render/map/extension/types.ts @@ -0,0 +1,89 @@ +import { ITexture } from '@motajs/render-assets'; +import { FaceDirection } from '@user/data-state'; + +export interface IMapHeroRenderer { + /** + * 设置勇士图片 + * @param image 勇士使用的图片 + */ + setImage(image: ITexture): void; + + /** + * 添加跟随者 + * @param image 跟随者图块数字 + * @param id 跟随者的 id,用于删除操作 + */ + addFollower(image: number, id: string): void; + + /** + * 取消跟随者 + * @param follower 跟随者的 id + * @param animate 填 `true` 的话,如果删除了中间的跟随者,后续跟随者会使用移动动画移动到下一格,否则瞬移至下一格 + */ + removeFollower(follower: string, animate: boolean): Promise; + + /** + * 移除所有跟随者 + */ + removeAllFollowers(): void; + + /** + * 设置勇士位置 + */ + setPosition(x: number, y: number): void; + + /** + * 开始移动,在移动前需要调用此方法切换勇士状态 + */ + startMove(): void; + + /** + * 等待勇士移动停止后,将移动状态切换为停止 + * @param waitFollower 是否也等待跟随者移动结束 + */ + waitMoveEnd(waitFollower: boolean): Promise; + + /** + * 立刻停止移动,勇士瞬移到目标点 + * @param stopFollower 是否也立刻停止跟随者的移动,此时跟随者也会瞬移到它们应该到达的地方 + */ + stopMove(stopFollower: boolean): void; + + /** + * 勇士朝某个方向移动 + * @param direction 移动方向 + */ + move(direction: FaceDirection, time: number): Promise; + + /** + * 跳跃勇士至目标点 + * @param x 目标点横坐标 + * @param y 目标点纵坐标 + * @param time 跳跃时长 + * @param waitFollower 是否等待跟随者也跳跃完毕 + */ + jumpTo( + x: number, + y: number, + time: number, + waitFollower: boolean + ): Promise; + + /** + * 设置勇士不透明度 + * @param alpha 不透明度 + */ + setAlpha(alpha: number): void; + + /** + * 设置跟随者的不透明度 + * @param identifier 跟随者标识符 + * @param alpha 跟随者不透明度 + */ + setFollowerAlpha(identifier: string, alpha: number): void; + + /** + * 摧毁这个勇士渲染拓展,释放相关资源 + */ + destroy(): void; +} diff --git a/packages-user/client-modules/src/render/map/index.ts b/packages-user/client-modules/src/render/map/index.ts index c0435c5..0aa67e7 100644 --- a/packages-user/client-modules/src/render/map/index.ts +++ b/packages-user/client-modules/src/render/map/index.ts @@ -1,3 +1,9 @@ -export * from './asset'; +export * from './block'; +export * from './constant'; +export * from './element'; +export * from './moving'; export * from './renderer'; +export * from './status'; export * from './types'; +export * from './vertex'; +export * from './viewport'; diff --git a/packages-user/client-modules/src/render/map/moving.ts b/packages-user/client-modules/src/render/map/moving.ts index 97c02cb..b805c41 100644 --- a/packages-user/client-modules/src/render/map/moving.ts +++ b/packages-user/client-modules/src/render/map/moving.ts @@ -24,11 +24,11 @@ export interface IMovingRenderer { } export class MovingBlock extends DynamicBlockStatus implements IMovingBlock { - readonly texture: IMaterialFramedData; readonly tile: number; readonly renderer: IMovingRenderer; readonly layer: IMapLayer; + texture: IMaterialFramedData; index: number; x: number = 0; y: number = 0; @@ -100,12 +100,19 @@ export class MovingBlock extends DynamicBlockStatus implements IMovingBlock { this.posUpdated = true; } + setTexture(texture: IMaterialFramedData): void { + if (texture === this.texture) return; + this.texture = texture; + this.renderer.vertex.updateMoving(this, true); + } + lineTo( x: number, y: number, time: number, timing?: TimingFn ): Promise { + if (!this.end) return Promise.resolve(this); this.startX = this.x; this.startY = this.y; this.targetX = x; @@ -129,6 +136,7 @@ export class MovingBlock extends DynamicBlockStatus implements IMovingBlock { } moveAs(curve: TimingFn<2>, time: number, timing?: TimingFn): Promise { + if (!this.end) return Promise.resolve(this); this.time = time; this.line = false; this.relative = false; @@ -154,6 +162,7 @@ export class MovingBlock extends DynamicBlockStatus implements IMovingBlock { time: number, timing?: TimingFn ): Promise { + if (!this.end) return Promise.resolve(this); this.time = time; this.line = false; this.relative = false; @@ -225,6 +234,25 @@ export class MovingBlock extends DynamicBlockStatus implements IMovingBlock { return true; } + endMoving(): void { + this.end = true; + if (this.line) { + this.x = this.targetX; + this.y = this.targetY; + } else { + const [x, y] = this.curve(1); + if (this.relative) { + this.x = x + this.startX; + this.y = y + this.startY; + } else { + this.x = x; + this.y = y; + } + } + this.promiseFunc(); + this.posUpdated = true; + } + destroy(): void { this.renderer.deleteMoving(this); } diff --git a/packages-user/client-modules/src/render/map/renderer.ts b/packages-user/client-modules/src/render/map/renderer.ts index f9ad3b5..443c90a 100644 --- a/packages-user/client-modules/src/render/map/renderer.ts +++ b/packages-user/client-modules/src/render/map/renderer.ts @@ -18,6 +18,7 @@ import { IMapBackgroundConfig, IMapRenderConfig, IMapRenderer, + IMapRendererTicker, IMapVertexGenerator, IMapViewportController, IMovingBlock, @@ -161,6 +162,8 @@ export class MapRenderer private needUpdateFrameCounter: boolean = true; /** 帧动画速率 */ private frameSpeed: number = 300; + /** 帧动画列表 */ + private tickers: Set = new Set(); /** 画布元素 */ readonly canvas: HTMLCanvasElement; @@ -1494,6 +1497,16 @@ export class MapRenderer } } + requestTicker(fn: (timestamp: number) => void): IMapRendererTicker { + const ticker = new MapRendererTicker(this, fn, this.timestamp); + this.tickers.add(ticker); + return ticker; + } + + removeTicker(ticker: MapRendererTicker): void { + this.tickers.delete(ticker); + } + updateTransform(): void { this.needUpdateTransform = true; } @@ -1515,10 +1528,7 @@ export class MapRenderer class RendererLayerStateHook implements Partial { constructor(readonly renderer: MapRenderer) {} - onChangeBackground( - _: IHookController, - tile: number - ): void { + onChangeBackground(tile: number): void { this.renderer.setTileBackground(tile); } @@ -1531,7 +1541,6 @@ class RendererLayerStateHook implements Partial { } onUpdateLayerArea( - _: IHookController, layer: IMapLayer, x: number, y: number, @@ -1542,7 +1551,6 @@ class RendererLayerStateHook implements Partial { } onUpdateLayerBlock( - _: IHookController, layer: IMapLayer, block: number, x: number, @@ -1551,3 +1559,20 @@ class RendererLayerStateHook implements Partial { this.renderer.updateLayerBlock(layer, block, x, y); } } + +class MapRendererTicker implements IMapRendererTicker { + constructor( + readonly renderer: MapRenderer, + readonly fn: (timestamp: number) => void, + public timestamp: number + ) {} + + tick(timestamp: number) { + this.timestamp = timestamp; + this.fn(timestamp); + } + + remove(): void { + this.renderer.removeTicker(this); + } +} diff --git a/packages-user/client-modules/src/render/map/types.ts b/packages-user/client-modules/src/render/map/types.ts index 21b2928..4efe3ab 100644 --- a/packages-user/client-modules/src/render/map/types.ts +++ b/packages-user/client-modules/src/render/map/types.ts @@ -198,6 +198,12 @@ export interface IMovingBlock extends IBlockStatus { */ setPos(x: number, y: number): void; + /** + * 设置此移动图块使用的贴图,最好预先打包至图集中,否则动态重建图集会很耗时间 + * @param texture 贴图对象 + */ + setTexture(texture: IMaterialFramedData): void; + /** * 沿直线移动到目标点 * @param x 目标横坐标,可以填小数 @@ -236,12 +242,27 @@ export interface IMovingBlock extends IBlockStatus { */ stepMoving(timestamp: number): boolean; + /** + * 立刻停止移动 + */ + endMoving(): void; + /** * 摧毁这个移动图块对象,之后不会再显示到画面上 */ destroy(): void; } +export interface IMapRendererTicker { + /** 当前的时间戳 */ + readonly timestamp: number; + + /** + * 移除这个帧函数 + */ + remove(): void; +} + export interface IMapRenderer { /** 地图渲染器使用的资源管理器 */ readonly manager: IMaterialManager; @@ -456,6 +477,12 @@ export interface IMapRenderer { * 当前地图状态是否发生改变,需要更新 */ needUpdate(): boolean; + + /** + * 添加一个每帧执行的函数 + * @param fn 每帧执行的函数 + */ + requestTicker(fn: (timestamp: number) => void): IMapRendererTicker; } export interface IMapVertexArray { diff --git a/packages-user/client-modules/src/render/ui/main.tsx b/packages-user/client-modules/src/render/ui/main.tsx index 503e638..ac2f0e2 100644 --- a/packages-user/client-modules/src/render/ui/main.tsx +++ b/packages-user/client-modules/src/render/ui/main.tsx @@ -48,6 +48,7 @@ import { mainUIController } from './controller'; import { LayerGroup } from '../elements'; import { isNil } from 'lodash-es'; import { materials } from '@user/client-base'; +import { mainMapRenderer } from '../commonIns'; const MainScene = defineComponent(() => { //#region 基本定义 @@ -261,6 +262,7 @@ const MainScene = defineComponent(() => { return () => (