diff --git a/packages/webgal/src/Core/controller/stage/pixi/PixiController.ts b/packages/webgal/src/Core/controller/stage/pixi/PixiController.ts index 76ec7f3d6..15924905c 100644 --- a/packages/webgal/src/Core/controller/stage/pixi/PixiController.ts +++ b/packages/webgal/src/Core/controller/stage/pixi/PixiController.ts @@ -85,15 +85,14 @@ export default class PixiStage { public readonly mainStageContainer: WebGALPixiContainer; public readonly foregroundEffectsContainer: PIXI.Container; public readonly backgroundEffectsContainer: PIXI.Container; - public frameDuration = 16.67; public notUpdateBacklogEffects = false; public readonly figureContainer: PIXI.Container; - public figureObjects: Array = []; + public figureObjects = this.createReactiveList([]); public stageWidth = SCREEN_CONSTANTS.width; public stageHeight = SCREEN_CONSTANTS.height; public assetLoader = new PIXI.Loader(); public readonly backgroundContainer: PIXI.Container; - public backgroundObjects: Array = []; + public backgroundObjects = this.createReactiveList([]); public mainStageObject: IStageObject; /** * 添加 Spine 立绘 @@ -104,11 +103,15 @@ export default class PixiStage { public addSpineFigure = addSpineFigureImpl.bind(this); public addSpineBg = addSpineBgImpl.bind(this); // 注册到 Ticker 上的函数 - private stageAnimations: Array = []; + private stageAnimations = this.createReactiveList([]); private loadQueue: { url: string; callback: () => void; name?: string }[] = []; private live2dFigureRecorder: Array = []; // 锁定变换对象(对象可能正在执行动画,不能应用变换) private lockTransformTarget: Array = []; + // 手动请求渲染防抖标记 + private isRenderPending = false; + // 更新 ticker 状态的防抖标记 + private isTickerUpdatePending = false; /** * 暂时没用上,以后可能用 @@ -121,6 +124,7 @@ export default class PixiStage { const app = new PIXI.Application({ backgroundAlpha: 0, preserveDrawingBuffer: true, + autoStart: false, }); // @ts-ignore @@ -182,19 +186,26 @@ export default class PixiStage { this.backgroundContainer, ); this.currentApp = app; - // 每 5s 获取帧率,并且防 loader 死 - const update = () => { - this.updateFps(); - setTimeout(update, 10000); - }; - update(); // loader 防死 const reload = () => { setTimeout(reload, 500); this.callLoader(); }; reload(); - this.initialize().then(() => {}); + this.initialize(); + this.requestRender(); + } + + public requestRender() { + if (this.isRenderPending) return; + this.isRenderPending = true; + + requestAnimationFrame(() => { + this.isRenderPending = false; + if (!this.currentApp?.ticker.started) { + this.currentApp?.render(); + } + }); } public getFigureObjects() { @@ -346,6 +357,7 @@ export default class PixiStage { return; } sprite.texture = texture; + this.requestRender(); }); } @@ -374,6 +386,7 @@ export default class PixiStage { return; } sprite.texture = texture; + this.requestRender(); }); } @@ -436,6 +449,7 @@ export default class PixiStage { // 挂载 thisBgContainer.addChild(bgSprite); + this.requestRender(); } }, 0); }; @@ -610,6 +624,7 @@ export default class PixiStage { } thisFigureContainer.pivot.set(0, this.stageHeight / 2); thisFigureContainer.addChild(figureSprite); + this.requestRender(); } }, 0); }; @@ -1075,13 +1090,6 @@ export default class PixiStage { } } - private updateFps() { - getScreenFps?.(120).then((fps) => { - this.frameDuration = 1000 / (fps as number); - // logger.info('当前帧率', fps); - }); - } - private lockStageObject(targetName: string) { this.lockTransformTarget.push(targetName); } @@ -1100,6 +1108,60 @@ export default class PixiStage { console.error('Failed to load figureCash:', error); } } + + private createReactiveList(array: T[]): T[] { + return new Proxy(array, { + // eslint-disable-next-line max-params + set: (target, property, value, receiver) => { + const result = Reflect.set(target, property, value, receiver); + this.updateTickerStatus(); + return result; + }, + deleteProperty: (target, property) => { + const result = Reflect.deleteProperty(target, property); + this.updateTickerStatus(); + return result; + }, + }); + } + + private updateTickerStatus() { + if (this.isTickerUpdatePending) return; + this.isTickerUpdatePending = true; + + Promise.resolve().then(() => { + this.isTickerUpdatePending = false; + const app = this.currentApp; + if (!app) return; + + const hasActiveAnimations = this.stageAnimations.length > 0; + const allObjects = [...this.figureObjects, ...this.backgroundObjects]; + const hasDynamicObjects = allObjects.some( + (obj) => + obj.sourceType === 'live2d' || + obj.sourceType === 'spine' || + obj.sourceType === 'video' || + obj.sourceType === 'gif', + ); + + const shouldRun = hasActiveAnimations || hasDynamicObjects; + + if (shouldRun) { + if (!app.ticker.started) { + app.ticker.start(); + logger.debug('Ticker: STARTED'); + } + } else { + if (app.ticker.started) { + app.ticker.stop(); + this.currentApp?.render(); + logger.debug('Ticker: STOPPED'); + } else { + this.requestRender(); + } + } + }); + } } function updateCurrentBacklogEffects(newEffects: IEffect[]) { @@ -1112,40 +1174,3 @@ function updateCurrentBacklogEffects(newEffects: IEffect[]) { webgalStore.dispatch(setStage({ key: 'effects', value: newEffects })); } - -/** - * @param {number} targetCount 不小于1的整数,表示经过targetCount帧之后返回结果 - * @return {Promise} - */ -const getScreenFps = (() => { - // 先做一下兼容性处理 - const nextFrame = [ - window.requestAnimationFrame, - // @ts-ignore - window.webkitRequestAnimationFrame, - // @ts-ignore - window.mozRequestAnimationFrame, - ].find((fn) => fn); - if (!nextFrame) { - console.error('requestAnimationFrame is not supported!'); - return; - } - return (targetCount = 60) => { - // 判断参数是否合规 - if (targetCount < 1) throw new Error('targetCount cannot be less than 1.'); - const beginDate = Date.now(); - let count = 0; - return new Promise((resolve) => { - (function log() { - nextFrame(() => { - if (++count >= targetCount) { - const diffDate = Date.now() - beginDate; - const fps = (count / diffDate) * 1000; - return resolve(fps); - } - log(); - }); - })(); - }); - }; -})(); diff --git a/packages/webgal/src/Core/controller/stage/pixi/animations/template.ts b/packages/webgal/src/Core/controller/stage/pixi/animations/template.ts index 74b017a10..9ae4c4c23 100644 --- a/packages/webgal/src/Core/controller/stage/pixi/animations/template.ts +++ b/packages/webgal/src/Core/controller/stage/pixi/animations/template.ts @@ -14,13 +14,13 @@ export function generateTemplateAnimationObj(targetKey: string, duration: number /** * 在此书写为动画设置初态的操作 */ - function setStartState() {} + function setStartState() { } // TODO:通用终态设置 /** * 在此书写为动画设置终态的操作 */ - function setEndState() {} + function setEndState() { } /** * 在此书写动画每一帧执行的函数 @@ -31,7 +31,7 @@ export function generateTemplateAnimationObj(targetKey: string, duration: number // 要操控的精灵 const sprite = target.pixiContainer; // 每一帧的时间 - const baseDuration = WebGAL.gameplay.pixiStage!.frameDuration; + const currentDeltaMS = WebGAL.gameplay.pixiStage!.currentApp!.ticker.deltaMS; /** * 在下面书写具体的动画 diff --git a/packages/webgal/src/Core/controller/stage/pixi/animations/testblur.ts b/packages/webgal/src/Core/controller/stage/pixi/animations/testblur.ts index 496e35fed..5d88127c5 100644 --- a/packages/webgal/src/Core/controller/stage/pixi/animations/testblur.ts +++ b/packages/webgal/src/Core/controller/stage/pixi/animations/testblur.ts @@ -36,8 +36,8 @@ export function generateTestblurAnimationObj(targetKey: string, duration: number function tickerFunc(delta: number) { if (target) { const container = target.pixiContainer; - const baseDuration = WebGAL.gameplay.pixiStage!.frameDuration; - const currentAddOplityDelta = (duration / baseDuration) * delta; + const currentDeltaMS = WebGAL.gameplay.pixiStage!.currentApp!.ticker.deltaMS; + const currentAddOplityDelta = (duration / currentDeltaMS) * delta; const increasement = 1 / currentAddOplityDelta; const decreasement = 5 / currentAddOplityDelta; if (container) diff --git a/packages/webgal/src/Core/controller/stage/pixi/animations/universalSoftIn.ts b/packages/webgal/src/Core/controller/stage/pixi/animations/universalSoftIn.ts index 61f8a3d69..02108bbaf 100644 --- a/packages/webgal/src/Core/controller/stage/pixi/animations/universalSoftIn.ts +++ b/packages/webgal/src/Core/controller/stage/pixi/animations/universalSoftIn.ts @@ -35,9 +35,9 @@ export function generateUniversalSoftInAnimationObj(targetKey: string, duration: function tickerFunc(delta: number) { if (target) { const sprite = target.pixiContainer; - const baseDuration = WebGAL.gameplay.pixiStage!.frameDuration; + const currentDeltaMS = WebGAL.gameplay.pixiStage!.currentApp!.ticker.deltaMS; - elapsedTime += baseDuration; + elapsedTime += currentDeltaMS; const realElapsedTime = Math.min(elapsedTime, duration); const progress = realElapsedTime / duration; diff --git a/packages/webgal/src/Core/controller/stage/pixi/animations/universalSoftOff.ts b/packages/webgal/src/Core/controller/stage/pixi/animations/universalSoftOff.ts index 3f5477457..773536310 100644 --- a/packages/webgal/src/Core/controller/stage/pixi/animations/universalSoftOff.ts +++ b/packages/webgal/src/Core/controller/stage/pixi/animations/universalSoftOff.ts @@ -35,9 +35,9 @@ export function generateUniversalSoftOffAnimationObj(targetKey: string, duration function tickerFunc(delta: number) { if (target) { const targetContainer = target.pixiContainer; - const baseDuration = WebGAL.gameplay.pixiStage!.frameDuration; + const currentDeltaMS = WebGAL.gameplay.pixiStage!.currentApp!.ticker.deltaMS; - elapsedTime += baseDuration; + elapsedTime += currentDeltaMS; const realElapsedTime = Math.min(elapsedTime, duration); const progress = realElapsedTime / duration; diff --git a/packages/webgal/src/Core/controller/storage/jumpFromBacklog.ts b/packages/webgal/src/Core/controller/storage/jumpFromBacklog.ts index 2ebf4ce6b..b8e50caa1 100644 --- a/packages/webgal/src/Core/controller/storage/jumpFromBacklog.ts +++ b/packages/webgal/src/Core/controller/storage/jumpFromBacklog.ts @@ -77,4 +77,7 @@ export const jumpFromBacklog = (index: number, refetchScene = true) => { // 重新显示 TextBox dispatch(setVisibility({ component: 'showTextBox', visibility: true })); + + // 重新渲染 + setTimeout(() => WebGAL.gameplay.pixiStage?.requestRender(), 100); };