refactor: 重构英雄列表UI与图标加载逻辑

1.  替换list-me预制体为list_me,统一命名规范
2.  调整ui5.plist的精灵边框参数,修复UI显示精度
3.  重写英雄图标加载逻辑,新增异步加载竞态保护与动画首帧缓存
4.  优化cSkill预制体的布局与样式参数
This commit is contained in:
pan
2026-07-27 09:44:02 +08:00
parent 336175c421
commit f634b48eb4
10 changed files with 7000 additions and 2540 deletions

View File

@@ -16,7 +16,7 @@
* - 英雄卡效果事件为 GameEvent.CallHero而非 UseSkillCard。
*/
import { mLogger } from "../common/Logger";
import { _decorator, Node, Sprite, Label, NodeEventType, Vec3, Tween, tween, UIOpacity } from "cc";
import { _decorator, Node, Sprite, Label, NodeEventType, Vec3, Tween, tween, UIOpacity, SpriteFrame, resources, AnimationClip } from "cc";
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp";
import { CardConfig, CardType } from "../common/config/CardSet";
@@ -91,6 +91,10 @@ export class CHeroComp extends CCComp {
private restPosition: Vec3 = new Vec3();
/** 透明度组件(用于淡入淡出) */
private opacityComp: UIOpacity | null = null;
/** 图标视觉令牌(异步加载竞态保护) */
private iconVisualToken: number = 0;
/** 当前显示的英雄 UUID避免相同 UUID 重复加载动画) */
private iconHeroUuid: number = 0;
// ======================== 生命周期 ========================
@@ -299,10 +303,8 @@ export class CHeroComp extends CCComp {
// 图标
if (this.icon_node) {
const iconId = this.getIconId(renderUuid);
if (iconId) {
this.updateIcon(this.icon_node, iconId);
}
this.iconVisualToken += 1;
this.updateHeroAnimation(this.icon_node, renderUuid, this.iconVisualToken);
}
// 购买按钮费用显示
@@ -446,30 +448,70 @@ export class CHeroComp extends CCComp {
return model.hero_uuid ?? null;
}
/** 获取英雄图标 id优先卡牌自定义 icon其次英雄 path兜底 uuid */
private getIconId(renderUuid: number): string {
if (!this.cardData) return "";
if (this.cardData.icon) return this.cardData.icon;
const hero = HeroInfo[renderUuid];
return hero?.path || `${renderUuid}`;
}
/**
* 为英雄图标加载 idle 动画剪辑,提取首帧 SpriteFrame 静止展示。
* 与 HInfoComp 的 updateHeroAnimation 一致,零运行时开销。
* @param node 图标节点
* @param uuid 英雄 UUID
* @param token 视觉令牌
*/
private updateHeroAnimation(node: Node, uuid: number, token: number) {
if (!node || !node.isValid) return;
// 相同英雄 UUID 不重复加载,避免刷新卡池时闪烁
if (this.iconHeroUuid === uuid) return;
this.iconHeroUuid = uuid;
/** 更新图标精灵 */
private updateIcon(node: Node, iconId: string) {
if (!node || !iconId) return;
const sprite = node.getComponent(Sprite) || node.getComponentInChildren(Sprite);
const frame = smc.uiconsAtlas ? smc.uiconsAtlas.getSpriteFrame(iconId) : null;
mLogger.log(this.debugMode, "CHeroComp", "updateIcon", {
nodeName: this.node?.name,
iconId,
spriteFound: !!sprite,
atlasReady: !!smc.uiconsAtlas,
frameFound: !!frame
});
if (!sprite) return;
if (smc.uiconsAtlas) {
sprite.spriteFrame = frame || null;
if (sprite) sprite.spriteFrame = null;
const hero = HeroInfo[uuid];
if (!hero) {
mLogger.log(this.debugMode, "CHeroComp", "updateHeroAnimation hero not found", {
nodeName: this.node?.name,
uuid
});
return;
}
const path = `game/heros/hero/${hero.path}/idle`;
mLogger.log(this.debugMode, "CHeroComp", "updateHeroAnimation load", {
nodeName: this.node?.name,
uuid,
path,
token
});
resources.load(path, AnimationClip, (err, clip) => {
if (err || !clip) {
mLogger.log(this.debugMode, "CHeroComp", "updateHeroAnimation load failed", {
nodeName: this.node?.name,
uuid,
path,
message: err?.message ?? null
});
return;
}
// 竞态保护:仅允许最新一次图标请求落地
if (token !== this.iconVisualToken) return;
if (!node || !node.isValid) return;
const target = node.getComponent(Sprite) || node.getComponentInChildren(Sprite);
if (!target || !target.isValid) return;
// 帧动画数据存于 spriteFrame 轨道的 _channel._curve._values裸 SpriteFrame 数组)
let firstFrame: SpriteFrame | undefined;
for (const track of clip.tracks) {
const curve = (track as unknown as { channel?: { curve?: { _values?: unknown[] } } }).channel?.curve;
const frame = curve?._values?.[0];
if (frame instanceof SpriteFrame) {
firstFrame = frame;
break;
}
}
if (firstFrame) {
target.spriteFrame = firstFrame;
}
});
}
// ======================== 长按信息框 ========================