From cd9c95d24d557bda65d086b10bebd34f440d56cb Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Thu, 29 Aug 2024 22:07:26 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=B9=B3=E6=BB=91=E8=A7=86=E8=A7=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/render/container.ts | 8 +- src/core/render/index.ts | 3 + src/core/render/item.ts | 15 +- src/core/render/preset/damage.ts | 6 +- src/core/render/preset/layer.ts | 20 +- src/core/render/preset/viewport.ts | 390 +++++++++++++++++++++++++++++ src/core/render/sprite.ts | 8 +- src/plugin/fx/portal.ts | 2 +- src/plugin/game/fallback.ts | 30 +++ 9 files changed, 468 insertions(+), 14 deletions(-) create mode 100644 src/core/render/preset/viewport.ts diff --git a/src/core/render/container.ts b/src/core/render/container.ts index aa62607..fb8d862 100644 --- a/src/core/render/container.ts +++ b/src/core/render/container.ts @@ -23,8 +23,12 @@ export class Container * @param type 渲染模式,absolute表示绝对位置,static表示跟随摄像机移动 * @param cache 是否启用缓存机制 */ - constructor(type: RenderItemPosition = 'static', cache: boolean = true) { - super(type, cache); + constructor( + type: RenderItemPosition = 'static', + cache: boolean = true, + fall: boolean = false + ) { + super(type, cache, fall); this.type = type; } diff --git a/src/core/render/index.ts b/src/core/render/index.ts index 963a760..b027789 100644 --- a/src/core/render/index.ts +++ b/src/core/render/index.ts @@ -9,6 +9,7 @@ import { LayerGroupFilter } from '@/plugin/fx/gameCanvas'; import { LayerGroupAnimate } from './preset/animate'; import { LayerGroupPortal } from '@/plugin/fx/portal'; import { LayerGroupHalo } from '@/plugin/fx/halo'; +import { FloorViewport } from './preset/viewport'; let main: MotaRenderer; @@ -37,6 +38,7 @@ Mota.require('var', 'loading').once('loaded', () => { const animate = new LayerGroupAnimate(); const portal = new LayerGroupPortal(); const halo = new LayerGroupHalo(); + const viewport = new FloorViewport(); layer.extends(damage); layer.extends(detail); layer.extends(filter); @@ -46,6 +48,7 @@ Mota.require('var', 'loading').once('loaded', () => { layer.getLayer('event')?.extends(door); layer.getLayer('event')?.extends(shadow); layer.extends(animate); + layer.extends(viewport); render.appendChild(layer); // console.log(render); diff --git a/src/core/render/item.ts b/src/core/render/item.ts index fe37c50..0e3bce2 100644 --- a/src/core/render/item.ts +++ b/src/core/render/item.ts @@ -119,6 +119,7 @@ export interface ERenderItemEvent { interface TickerDelegation { fn: TickerFn; + timeout?: number; endFn?: () => void; } @@ -179,11 +180,18 @@ export abstract class RenderItem protected cacheDirty: boolean = true; /** 是否启用缓存机制 */ readonly enableCache: boolean = true; + /** 是否启用transform下穿机制 */ + readonly transformFallThrough: boolean = false; - constructor(type: RenderItemPosition, enableCache: boolean = true) { + constructor( + type: RenderItemPosition, + enableCache: boolean = true, + transformFallThrough: boolean = false + ) { super(); this.enableCache = enableCache; + this.transformFallThrough = transformFallThrough; this.type = type; this.cache.withGameScale(true); @@ -222,7 +230,7 @@ export abstract class RenderItem if (this.hidden) return; this.emit('beforeRender', transform); this.needUpdate = false; - const tran = this.transform; + const tran = this.transformFallThrough ? transform : this.transform; const ax = -this.anchorX * this.width; const ay = -this.anchorY * this.height; @@ -320,7 +328,7 @@ export abstract class RenderItem RenderItem.tickerMap.set(id, delegation); RenderItem.ticker.add(fn); if (typeof time === 'number' && time < 2147438647 && time > 0) { - setTimeout(() => { + delegation.timeout = window.setTimeout(() => { RenderItem.ticker.remove(fn); end?.(); }, time); @@ -332,6 +340,7 @@ export abstract class RenderItem const delegation = RenderItem.tickerMap.get(id); if (!delegation) return false; RenderItem.ticker.remove(delegation.fn); + window.clearTimeout(delegation.timeout); if (callEnd) delegation.endFn?.(); return true; } diff --git a/src/core/render/preset/damage.ts b/src/core/render/preset/damage.ts index 77dd9c2..9f40815 100644 --- a/src/core/render/preset/damage.ts +++ b/src/core/render/preset/damage.ts @@ -165,7 +165,7 @@ export class Damage extends Sprite { private needUpdateBlocks: Set = new Set(); constructor() { - super('absolute', false); + super('absolute', false, true); this.block = new BlockCacher(0, 0, core._WIDTH_, 1); this.type = 'absolute'; @@ -175,11 +175,11 @@ export class Damage extends Sprite { this.damageMap.setAntiAliasing(true); this.damageMap.size(core._PX_, core._PY_); - this.setRenderFn((canvas, camera) => { + this.setRenderFn((canvas, transform) => { const { ctx } = canvas; const { width, height } = canvas; ctx.imageSmoothingEnabled = false; - this.renderDamage(camera); + this.renderDamage(transform); ctx.drawImage(this.damageMap.canvas, 0, 0, width, height); }); } diff --git a/src/core/render/preset/layer.ts b/src/core/render/preset/layer.ts index 3faf837..8379d2c 100644 --- a/src/core/render/preset/layer.ts +++ b/src/core/render/preset/layer.ts @@ -107,11 +107,14 @@ export class LayerGroup extends Container implements IAnimateFrame { /** 地图显示层 */ layers: Map = new Map(); + /** 这个地图组的摄像机 */ + camera: Transform = new Transform(); + private needRender?: NeedRenderData; private extend: Map = new Map(); constructor() { - super('static'); + super('static', true); this.setHD(true); this.setAntiAliasing(false); @@ -128,6 +131,17 @@ export class LayerGroup extends Container implements IAnimateFrame { binder.bindThis(); } + protected render(canvas: MotaOffscreenCanvas2D): void { + const { ctx } = canvas; + + this.sortedChildren.forEach(v => { + if (v.hidden) return; + ctx.save(); + v.renderContent(canvas, this.camera); + ctx.restore(); + }); + } + /** * 添加渲染拓展,可以将渲染拓展理解为一类插件,通过指定的函数在对应时刻执行一些函数, * 来达到执行自己想要的功能的效果。例如样板自带的勇士渲染、伤害渲染等都由此实现。 @@ -633,7 +647,7 @@ export class Layer extends Container { protected backMap: MotaOffscreenCanvas2D = new MotaOffscreenCanvas2D(); /** 最终渲染至的Sprite */ - main: Sprite = new Sprite('static', false); + main: Sprite = new Sprite('absolute', false, true); /** 渲染的层 */ layer?: FloorLayer; @@ -670,7 +684,7 @@ export class Layer extends Container { moving: Set = new Set(); constructor() { - super('absolute', false); + super('absolute', false, true); // this.setHD(false); this.setAntiAliasing(false); diff --git a/src/core/render/preset/viewport.ts b/src/core/render/preset/viewport.ts new file mode 100644 index 0000000..1db9007 --- /dev/null +++ b/src/core/render/preset/viewport.ts @@ -0,0 +1,390 @@ +import { logger } from '@/core/common/logger'; +import { HeroRenderer } from './hero'; +import { ILayerGroupRenderExtends, LayerGroup } from './layer'; +import { Transform } from '../transform'; +import { LayerGroupFloorBinder } from './floor'; +import { hyper, TimingFn } from 'mutate-animate'; +import { RenderAdapter } from '../adapter'; + +export class FloorViewport implements ILayerGroupRenderExtends { + id: string = 'viewport'; + + group!: LayerGroup; + hero!: HeroRenderer; + transform!: Transform; + binder!: LayerGroupFloorBinder; + + /** 是否启用视角控制拓展 */ + enabled: boolean = true; + + /** 渐变的速率曲线 */ + transitionFn: TimingFn = hyper('sin', 'out'); + /** 加减速的速率曲线 */ + movingEaseFn: TimingFn = t => t ** 2; + + /** 突变时的渐变时长 */ + transitionTime: number = 600; + + /** 当前视角移动速度 */ + private speedX: number = 0; + private speedY: number = 0; + /** X方向移动状态,0表示静止,1表示加速过程,2表示匀速过程,3表示减速过程 */ + private movingStatusX: 0 | 1 | 2 | 3 = 0; + /** X方向加减速过程进度 */ + private movingEaseProgressX: number = 0; + /** Y方向移动状态,0表示静止,1表示加速过程,2表示匀速过程,3表示减速过程 */ + private movingStatusY: 0 | 1 | 2 | 3 = 0; + /** Y方向加减速过程进度 */ + private movingEaseProgressY: number = 0; + /** X方向移动结束坐标 */ + private movingEndX: number = 0; + /** X方向移动结束坐标 */ + private movingEndY: number = 0; + /** X方向进入第一阶段的时刻 */ + private movingAccX: number = 0; + /** Y方向进入第一阶段的时刻 */ + private movingAccY: number = 0; + /** X方向进入第二阶段的时刻 */ + private movingConstantX: number = 0; + /** Y方向进入第二阶段的时刻 */ + private movingConstantY: number = 0; + /** X方向进入第三阶段的时刻 */ + private movingDeaccX: number = 0; + /** Y方向进入第三阶段的时刻 */ + private movingDeaccY: number = 0; + /** X方向进入第一阶段的横坐标 */ + private movingAccPosX: number = 0; + /** Y方向进入第一阶段的横坐标 */ + private movingAccPosY: number = 0; + /** X方向进入第二阶段的横坐标 */ + private movingConstantPosX: number = 0; + /** Y方向进入第二阶段的横坐标 */ + private movingConstantPosY: number = 0; + /** X方向进入第三阶段的横坐标 */ + private movingDeaccPosX: number = 0; + /** Y方向进入第三阶段的横坐标 */ + private movingDeaccPosY: number = 0; + /** X方向进入第一阶段的进度 */ + private movingAccProgressX: number = 0; + /** Y方向进入第一阶段的进度 */ + private movingAccProgressY: number = 0; + /** X方向进入第三阶段的进度 */ + private movingDeaccProgressX: number = 0; + /** Y方向进入第三阶段的进度 */ + private movingDeaccProgressY: number = 0; + /** 移动的加减速时长 */ + private movingEaseTime: number = 0; + + /** 当前视角位置 */ + private nx: number = 0; + private ny: number = 0; + + /** 委托ticker */ + private delegation: number = -1; + /** 渐变委托ticker */ + private transition: number = -1; + /** 移动委托ticker */ + private moving: number = -1; + /** 是否在渐变过程中 */ + private inTransition: boolean = false; + /** 是否在移动过程中 */ + private inMoving: boolean = false; + + /** + * 禁用自动视角控制 + */ + disable() { + this.enabled = false; + } + + /** + * 启用自动视角控制 + */ + enable() { + this.enabled = true; + } + + /** + * 传入视角的目标位置,将其限定在地图范围内后返回 + * @param x 图格横坐标 + * @param y 图格纵坐标 + */ + getBoundedPosition(x: number, y: number) { + const width = core._WIDTH_; + const height = core._HEIGHT_; + const minX = (width - 1) / 2; + const minY = (height - 1) / 2; + const floor = core.status.maps[this.binder.getFloor()]; + const maxX = floor.width - minX - 1; + const maxY = floor.height - minY - 1; + + // return { x, y }; + return { x: core.clamp(x, minX, maxX), y: core.clamp(y, minY, maxY) }; + } + + /** + * 设置视角位置 + * @param x 目标图格横坐标 + * @param y 目标图格纵坐标 + */ + setPosition(x: number, y: number) { + const { x: nx, y: ny } = this.getBoundedPosition(x, y); + this.group.removeTicker(this.transition, false); + this.nx = nx; + this.ny = ny; + } + + /** + * 当勇士通过移动改变至指定位置时移动视角 + * @param x 目标图格横坐标 + * @param y 目标图格纵坐标 + */ + moveTo(x: number, y: number) { + const { x: nx, y: ny } = this.getBoundedPosition(x, y); + if (this.inTransition) { + const distance = Math.hypot(this.nx - nx, this.ny - ny); + const time = core.clamp(distance * 200, 200, 600); + this.createTransition(nx, ny, time); + } else { + this.createTransition(nx, ny, 200); + // const moveSpeed = 1000 / this.hero.speed; + // this.speedX = moveSpeed; + // this.speedY = moveSpeed; + // this.movingEndX = x; + // this.movingEndY = y; + // this.movingEaseTime = moveSpeed * 2; + // this.processMoving(x, y); + } + } + + private processMoving(x: number, y: number) { + this.movingEndX = x; + this.movingEndY = y; + if (!this.inMoving) { + this.movingStatusX = 0; + this.movingEaseProgressX = 0; + this.movingStatusY = 0; + this.movingEaseProgressY = 0; + this.inMoving = true; + } else { + } + } + + /** + * 当勇士位置突变至指定位置时移动视角 + * @param x 目标图格横坐标 + * @param y 目标图格纵坐标 + */ + mutateTo(x: number, y: number) { + const { x: nx, y: ny } = this.getBoundedPosition(x, y); + this.createTransition(nx, ny, this.transitionTime); + } + + private createTransition(x: number, y: number, time: number) { + const start = Date.now(); + const end = start + time; + const sx = this.nx; + const sy = this.ny; + const dx = x - sx; + const dy = y - sy; + this.speedX = 0; + this.speedY = 0; + + this.inTransition = true; + this.group.removeTicker(this.transition, false); + this.transition = this.group.delegateTicker( + () => { + const now = Date.now(); + if (now >= end) { + this.group.removeTicker(this.transition, true); + return; + } + const progress = this.transitionFn((now - start) / time); + const tx = dx * progress; + const ty = dy * progress; + this.nx = tx + sx; + this.ny = ty + sy; + }, + time, + () => { + this.nx = x; + this.ny = y; + this.inTransition = false; + } + ); + } + + private createMoving() { + this.moving = this.group.delegateTicker(() => { + const nx = this.nx; + const ny = this.ny; + const now = Date.now(); + const dx = Math.sign(this.movingEndX - nx); + const dy = Math.sign(this.movingEndY - ny); + const fn = this.movingEaseFn; + // 进行进度判断 + if (this.movingEndX !== nx && this.movingStatusX === 0) { + this.movingStatusX = 1; + this.movingAccX = now; + this.movingAccProgressX = this.movingEaseProgressX; + this.movingAccPosX = nx - fn(this.movingAccProgressX) * dx; + } + if (this.movingEndY !== ny && this.movingStatusY === 0) { + this.movingEaseProgressY = 1; + this.movingAccY = now; + this.movingAccProgressY = this.movingEaseProgressY; + this.movingAccPosY = ny - fn(this.movingAccProgressY) * dy; + } + if ( + Math.abs(this.movingEndX - nx) <= 1 && + this.movingStatusX !== 3 + ) { + this.movingStatusX = 3; + this.movingDeaccX = now; + this.movingDeaccProgressX = this.movingEaseProgressX; + this.movingDeaccPosX = + nx - (fn(this.movingDeaccProgressX) + 1) * dx; + } + if ( + Math.abs(this.movingEndY - ny) <= 1 && + this.movingStatusY !== 3 + ) { + this.movingStatusY = 3; + this.movingDeaccY = now; + this.movingDeaccProgressY = this.movingEaseProgressY; + this.movingDeaccPosY = + ny - (fn(this.movingDeaccProgressY) + 1) * dy; + } + if (this.movingEaseProgressX >= 1 && this.movingStatusX === 1) { + this.movingStatusX = 2; + this.movingConstantX = now; + this.movingConstantPosX = nx; + } + if (this.movingEaseProgressY >= 1 && this.movingStatusY === 1) { + this.movingStatusY = 2; + this.movingConstantY = now; + this.movingConstantPosY = ny; + } + if (this.movingEaseProgressX <= 0 && this.movingStatusX === 3) { + this.nx = this.movingEndX; + this.movingStatusX = 0; + this.movingEaseProgressX = 0; + } + if (this.movingEaseProgressY <= 0 && this.movingStatusY === 3) { + this.ny = this.movingEndY; + this.movingStatusY = 0; + this.movingEaseProgressY = 0; + } + + // 平滑视角位置计算 + if (this.movingEaseProgressX === 1) { + // 加速阶段 + this.movingEaseProgressX = + (now - this.movingAccX) / this.movingEaseTime; + const tx = fn(this.movingEaseProgressX) * dx; + this.nx = this.movingAccPosX + tx; + } else if (this.movingEaseProgressX === 2) { + // 匀速阶段 + const time = now - this.movingConstantX; + this.nx = this.movingConstantPosX + time * this.speedX * dx; + } else if (this.movingEaseProgressX === 3) { + // 减速阶段 + this.movingEaseProgressX = + 1 - (now - this.movingDeaccX) / this.movingEaseTime; + const tx = fn(this.movingEaseProgressX) * dx; + this.nx = this.movingDeaccPosX + tx; + } + + if (this.movingEaseProgressY === 1) { + // 加速阶段 + this.movingEaseProgressY = + (now - this.movingAccY) / this.movingEaseTime; + const ty = fn(this.movingEaseProgressY) * dy; + this.ny = this.movingAccPosY + ty; + } else if (this.movingEaseProgressY === 2) { + // 匀速阶段 + const time = now - this.movingConstantY; + this.ny = this.movingConstantPosY + time * this.speedY * dy; + } else if (this.movingEaseProgressY === 3) { + // 减速阶段 + this.movingEaseProgressY = + 1 - (now - this.movingDeaccY) / this.movingEaseTime; + const ty = fn(this.movingEaseProgressY) * dy; + this.ny = this.movingDeaccPosY + ty; + } + }); + } + + private create() { + let nx = this.nx; + let ny = this.ny; + const halfWidth = core._PX_ / 2; + const halfHeight = core._PY_ / 2; + this.delegation = this.group.delegateTicker(() => { + if (this.nx === nx && this.ny === ny) return; + const cell = this.group.cellSize; + const half = cell / 2; + nx = this.nx; + ny = this.ny; + const ox = nx * cell; + const oy = ny * cell; + core.bigmap.offsetX = ox; + core.bigmap.offsetY = oy; + + this.group.camera.setTranslate( + -ox + halfWidth - half, + -oy + halfHeight - half + ); + this.group.update(this.group); + }); + // this.createMoving(); + } + + awake(group: LayerGroup): void { + this.group = group; + this.transform = group.transform; + const ex1 = group.getLayer('event')?.getExtends('floor-hero'); + const ex2 = group.getExtends('floor-binder'); + if ( + ex1 instanceof HeroRenderer && + ex2 instanceof LayerGroupFloorBinder + ) { + this.hero = ex1; + this.binder = ex2; + this.create(); + adapter.add(this); + } else { + logger.error( + 15, + `Viewport extends needs 'floor-hero' extends as dependency.` + ); + group.removeExtends('viewport'); + } + } + + onDestroy(group: LayerGroup): void { + group.removeTicker(this.delegation); + group.removeTicker(this.transition); + group.removeTicker(this.moving); + adapter.remove(this); + } +} + +const adapter = new RenderAdapter('viewport'); +adapter.receive('mutateTo', (item, x, y) => { + item.mutateTo(x, y); + return Promise.resolve(); +}); +adapter.receive('moveTo', (item, x, y) => { + item.moveTo(x, y); + return Promise.resolve(); +}); +adapter.receive('setPosition', (item, x, y) => { + item.setPosition(x, y); + return Promise.resolve(); +}); + +const hook = Mota.require('var', 'hook'); +hook.on('changingFloor', (_, loc) => { + adapter.all('setPosition', loc.x, loc.y); +}); diff --git a/src/core/render/sprite.ts b/src/core/render/sprite.ts index ceafd9d..08ad016 100644 --- a/src/core/render/sprite.ts +++ b/src/core/render/sprite.ts @@ -19,8 +19,12 @@ export class Sprite extends RenderItem< * @param type 渲染模式,absolute表示绝对位置,不会跟随自身的Transform改变 * @param cache 是否启用缓存机制 */ - constructor(type: RenderItemPosition = 'static', cache: boolean = true) { - super(type, cache); + constructor( + type: RenderItemPosition = 'static', + cache: boolean = true, + fall: boolean = false + ) { + super(type, cache, fall); this.type = type; this.renderFn = () => {}; } diff --git a/src/plugin/fx/portal.ts b/src/plugin/fx/portal.ts index e65daab..c3f1ae2 100644 --- a/src/plugin/fx/portal.ts +++ b/src/plugin/fx/portal.ts @@ -95,7 +95,7 @@ export class Portal extends Sprite { private delegation: number; constructor() { - super('static', false); + super('static', false, true); this.particleSetting = mainSetting.getSetting('fx.portalParticle')!; diff --git a/src/plugin/game/fallback.ts b/src/plugin/game/fallback.ts index 4f52280..def3dbc 100644 --- a/src/plugin/game/fallback.ts +++ b/src/plugin/game/fallback.ts @@ -10,6 +10,7 @@ import { BluePalace } from '@/game/mechanism/misc'; import { backDir } from './utils'; import type { TimingFn } from 'mutate-animate'; import EventEmitter from 'eventemitter3'; +import type { FloorViewport } from '@/core/render/preset/viewport'; // 向后兼容用,会充当两个版本间过渡的作用 @@ -18,6 +19,7 @@ interface Adapters { 'door-animate'?: RenderAdapter; animate?: RenderAdapter; layer?: RenderAdapter; + viewport?: RenderAdapter; } interface MoveEvent { @@ -38,11 +40,13 @@ export function init() { const doorAnimate = Adapter.get('door-animate'); const animate = Adapter.get('animate'); const layer = Adapter.get('layer'); + const viewport = Adapter.get('viewport'); adapters['hero-adapter'] = hero; adapters['door-animate'] = doorAnimate; adapters['animate'] = animate; adapters['layer'] = layer; + adapters['viewport'] = viewport; } let moving: boolean = false; @@ -193,6 +197,17 @@ export function init() { } stepEnding = adapter.all('move', moveDir); + if (portal && portalData) { + adapters.viewport?.all( + 'mutateTo', + portalData.x, + portalData.y + ); + } else { + const { nx, ny } = getNextLoc(); + adapters.viewport?.all('moveTo', nx, ny); + } + await stepEnding; moveEmit.emit('stepEnd'); @@ -260,11 +275,13 @@ export function init() { callback?: () => void ) { if (portal && portalData) { + const before = moveDir; const { x, y, dir } = portalData; core.setHeroLoc('x', x); core.setHeroLoc('y', y); core.setHeroLoc('direction', dir); portal = false; + moveDir = before; } else if (!noPass) { const { nx, ny } = getNextLoc(); core.setHeroLoc('x', nx, true); @@ -929,6 +946,7 @@ export function init() { if (moving) return; const sx = core.getHeroLoc('x'); const sy = core.getHeroLoc('y'); + adapters.viewport?.all('mutateTo', ex, ey); const locked = core.status.lockControl; core.lockControl(); @@ -956,6 +974,18 @@ export function init() { core.setHeroLoc('y', ey); callback?.(); }; + + // ----- 视角处理 + + ////// 瞬间移动 ////// + control.prototype.moveDirectly = function ( + destX: number, + destY: number, + ignoreSteps: number + ) { + adapters.viewport?.all('mutateTo', destX, destY); + return this.controldata.moveDirectly(destX, destY, ignoreSteps); + }; }); loading.once('loaded', () => {