perf: 优化通关弹窗

This commit is contained in:
richarjiang
2026-05-06 15:53:08 +08:00
parent 84f45ebfdf
commit 32adc7c467
6 changed files with 957 additions and 335 deletions

View File

@@ -217,6 +217,9 @@ export class PageLevel extends BaseView {
/** 本次通关弹窗使用的已通关数量 */
private _passModalCompletedLevelCount: number | null = null;
/** 本次通关弹窗动画起点(通关前)的已通关数量;为 null 表示不播动画 */
private _passModalPreviousCompletedLevelCount: number | null = null;
/** 错误弹窗实例 */
private _wrongModalNode: Node | null = null;
@@ -1570,8 +1573,11 @@ export class PageLevel extends BaseView {
private reportLevelCompleted(levelId: string, timeSpent: number): void {
if (!this._isShareMode) {
// 乐观更新通关计数(用于称号展示)
const previousCount = AuthManager.instance.completedLevelCount;
AuthManager.instance.addCompletedLevelCount();
this._passModalCompletedLevelCount = AuthManager.instance.completedLevelCount;
// 本次预期为首次通关,起点 = 通关前计数;如果回调回退,则清掉避免误播动画
this._passModalPreviousCompletedLevelCount = previousCount;
void StaminaManager.instance.completeLevel(levelId, timeSpent).then((result) => {
if (result) {
@@ -1582,6 +1588,7 @@ export class PageLevel extends BaseView {
// 非首次通关,回退乐观更新
AuthManager.instance.addCompletedLevelCount(-1);
this._passModalCompletedLevelCount = AuthManager.instance.completedLevelCount;
this._passModalPreviousCompletedLevelCount = null;
}
console.log(`[PageLevel] 通关上报成功,首次通关: ${result.firstClear}, 有下一关: ${!!result.nextLevel}`);
}
@@ -1590,6 +1597,7 @@ export class PageLevel extends BaseView {
}
this._passModalCompletedLevelCount = null;
this._passModalPreviousCompletedLevelCount = null;
// fire-and-forget: errors are logged inside reportLevelProgress
void ShareManager.instance.reportLevelProgress(levelId, true, timeSpent);
}
@@ -1631,9 +1639,17 @@ export class PageLevel extends BaseView {
// 获取 PassModal 组件并设置回调
const passModal = modalNode.getComponent(PassModal);
if (passModal) {
const completedCount = this._getPassModalCompletedLevelCount();
const titleInfo = AchievementTitleManager.getTitleInfo(completedCount);
const previousCompletedCount = this._passModalPreviousCompletedLevelCount;
const previousTitleInfo = (previousCompletedCount !== null && previousCompletedCount !== completedCount)
? AchievementTitleManager.getTitleInfo(previousCompletedCount)
: undefined;
passModal.setParams({
levelIndex: this.getDisplayLevelNumber(),
titleInfo: AchievementTitleManager.getTitleInfo(this._getPassModalCompletedLevelCount())
titleInfo,
previousTitleInfo
});
passModal.setCallbacks({
onNextLevel: () => {
@@ -1645,6 +1661,8 @@ export class PageLevel extends BaseView {
console.log('[PageLevel] 分享完成');
}
});
// 动画消费完一次后清除起点,避免弹窗多次打开时复用
this._passModalPreviousCompletedLevelCount = null;
// 手动调用 onViewLoad 和 onViewShow
passModal.onViewLoad();
passModal.onViewShow();

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
import { _decorator, Node, Label, AudioClip, AudioSource, view, UITransform, Size, ProgressBar } from 'cc';
import { _decorator, Node, Label, AudioClip, AudioSource, view, UITransform, Size, ProgressBar, tween, Tween } from 'cc';
import { BaseModal } from 'db://assets/scripts/core/BaseModal';
import { WxSDK } from 'db://assets/scripts/utils/WxSDK';
const { ccclass, property } = _decorator;
@@ -22,6 +22,12 @@ export interface PassModalTitleInfo {
interface PassModalParams {
levelIndex?: number;
titleInfo?: PassModalTitleInfo;
/**
* 通关前的称号信息。传入后,本次显示会把进度条从该起点动画到 titleInfo 的终点;
* 起点与终点 titleText 不同则分两段(先填满当前等级,再切换到新等级后填到目标进度)。
* 分享模式等无本地进度变化的场景不要传。
*/
previousTitleInfo?: PassModalTitleInfo;
}
/**
@@ -57,22 +63,40 @@ export class PassModal extends BaseModal {
@property(AudioClip)
successAudio: AudioClip | null = null;
/** 进度条动画起始前的等待时长(秒),等弹窗开场动画稳定后再开始 */
private static readonly PROGRESS_ANIM_START_DELAY = 0.4;
/** 单段进度条填充动画时长(秒) */
private static readonly PROGRESS_ANIM_SEGMENT_DURATION = 0.6;
/** 跨称号切换时的等级信息刷新停顿(秒),让玩家看清称号变更 */
private static readonly PROGRESS_ANIM_LEVELUP_PAUSE = 0.12;
/** 回调函数 */
private _callbacks: PassModalCallbacks = {};
/** 缓存的屏幕尺寸 */
private _screenSize: Size | null = null;
/** 称号展示数据 */
/** 称号展示数据(终态) */
private _titleInfo: PassModalTitleInfo = {
titleText: '冷场小白1级',
nextTitleProgress: 0,
progressText: '还差3题获得冷场小白2级'
};
/** 动画起点。为 null 表示不做进度动画,直接展示终态 */
private _previousTitleInfo: PassModalTitleInfo | null = null;
/** 进度动画所绑定的对象,用于 Tween.stopAllByTarget */
private readonly _progressTweenTarget: { progress: number } = { progress: 0 };
setParams(params: PassModalParams): void {
super.setParams(params);
// previousTitleInfo 可以显式传 null 来禁用动画undefined 表示"保持已有状态"
if (params && 'previousTitleInfo' in params) {
this._previousTitleInfo = params.previousTitleInfo ?? null;
}
if (params?.titleInfo) {
this.setTitleInfo(params.titleInfo);
}
@@ -93,7 +117,7 @@ export class PassModal extends BaseModal {
...this._titleInfo,
...titleInfo
};
this._updateTitleInfo();
this._refreshTitleView();
}
/**
@@ -110,14 +134,24 @@ export class PassModal extends BaseModal {
onViewShow(): void {
super.onViewShow();
this._updateWidget();
this._updateTitleInfo();
this._refreshTitleView();
this._playSuccessSound();
this._playProgressAnimation();
}
/**
* 页面隐藏时调用
*/
onViewHide(): void {
super.onViewHide();
this._stopProgressAnimation();
}
/**
* 页面销毁时调用
*/
onViewDestroy(): void {
this._stopProgressAnimation();
this._unbindButtonEvents();
}
@@ -177,20 +211,151 @@ export class PassModal extends BaseModal {
}
/**
* 更新称号体系核心变量
* 用当前 _titleInfo 刷新视图(称号、进度条、进度文案)
* 进度条动画运行时,会由动画控制进度值,这里仍然把进度写为终态
* —— _playProgressAnimation 会在动画开始前覆盖为起点。
*/
private _updateTitleInfo(): void {
if (this.titleLevelLabel && this._titleInfo.titleText !== undefined) {
this.titleLevelLabel.string = this._titleInfo.titleText;
private _refreshTitleView(): void {
this._applyTitleText(this._titleInfo.titleText);
this._applyProgressText(this._titleInfo.progressText);
this._applyProgressValue(this._titleInfo.nextTitleProgress);
}
private _applyTitleText(text: string | undefined): void {
if (this.titleLevelLabel && text !== undefined) {
this.titleLevelLabel.string = text;
}
}
private _applyProgressText(text: string | undefined): void {
if (this.progressLabel && text !== undefined) {
this.progressLabel.string = text;
}
}
private _applyProgressValue(progress: number | undefined): void {
if (this.titleProgressBar && progress !== undefined) {
this.titleProgressBar.progress = this._normalizeProgress(progress);
}
}
/**
* 根据 _previousTitleInfo → _titleInfo 驱动进度条过渡动画
*
* 三种情况:
* 1. 无起点信息或起点/终点相同:不播动画
* 2. 同称号下涨进度:一段 tween
* 3. 跨称号:先把旧称号填到 1.0,然后切换称号文字、进度回 0再 tween 到终点进度
*/
private _playProgressAnimation(): void {
const prev = this._previousTitleInfo;
// 动画是一次性的,播放前消费掉,避免弹窗被复用时重复播
this._previousTitleInfo = null;
if (!this.titleProgressBar || !prev) {
return;
}
if (this.titleProgressBar && this._titleInfo.nextTitleProgress !== undefined) {
this.titleProgressBar.progress = this._normalizeProgress(this._titleInfo.nextTitleProgress);
const startProgress = prev.nextTitleProgress;
const endProgress = this._titleInfo.nextTitleProgress;
if (startProgress === undefined || endProgress === undefined) {
return;
}
if (this.progressLabel && this._titleInfo.progressText !== undefined) {
this.progressLabel.string = this._titleInfo.progressText;
const isSameTitle = prev.titleText === undefined
|| this._titleInfo.titleText === undefined
|| prev.titleText === this._titleInfo.titleText;
// 同称号且起止相同,没必要播动画
if (isSameTitle && Math.abs(startProgress - endProgress) < 1e-4) {
return;
}
this._stopProgressAnimation();
if (isSameTitle) {
// 先展示起点,避免 _refreshTitleView 已把条填到终态
this._applyProgressValue(startProgress);
this._runProgressTween(startProgress, endProgress, PassModal.PROGRESS_ANIM_START_DELAY);
return;
}
// 跨称号:先让旧称号文字和起点进度出现在屏上
this._applyTitleText(prev.titleText);
this._applyProgressText(prev.progressText);
this._applyProgressValue(startProgress);
const self = this;
const tweenTarget = this._progressTweenTarget;
// raw 值保留 0~1下发时经 _normalizeProgress
tweenTarget.progress = Math.max(0, Math.min(1, startProgress));
const clampedEnd = Math.max(0, Math.min(1, endProgress));
const onUpdate = () => {
if (self.titleProgressBar?.isValid) {
self.titleProgressBar.progress = self._normalizeProgress(tweenTarget.progress);
}
};
tween(tweenTarget)
.delay(PassModal.PROGRESS_ANIM_START_DELAY)
.to(
PassModal.PROGRESS_ANIM_SEGMENT_DURATION,
{ progress: 1 },
{ easing: 'sineOut', onUpdate }
)
.call(() => {
// 切到新称号。progressText/titleText 都切到终态;进度值从 0 开始
self._applyTitleText(self._titleInfo.titleText);
self._applyProgressText(self._titleInfo.progressText);
tweenTarget.progress = 0;
if (self.titleProgressBar?.isValid) {
self.titleProgressBar.progress = self._normalizeProgress(0);
}
})
.delay(PassModal.PROGRESS_ANIM_LEVELUP_PAUSE)
.to(
PassModal.PROGRESS_ANIM_SEGMENT_DURATION,
{ progress: clampedEnd },
{ easing: 'sineOut', onUpdate }
)
.start();
}
private _runProgressTween(from: number, to: number, delay: number): void {
if (!this.titleProgressBar) {
return;
}
const tweenTarget = this._progressTweenTarget;
// raw 值保留 0~1 区间onUpdate 里经 _normalizeProgress 再下发,避免畸变区段
tweenTarget.progress = Math.max(0, Math.min(1, from));
this.titleProgressBar.progress = this._normalizeProgress(from);
const self = this;
const chain = tween(tweenTarget);
if (delay > 0) {
chain.delay(delay);
}
chain
.to(
PassModal.PROGRESS_ANIM_SEGMENT_DURATION,
{ progress: Math.max(0, Math.min(1, to)) },
{
easing: 'sineOut',
onUpdate: () => {
if (self.titleProgressBar?.isValid) {
self.titleProgressBar.progress = self._normalizeProgress(tweenTarget.progress);
}
}
}
)
.start();
}
private _stopProgressAnimation(): void {
Tween.stopAllByTarget(this._progressTweenTarget);
}
/**

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -0,0 +1,134 @@
{
"ver": "1.0.27",
"importer": "image",
"imported": true,
"uuid": "568a4811-9616-47c0-a961-bb2ad2854cdb",
"files": [
".json",
".png"
],
"subMetas": {
"6c48a": {
"importer": "texture",
"uuid": "568a4811-9616-47c0-a961-bb2ad2854cdb@6c48a",
"displayName": "flatIconBack",
"id": "6c48a",
"name": "texture",
"userData": {
"wrapModeS": "clamp-to-edge",
"wrapModeT": "clamp-to-edge",
"imageUuidOrDatabaseUri": "568a4811-9616-47c0-a961-bb2ad2854cdb",
"isUuid": true,
"visible": false,
"minfilter": "linear",
"magfilter": "linear",
"mipfilter": "none",
"anisotropy": 0
},
"ver": "1.0.22",
"imported": true,
"files": [
".json"
],
"subMetas": {}
},
"f9941": {
"importer": "sprite-frame",
"uuid": "568a4811-9616-47c0-a961-bb2ad2854cdb@f9941",
"displayName": "flatIconBack",
"id": "f9941",
"name": "spriteFrame",
"userData": {
"trimThreshold": 1,
"rotated": false,
"offsetX": 0,
"offsetY": 0.5,
"trimX": 20,
"trimY": 56,
"width": 472,
"height": 399,
"rawWidth": 512,
"rawHeight": 512,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"packable": true,
"pixelsToUnit": 100,
"pivotX": 0.5,
"pivotY": 0.5,
"meshType": 0,
"vertices": {
"rawPosition": [
-236,
-199.5,
0,
236,
-199.5,
0,
-236,
199.5,
0,
236,
199.5,
0
],
"indexes": [
0,
1,
2,
2,
1,
3
],
"uv": [
20,
456,
492,
456,
20,
57,
492,
57
],
"nuv": [
0.0390625,
0.111328125,
0.9609375,
0.111328125,
0.0390625,
0.890625,
0.9609375,
0.890625
],
"minPos": [
-236,
-199.5,
0
],
"maxPos": [
236,
199.5,
0
]
},
"isUuid": true,
"imageUuidOrDatabaseUri": "568a4811-9616-47c0-a961-bb2ad2854cdb@6c48a",
"atlasUuid": "",
"trimType": "auto"
},
"ver": "1.0.12",
"imported": true,
"files": [
".json"
],
"subMetas": {}
}
},
"userData": {
"type": "sprite-frame",
"fixAlphaTransparencyArtifacts": false,
"hasAlpha": true,
"redirect": "568a4811-9616-47c0-a961-bb2ad2854cdb@6c48a"
}
}

View File

@@ -51,8 +51,8 @@
"rawHeight": 235,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"borderLeft": 120,
"borderRight": 120,
"packable": true,
"pixelsToUnit": 100,
"pivotX": 0.5,