refactor(ChipComp): 优化英雄图鉴卡图标渲染逻辑

1. 移除动画播放逻辑,改为加载英雄 idle 动画首帧静态展示,降低运行时开销
2. 调整注释与变量命名,统一代码表述风格
3. 修复异步加载竞态保护逻辑
4. 同时删除了冗余的 buff_bg 预制体文件,更新了 maxw/maxb/maxr 预制体的位置与缩放参数,调整了 clips 预制体的显示属性与贴图

fix: 修复部分UI元素的显示异常问题
This commit is contained in:
pan
2026-09-01 10:41:36 +08:00
parent f40ff033c2
commit 4ad8b2e0b6
8 changed files with 52 additions and 342 deletions

View File

@@ -163,13 +163,10 @@
"b": 255, "b": 255,
"a": 255 "a": 255
}, },
"_spriteFrame": { "_spriteFrame": null,
"__uuid__": "cb93c900-b440-4571-91d1-7da1636e3d73@ce362",
"__expectedType__": "cc.SpriteFrame"
},
"_type": 0, "_type": 0,
"_fillType": 1, "_fillType": 1,
"_sizeMode": 1, "_sizeMode": 0,
"_fillCenter": { "_fillCenter": {
"__type__": "cc.Vec2", "__type__": "cc.Vec2",
"x": 0, "x": 0,

View File

@@ -1,274 +0,0 @@
[
{
"__type__": "cc.Prefab",
"_name": "buff_bg",
"_objFlags": 0,
"__editorExtras__": {},
"_native": "",
"data": {
"__id__": 1
},
"optimizationPolicy": 0,
"persistent": false
},
{
"__type__": "cc.Node",
"_name": "buff_bg",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": null,
"_children": [
{
"__id__": 2
}
],
"_active": true,
"_components": [
{
"__id__": 8
},
{
"__id__": 10
}
],
"_prefab": {
"__id__": 12
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 1,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": ""
},
{
"__type__": "cc.Node",
"_name": "skill",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 3
},
{
"__id__": 5
}
],
"_prefab": {
"__id__": 7
},
"_lpos": {
"__type__": "cc.Vec3",
"x": -2,
"y": -10,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 0.8,
"y": 0.8,
"z": 1
},
"_mobility": 0,
"_layer": 1,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": ""
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 2
},
"_enabled": true,
"__prefab": {
"__id__": 4
},
"_contentSize": {
"__type__": "cc.Size",
"width": 113,
"height": 126
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "b4sNQPJWFKha7x75SWXmRj"
},
{
"__type__": "cc.Sprite",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 2
},
"_enabled": true,
"__prefab": {
"__id__": 6
},
"_customMaterial": null,
"_srcBlendFactor": 2,
"_dstBlendFactor": 4,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"a": 255
},
"_spriteFrame": {
"__uuid__": "2423272e-e63b-4736-b15b-30b40cf98a23@35919",
"__expectedType__": "cc.SpriteFrame"
},
"_type": 0,
"_fillType": 1,
"_sizeMode": 1,
"_fillCenter": {
"__type__": "cc.Vec2",
"x": 0,
"y": 0
},
"_fillStart": 0.2,
"_fillRange": 1,
"_isTrimmedMode": true,
"_useGrayscale": false,
"_atlas": {
"__uuid__": "2423272e-e63b-4736-b15b-30b40cf98a23",
"__expectedType__": "cc.SpriteAtlas"
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "9a+65VIghBm4HQxuHPQ/mg"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "7a5LvbdlxEc6FKa7cun2oB",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 1
},
"_enabled": true,
"__prefab": {
"__id__": 9
},
"_contentSize": {
"__type__": "cc.Size",
"width": 80,
"height": 110
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "63NP9yq3hEUKD/OZZZ5t7x"
},
{
"__type__": "cc.Animation",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 1
},
"_enabled": true,
"__prefab": {
"__id__": 11
},
"playOnLoad": true,
"_clips": [
{
"__uuid__": "a732ebac-b3c8-4f43-ac73-67a90a02ecfa",
"__expectedType__": "cc.AnimationClip"
}
],
"_defaultClip": {
"__uuid__": "a732ebac-b3c8-4f43-ac73-67a90a02ecfa",
"__expectedType__": "cc.AnimationClip"
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "c6LOemuvJKyYCqlF/yUJcr"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "c46/YsCPVOJYA4mWEpNYRx",
"instance": null,
"targetOverrides": null
}
]

View File

@@ -1,13 +0,0 @@
{
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "9528e2ff-e9a0-42f8-88bd-0f172275e351",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "buff_bg"
}
}

View File

@@ -87,7 +87,7 @@
"_lpos": { "_lpos": {
"__type__": "cc.Vec3", "__type__": "cc.Vec3",
"x": 0, "x": 0,
"y": -25.982, "y": -37.784,
"z": 0 "z": 0
}, },
"_lrot": { "_lrot": {
@@ -99,8 +99,8 @@
}, },
"_lscale": { "_lscale": {
"__type__": "cc.Vec3", "__type__": "cc.Vec3",
"x": 1, "x": 1.2,
"y": 1, "y": 1.2,
"z": 1 "z": 1
}, },
"_mobility": 0, "_mobility": 0,

