-
-
Notifications
You must be signed in to change notification settings - Fork 353
fix: #987 next 后接场景跳转失效问题 #995
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,6 @@ | ||
| import { IPerform } from '@/Core/Modules/perform/performInterface'; | ||
| import { ISentence } from '@/Core/controller/scene/sceneInterface'; | ||
| import { nextSentence } from '@/Core/controller/gamePlay/nextSentence'; | ||
| import { continueSentence } from '@/Core/controller/gamePlay/nextSentence'; | ||
| import { WEBGAL_NONE } from '@/Core/constants'; | ||
| import { getBooleanArgByKey } from '@/Core/util/getSentenceArg'; | ||
| import { stageStateManager } from '@/Core/Modules/stage/stageStateManager'; | ||
|
|
@@ -110,15 +110,37 @@ export class PerformController { | |
| return this.pendingPerformList.some(({ perform }) => perform.blockingStateCalculation?.() ?? false); | ||
| } | ||
|
|
||
| /** | ||
| * 是否存在正在运行且阻塞下一步的演出。 | ||
| * | ||
| * blockingNext 是最强的运行时推进阻塞:用户 next、内部 continue 和 forward 都要等待它解除。 | ||
| */ | ||
| public hasBlockingNextPerform() { | ||
| return this.performList.some((e) => e.blockingNext()); | ||
| } | ||
|
|
||
| /** | ||
| * 是否存在可以被下一步提前结束的普通演出。 | ||
| * | ||
| * 这不是一种阻塞。它表示当前 next/continue 需要先结算这些非 hold 演出, | ||
| * 让它们的 stopFunction 和状态清理完成后,再决定是否继续推进。 | ||
| */ | ||
| public hasUnsettledNonHoldPerform() { | ||
| return this.performList.some((e) => !e.isHoldOn && !e.skipNextCollect); | ||
| } | ||
|
|
||
| public settleNonHoldPerforms() { | ||
| /** | ||
| * 结算当前正在运行的普通非 hold 演出。 | ||
| * | ||
| * 用户 next 调用时,goNextWhenOver 应传 true,保留旧语义: | ||
| * 如果被提前结束的演出要求结束后继续,则由 perform 自己触发内部继续。 | ||
| * | ||
| * 内部 continue 调用时,goNextWhenOver 应传 false: | ||
| * 调用方会在结算后立即继续 forward,不能再让旧 perform 额外触发一次继续。 | ||
| * | ||
| * @param goNextWhenOver 是否消费被结算演出的 goNextWhenOver 标记。 | ||
| */ | ||
| public settleNonHoldPerforms(goNextWhenOver = true) { | ||
| let isGoNext = false; | ||
| for (let i = 0; i < this.performList.length; i++) { | ||
| const e = this.performList[i]; | ||
|
|
@@ -136,15 +158,21 @@ export class PerformController { | |
| } | ||
| } | ||
| stageStateManager.commit(); | ||
| if (isGoNext) { | ||
| nextSentence(); | ||
| if (isGoNext && goNextWhenOver) { | ||
| continueSentence(); | ||
| } | ||
| } | ||
|
|
||
| public clearNonHoldPerformsFromStageState() { | ||
| stageStateManager.clearUncommittedNonHoldPerforms(); | ||
| } | ||
|
|
||
| /** | ||
| * 启动一个已提交的 perform。 | ||
| * | ||
| * startFunction 只会在 commitPendingPerforms 之后运行,因此可以依赖已提交的 stage state。 | ||
| * 这里同时把脚本层的 -continue 转成 perform.goNextWhenOver。 | ||
| */ | ||
| private startPerform(perform: IPerform, script: ISentence) { | ||
| perform.isStarted = true; | ||
| perform.startFunction?.(); | ||
|
|
@@ -186,15 +214,14 @@ export class PerformController { | |
| this.clearPerformTimeout(e); | ||
| /** | ||
| * 在演出列表里删除演出对象的操作必须在调用 goNextWhenOver 之前 | ||
| * 因为 goNextWhenOver 会调用 nextSentence,而 nextSentence 会清除目前未结束的演出 | ||
| * 那么 nextSentence 函数就会删除这个演出,但是此时,在这个上下文,i 已经被确定了 | ||
| * 因为 goNextWhenOver 会触发继续推进,而继续推进会清除目前未结束的演出 | ||
| * 那么继续推进就会删除这个演出,但是此时,在这个上下文,i 已经被确定了 | ||
| * 所以 goNextWhenOver 后的代码会多删东西,解决方法就是在调用 goNextWhenOver 前先删掉这个演出对象 | ||
| * 此问题对所有 goNextWhenOver 属性为真的演出都有影响,但只有 2 个演出有此问题 | ||
| */ | ||
| this.performList.splice(i, 1); | ||
| i--; | ||
| if (e.goNextWhenOver) { | ||
| // nextSentence(); | ||
| this.goNextWhenOver(); | ||
| } | ||
| this.erasePerformFromState(name); | ||
|
|
@@ -212,7 +239,6 @@ export class PerformController { | |
| this.performList.splice(i, 1); | ||
| i--; | ||
| if (e.goNextWhenOver) { | ||
| // nextSentence(); | ||
| this.goNextWhenOver(); | ||
| } | ||
| /** | ||
|
|
@@ -259,16 +285,15 @@ export class PerformController { | |
| this.clearPerformTimeout(perform); | ||
| /** | ||
| * 在演出列表里删除演出对象的操作必须在调用 goNextWhenOver 之前 | ||
| * 因为 goNextWhenOver 会调用 nextSentence,而 nextSentence 会清除目前未结束的演出 | ||
| * 那么 nextSentence 函数就会删除这个演出,但是此时,在这个上下文,i 已经被确定了 | ||
| * 因为 goNextWhenOver 会触发继续推进,而继续推进会清除目前未结束的演出 | ||
| * 那么继续推进就会删除这个演出,但是此时,在这个上下文,i 已经被确定了 | ||
| * 所以 goNextWhenOver 后的代码会多删东西,解决方法就是在调用 goNextWhenOver 前先删掉这个演出对象 | ||
| * 此问题对所有 goNextWhenOver 属性为真的演出都有影响,但只有 2 个演出有此问题 | ||
| */ | ||
| this.performList.splice(idx, 1); | ||
| this.erasePerformFromState(perform.performName); | ||
| stageStateManager.commit(); | ||
| if (perform.goNextWhenOver) { | ||
| // nextSentence(); | ||
| this.goNextWhenOver(); | ||
| } | ||
| } | ||
|
|
@@ -300,6 +325,12 @@ export class PerformController { | |
| perform.isStarted = false; | ||
| } | ||
|
|
||
| /** | ||
| * perform 结束后的内部继续推进。 | ||
| * | ||
| * goNextWhenOver 不等于无条件跳下一句;它仍然必须等待所有 blockingNext 演出结束。 | ||
| * 等待完成后使用 continueSentence,避免触发 userInteractNext,也避免把自然结束误当成用户点击。 | ||
| */ | ||
| private goNextWhenOver = () => { | ||
| let isBlockingNext = false; | ||
| this.performList?.forEach((e) => { | ||
|
Comment on lines
334
to
336
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 在这里,我们通过 private goNextWhenOver = () => {
const existingTimeout = goNextOverTimeoutMap.get(this);
if (existingTimeout) clearTimeout(existingTimeout);
const timeout = setTimeout(() => {
goNextOverTimeoutMap.delete(this);
let isBlockingNext = false;
this.performList?.forEach((e) => { |
||
|
|
@@ -311,7 +342,7 @@ export class PerformController { | |
| // 有阻塞,提前结束 | ||
| setTimeout(this.goNextWhenOver, 100); | ||
| } else { | ||
| nextSentence(); | ||
| continueSentence(); | ||
| } | ||
| }; | ||
|
Comment on lines
342
to
347
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 配合顶部的防抖逻辑,在定时器回调中,如果检测到仍有阻塞的演出,则在 100ms 后重新调度;否则安全地调用 // 有阻塞,提前结束
const retryTimeout = setTimeout(this.goNextWhenOver, 100);
goNextOverTimeoutMap.set(this, retryTimeout);
} else {
continueSentence();
}
}, 0);
goNextOverTimeoutMap.set(this, timeout);
}; |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,9 +6,18 @@ import { WebGAL } from '@/Core/WebGAL'; | |
| import { stageStateManager } from '@/Core/Modules/stage/stageStateManager'; | ||
|
|
||
| /** | ||
| * 步进前工作:检查阻塞,并在当前演出未完成时提前结束普通演出。 | ||
| * 执行一次推进前检查。 | ||
| * | ||
| * 这里处理三种“不能直接进入下一条语句”的情况: | ||
| * 1. 场景正在异步写入时,任何推进都必须停止。 | ||
| * 2. 存在 blockingNext 的演出时,用户推进和内部推进都必须等待。 | ||
| * 3. 存在可提前结束的非 hold 演出时,用户推进只负责结束演出并停下; | ||
| * 内部继续推进会先结束演出,然后继续执行下一条语句。 | ||
| * | ||
| * @param continueAfterSettling 是否在清理普通非 hold 演出后继续推进。 | ||
| * @returns true 表示可以继续调用 forward/commitForward。 | ||
| */ | ||
| export const preForward = () => { | ||
| export const preForward = (continueAfterSettling = false) => { | ||
| if (WebGAL.sceneManager.lockSceneWrite) { | ||
| logger.warn('next 被场景切换阻塞!'); | ||
| return false; | ||
|
|
@@ -22,15 +31,19 @@ export const preForward = () => { | |
| const hasUnsettledNonHoldPerform = WebGAL.gameplay.performController.hasUnsettledNonHoldPerform(); | ||
| if (hasUnsettledNonHoldPerform) { | ||
| logger.debug('提前结束被触发,现在清除普通演出'); | ||
| WebGAL.gameplay.performController.settleNonHoldPerforms(); | ||
| return false; | ||
| // 用户 next 不消费 goNextWhenOver;内部继续推进会自己接着 forward,避免重复触发下一步。 | ||
| WebGAL.gameplay.performController.settleNonHoldPerforms(!continueAfterSettling); | ||
| return continueAfterSettling; | ||
| } | ||
|
|
||
| return true; | ||
| }; | ||
|
|
||
| /** | ||
| * 执行一条语句或由 -next 连接的语句序列,只修改演算状态并收集演出。 | ||
| * 执行一条语句或由 -next 连接的语句序列。 | ||
| * | ||
| * forward 只推进 calculationStageState,并把命令返回的 perform 收集到 pending 列表; | ||
| * 它不会提交视图状态,也不会启动 perform。调用方必须在合适时机调用 commitForward。 | ||
| */ | ||
| export const forward = () => { | ||
| if (WebGAL.sceneManager.lockSceneWrite) { | ||
|
|
@@ -55,16 +68,44 @@ export const forward = () => { | |
| }; | ||
|
|
||
| /** | ||
| * 将演算状态提交到当前视图状态,并启动本序列收集到的演出。 | ||
| * 将本轮 forward 的演算结果提交到视图,并启动 pending perform。 | ||
| * | ||
| * 提交流程分三步:先提交 stage state,再启动 perform,最后应用 Pixi effects。 | ||
| * 这个顺序保证 startFunction 看到的是已提交的视图状态。 | ||
| */ | ||
| export const commitForward = () => { | ||
| stageStateManager.commit({ applyPixiEffects: false }); | ||
| WebGAL.gameplay.performController.commitPendingPerforms(); | ||
| stageStateManager.applyCommittedPixiEffects(); | ||
| }; | ||
|
|
||
| /** | ||
| * 内部继续推进。 | ||
| * | ||
| * 供场景切换完成、perform 自然结束、输入控件提交等内核流程调用。 | ||
| * 它不会触发 userInteractNext,因此不会把“内部自动继续”误判为用户点击。 | ||
| * 如果当前只剩可提前结束的非 hold 演出,会先结算它们并继续执行下一条语句。 | ||
| */ | ||
| export const continueSentence = () => { | ||
| const GUIState = webgalStore.getState().GUI; | ||
| if (GUIState.showTitle) { | ||
| return; | ||
| } | ||
|
|
||
| if (!preForward(true)) { | ||
| return; | ||
| } | ||
|
|
||
| forward(); | ||
| commitForward(); | ||
|
Comment on lines
+99
to
+100
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| }; | ||
|
|
||
| /** | ||
| * 用户操作步进。 | ||
| * | ||
| * 供点击、键盘、自动播放和快进等“外部下一步”入口调用。 | ||
| * 它会触发 userInteractNext,让 intro 等演出先响应用户输入。 | ||
| * 如果当前存在可提前结束的普通演出,本次用户推进只结束演出,不再继续执行下一条语句。 | ||
| */ | ||
| export const nextSentence = () => { | ||
| WebGAL.events.userInteractNext.emit(); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,9 @@ | ||
| import { nextSentence } from '@/Core/controller/gamePlay/nextSentence'; | ||
| import { continueSentence } from '@/Core/controller/gamePlay/nextSentence'; | ||
| import { jumpToLabel } from '@/Core/gameScripts/label/jumpToLabel'; | ||
|
|
||
| export const jmp = (labelName: string, autoNext = true) => { | ||
| const isJumped = jumpToLabel(labelName); | ||
| if (isJumped && autoNext) { | ||
| setTimeout(nextSentence, 1); | ||
| setTimeout(continueSentence, 1); | ||
| } | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
为了解决多个并行演出(parallel performs)在同一帧/同一 Tick 结束或被卸载时,多次触发
goNextWhenOver导致游戏意外跳过多个句子的潜在 Bug,建议对goNextWhenOver进行防抖(debounce)处理。我们可以在文件顶部声明一个
goNextOverTimeoutMap,用于存储每个PerformController实例的定时器。