View File

@@ -87,7 +87,7 @@
"_lpos": { "_lpos": {
"__type__": "cc.Vec3", "__type__": "cc.Vec3",
"x": 0, "x": 0,
"y": -25.982, "y": -36.941,
"z": 0 "z": 0
}, },
"_lrot": { "_lrot": {
@@ -99,8 +99,8 @@
}, },
"_lscale": { "_lscale": {
"__type__": "cc.Vec3", "__type__": "cc.Vec3",
"x": 1, "x": 1.2,
"y": 1, "y": 1.2,
"z": 1 "z": 1
}, },
"_mobility": 0, "_mobility": 0,

View File

@@ -87,7 +87,7 @@
"_lpos": { "_lpos": {
"__type__": "cc.Vec3", "__type__": "cc.Vec3",
"x": 0, "x": 0,
"y": -30, "y": -37.394,
"z": 0 "z": 0
}, },
"_lrot": { "_lrot": {
@@ -99,8 +99,8 @@
}, },
"_lscale": { "_lscale": {
"__type__": "cc.Vec3", "__type__": "cc.Vec3",
"x": 0.8, "x": 1.2,
"y": 1, "y": 1.2,
"z": 1 "z": 1
}, },
"_mobility": 0, "_mobility": 0,

View File

@@ -497,7 +497,7 @@
"__id__": 2 "__id__": 2
}, },
"_children": [], "_children": [],
"_active": false, "_active": true,
"_components": [ "_components": [
{ {
"__id__": 20 "__id__": 20
@@ -555,8 +555,8 @@
}, },
"_contentSize": { "_contentSize": {
"__type__": "cc.Size", "__type__": "cc.Size",
"width": 114.28571428571429, "width": 140,
"height": 114.28571428571429 "height": 140
}, },
"_anchorPoint": { "_anchorPoint": {
"__type__": "cc.Vec2", "__type__": "cc.Vec2",
@@ -586,13 +586,13 @@
"_dstBlendFactor": 4, "_dstBlendFactor": 4,
"_color": { "_color": {
"__type__": "cc.Color", "__type__": "cc.Color",
"r": 255, "r": 211,
"g": 255, "g": 30,
"b": 255, "b": 30,
"a": 255 "a": 255
}, },
"_spriteFrame": { "_spriteFrame": {
"__uuid__": "cb93c900-b440-4571-91d1-7da1636e3d73@36a81", "__uuid__": "cb93c900-b440-4571-91d1-7da1636e3d73@b36c4",
"__expectedType__": "cc.SpriteFrame" "__expectedType__": "cc.SpriteFrame"
}, },
"_type": 1, "_type": 1,

View File

@@ -3,15 +3,16 @@
* @description 英雄图鉴卡预制体组件(UI 视图层·精简版) * @description 英雄图鉴卡预制体组件(UI 视图层·精简版)
* *
* 职责: * 职责:
* 1. 根据英雄 UUID 渲染 **图标**(idle 动画)。 * 1. 根据英雄 UUID 渲染 **图标**(idle 动画首帧静态展示,零运行时动画开销)。
* 2. 根据 HeroInfo.card_lv 切换 **背景底框颜色**(BG_node 下的 green/blue/purple/yellow/red 子节点)。 * 2. 根据 HeroInfo.card_lv 切换 **背景底框颜色**(BG_node 下的 green/blue/purple/yellow/red 子节点)。
* *
* 说明: * 说明:
* - 仅处理英雄展示,数据源为 heroSet 的 HeroInfo,不依赖 CardSet。 * - 仅处理英雄展示,数据源为 heroSet 的 HeroInfo,不依赖 CardSet。
* - 通过 iconVisualToken 防止异步动画加载竞态。 * - 图标采用 CHeroComp 同款方案:加载 idle AnimationClip 取首帧 SpriteFrame 静止展示,
* 通过 iconVisualToken 防止异步加载竞态。
*/ */
import { mLogger } from "../common/Logger"; import { mLogger } from "../common/Logger";
import { _decorator, Animation, AnimationClip, Node, Sprite, Vec3, resources } from "cc"; import { _decorator, AnimationClip, Node, Sprite, resources, SpriteFrame } from "cc";
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS"; import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp"; import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp";
import { HeroInfo } from "../common/config/heroSet"; import { HeroInfo } from "../common/config/heroSet";
@@ -22,7 +23,7 @@ const { ccclass, property } = _decorator;
/** /**
* ChipComp —— 单张英雄卡图标/背景视图组件 * ChipComp —— 单张英雄卡图标/背景视图组件
* *
* 挂载在图鉴卡预制体上,仅负责英雄图标(idle 动画)与背景底框的渲染。 * 挂载在图鉴卡预制体上,仅负责英雄图标(静态首帧)与背景底框的渲染。
*/ */
@ccclass('ChipComp') @ccclass('ChipComp')
@ecs.register('ChipComp', false) @ecs.register('ChipComp', false)
@@ -44,7 +45,7 @@ export class ChipComp extends CCComp {
// ======================== 对外接口 ======================== // ======================== 对外接口 ========================
/** /**
* 通过英雄 UUID 渲染英雄卡(idle 动画图标 + 背景底框)。 * 通过英雄 UUID 渲染英雄卡(静态图标 + 背景底框)。
* @param uuid 英雄 UUID(须存在于 HeroInfo) * @param uuid 英雄 UUID(须存在于 HeroInfo)
*/ */
setHeroByUuid(uuid: number) { setHeroByUuid(uuid: number) {
@@ -60,9 +61,9 @@ export class ChipComp extends CCComp {
this.BG_node.children.forEach(child => child.active = (child.name === color)); this.BG_node.children.forEach(child => child.active = (child.name === color));
} }
// 图标:idle 动画(镜像翻转,原图朝右,卡面需朝左) // 图标:idle 动画首帧静态展示
if (this.icon_node) { if (this.icon_node) {
this.updateHeroAnimation(this.icon_node, uuid, this.iconVisualToken); this.updateHeroIcon(this.icon_node, uuid, this.iconVisualToken);
} }
mLogger.log(this.debugMode, "ChipComp", "setHeroByUuid", { uuid }); mLogger.log(this.debugMode, "ChipComp", "setHeroByUuid", { uuid });
} }
@@ -75,56 +76,55 @@ export class ChipComp extends CCComp {
this.BG_node.children.forEach(child => child.active = false); this.BG_node.children.forEach(child => child.active = false);
} }
if (this.icon_node) { if (this.icon_node) {
this.clearIconAnimation(this.icon_node);
const sprite = this.icon_node.getComponent(Sprite) || this.icon_node.getComponentInChildren(Sprite); const sprite = this.icon_node.getComponent(Sprite) || this.icon_node.getComponentInChildren(Sprite);
if (sprite) sprite.spriteFrame = null; if (sprite) sprite.spriteFrame = null;
} }
this.node.active = false; this.node.active = false;
} }
// ======================== 动画 ======================== // ======================== 图标(静态首帧) ========================
/** /**
* 为英雄图标加载并播放 idle 动画。 * 加载英雄 idle 动画剪辑,提取首帧 SpriteFrame 静止展示(零运行时动画开销)。
* 使用 token 做竞态保护,确保异步回调时不会覆盖已更新的图标。 * 使用 token 做竞态保护,确保异步回调时不会覆盖已更新的图标。
*/ */
private updateHeroAnimation(node: Node, uuid: number, token: number) { private updateHeroIcon(node: Node, uuid: number, token: number) {
const sprite = node?.getComponent(Sprite) || node?.getComponentInChildren(Sprite); if (!node || !node.isValid) return;
const sprite = node.getComponent(Sprite) || node.getComponentInChildren(Sprite);
if (sprite) sprite.spriteFrame = null; if (sprite) sprite.spriteFrame = null;
const hero = HeroInfo[uuid]; const hero = HeroInfo[uuid];
if (!hero) return; if (!hero) return;
const anim = node.getComponent(Animation) || node.addComponent(Animation);
this.clearAnimationClips(anim);
const path = `game/heros/hero/${hero.path}/idle`; const path = `game/heros/hero/${hero.path}/idle`;
resources.load(path, AnimationClip, (err, clip) => { resources.load(path, AnimationClip, (err, clip) => {
if (err || !clip) { if (err || !clip) {
mLogger.log(this.debugMode, "ChipComp", `load hero animation failed ${uuid}`, err); mLogger.log(this.debugMode, "ChipComp", `load hero animation failed ${uuid}`, err);
return; return;
} }
if (token !== this.iconVisualToken || this.card_uuid !== uuid) { // 竞态保护:仅允许最新一次图标请求落地
return; if (token !== this.iconVisualToken || this.card_uuid !== uuid) 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;
} }
this.clearAnimationClips(anim);
anim.addClip(clip);
anim.play("idle");
}); });
} }
/** 清除图标节点上的动画(停止播放并移除所有 clip) */
private clearIconAnimation(node: Node) {
const anim = node?.getComponent(Animation);
if (!anim) return;
anim.stop();
this.clearAnimationClips(anim);
}
/** 移除 Animation 组件上的全部 AnimationClip */
private clearAnimationClips(anim: Animation) {
const clips = anim.clips as AnimationClip[] | undefined;
if (!clips || clips.length === 0) return;
[...clips].forEach(clip => anim.removeClip(clip, true));
}
// ======================== 生命周期钩子 ======================== // ======================== 生命周期钩子 ========================
/** ECS 组件移除时的释放钩子:销毁节点 */ /** ECS 组件移除时的释放钩子:销毁节点 */