refactor(hero): 重构英雄头顶状态栏逻辑到独立组件

将原HeroViewComp中的头顶血条、等级、护盾UI逻辑抽离为HeroTopBarComp,
统一管理头顶状态栏的初始化、数据更新、动画和显隐逻辑,减少HeroViewComp代码冗余,
提升代码可维护性和复用性。同时更新预制体关联,适配新的组件结构。
This commit is contained in:
pan
2026-08-13 17:20:38 +08:00
parent 76082d8e1f
commit d06c99e2f1
4 changed files with 245 additions and 153 deletions

View File

@@ -38,10 +38,13 @@
"_components": [ "_components": [
{ {
"__id__": 110 "__id__": 110
},
{
"__id__": 112
} }
], ],
"_prefab": { "_prefab": {
"__id__": 112 "__id__": 114
}, },
"_lpos": { "_lpos": {
"__type__": "cc.Vec3", "__type__": "cc.Vec3",
@@ -1277,7 +1280,7 @@
"__id__": 65 "__id__": 65
} }
], ],
"_active": true, "_active": false,
"_components": [ "_components": [
{ {
"__id__": 73 "__id__": 73
@@ -2520,8 +2523,8 @@
}, },
"_contentSize": { "_contentSize": {
"__type__": "cc.Size", "__type__": "cc.Size",
"width": 70, "width": 40,
"height": 20 "height": 11
}, },
"_anchorPoint": { "_anchorPoint": {
"__type__": "cc.Vec2", "__type__": "cc.Vec2",
@@ -2534,6 +2537,34 @@
"__type__": "cc.CompPrefabInfo", "__type__": "cc.CompPrefabInfo",
"fileId": "cdvV5hAkdEaY6AT6urHKuf" "fileId": "cdvV5hAkdEaY6AT6urHKuf"
}, },
{
"__type__": "c4181/FEWhKbY0D0PLdCyKi",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 1
},
"_enabled": true,
"__prefab": {
"__id__": 113
},
"hpProgressBar": {
"__id__": 53
},
"lvLabel": null,
"shieldProgressBar": {
"__id__": 77
},
"hpBarSprite": {
"__id__": 36
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "3eCKBFOJhLHKqcnak/mi3d"
},
{ {
"__type__": "cc.PrefabInfo", "__type__": "cc.PrefabInfo",
"root": { "root": {

View File

@@ -0,0 +1,184 @@
import { _decorator, Label, ProgressBar, Sprite, UIOpacity, Vec3, v3, tween, Tween, Color, clamp } 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 { HeroAttrsComp } from "./HeroAttrsComp";
import { FacSet, FightSet } from "../common/config/GameSet";
const { ccclass, property } = _decorator;
/**
* 英雄头顶状态栏组件
*
* 挂载在英雄节点下的 "top" 子节点上,独立管理:
* - 血条进度ProgressBar
* - 护盾数值条ProgressBartop/shield 节点,显示 shield / SHIELD_MAX
* - 等级数字Label
* - 透明度激活 / 闲置
* - 受击抖动动画
*
* 数据流HeroViewComp 将 HeroAttrsComp 引用传入,本组件通过 model 读取属性。
* 注意:英雄根节点下的 "shielded" 是护盾特效动画节点,由 HeroViewComp 管理,不在本组件职责内。
*/
@ccclass('HeroTopBarComp')
@ecs.register('HeroTopBar', false)
export class HeroTopBarComp extends CCComp {
// ==================== @property 绑定(编辑器拖拽) ====================
@property({ type: ProgressBar, tooltip: "血条进度条" })
private hpProgressBar: ProgressBar = null!;
@property({ type: Label, tooltip: "等级数字" })
private lvLabel: Label = null!;
@property({ type: ProgressBar, tooltip: "护盾数值条top 下的 shield 节点)" })
private shieldProgressBar: ProgressBar = null!;
@property({ type: Sprite, tooltip: "血条填充 Sprite用于设置阵营颜色" })
private hpBarSprite: Sprite = null!;
// ==================== 内部引用 ====================
private topOpacity: UIOpacity = null!;
private basePos: Vec3 = v3();
// ==================== 配置 ====================
private hp_height: number = 90;
private boss_hp_height: number = 160;
// ==================== 透明度 ====================
private readonly barIdleOpacity: number = 153;
private readonly barActiveOpacity: number = 255;
private readonly idleOpacityDelay: number = 0.25;
/** 外部赋值:英雄数据模型 */
private _model: HeroAttrsComp = null!;
public get model(): HeroAttrsComp { return this._model; }
public set model(value: HeroAttrsComp) { this._model = value; }
private readonly restoreBarIdleOpacity = () => {
this.setTopBarOpacity(false);
};
/**
* 初始化 / 对象池复用时重置
* @param model 英雄属性组件
* @param direction 显示方向1 或 -1
*/
public init(model: HeroAttrsComp, direction: number = 1): void {
this._model = model;
// UIOpacity 无 @property 绑定,运行时获取或添加
this.topOpacity = this.node.getComponent(UIOpacity) || this.node.addComponent(UIOpacity);
// 方向取 abs避免对象池复用时 scale 累乘导致血条反转
this.node.setScale(direction * Math.abs(this.node.scale.x), 1 * this.node.scale.y);
// 位置
const height = model.is_boss ? this.boss_hp_height : this.hp_height;
this.node.setPosition(0, height, 0);
this.basePos = this.node.position.clone();
// 血条颜色(英雄阵营为绿色)
if (model.fac === FacSet.HERO && this.hpBarSprite) {
this.hpBarSprite.color = new Color("#2ECC71");
}
// 确保血条等 UI 始终在最上层
this.node.setSiblingIndex(999);
// 重置显示状态
this.node.getChildByName("hp")!.active = true;
this.node.getChildByName("cd")!.active = false;
this.node.getChildByName("lv")!.active = true;
this.node.active = true;
this.setTopBarOpacity(false);
this.hpShow();
this.lvShow();
this.shieldShow(model.shield);
}
/** 更新血量进度条 */
public hpShow(): void {
if (!this._model || !this.hpProgressBar) return;
const hp = this._model.hp;
const hpMax = this._model.hp_max;
let targetProgress = hpMax > 0 ? hp / hpMax : 0;
targetProgress = clamp(targetProgress, 0, 1);
const prevProgress = this.hpProgressBar.progress;
if (targetProgress < prevProgress) {
this.activateTopBar();
this.playHpBarShake();
}
this.hpProgressBar.progress = targetProgress;
if (targetProgress > prevProgress) {
this.setTopBarOpacity(false);
}
}
/** 更新护盾数值条(百分比 = shield / SHIELD_MAX */
public shieldShow(shield: number = 0): void {
if (!this.shieldProgressBar) return;
const progress = FightSet.SHIELD_MAX > 0 ? shield / FightSet.SHIELD_MAX : 0;
this.shieldProgressBar.progress = clamp(progress, 0, 1);
// 护盾节点显隐
this.shieldProgressBar.node.active = shield > 0;
}
/** 更新等级数字 */
public lvShow(): void {
if (!this.lvLabel || !this._model) return;
this.lvLabel.string = String(this._model.lv);
}
/** 激活顶栏(高亮 + 延迟恢复闲置透明度) */
public activateTopBar(): void {
this.setTopBarOpacity(true);
this.unschedule(this.restoreBarIdleOpacity);
this.scheduleOnce(this.restoreBarIdleOpacity, this.idleOpacityDelay);
}
/** 设置顶栏透明度 */
public setTopBarOpacity(isActive: boolean): void {
if (!this.node || !this.node.isValid) return;
if (this.topOpacity) {
this.topOpacity.opacity = isActive ? this.barActiveOpacity : this.barIdleOpacity;
}
this.node.active = true;
}
/** 显示顶栏 */
public show(): void {
if (this.node && this.node.isValid) this.node.active = true;
}
/** 隐藏顶栏 */
public hide(): void {
if (this.node && this.node.isValid) this.node.active = false;
}
/** 血条受击抖动动画 */
private playHpBarShake(): void {
if (!this.node || !this.node.isValid) return;
Tween.stopAllByTarget(this.node);
this.node.setPosition(this.basePos);
tween(this.node)
.by(0.04, { position: v3(-3, 0, 0) })
.by(0.04, { position: v3(6, 0, 0) })
.by(0.04, { position: v3(-5, 0, 0) })
.by(0.04, { position: v3(2, 0, 0) })
.call(() => {
this.node.setPosition(this.basePos);
})
.start();
}
/** 对象池回收 / 组件重置时调用 */
public reset(): void {
this.unscheduleAllCallbacks();
if (this.node && this.node.isValid) {
Tween.stopAllByTarget(this.node);
this.node.setPosition(this.basePos);
}
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "c4181fc5-1168-4a6d-8d03-d0f2dd0b22a2",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -1,8 +1,9 @@
import { Vec3, _decorator, v3, Collider2D, Contact2DType, Label, Node, Prefab, instantiate, ProgressBar, Component, Material, Sprite, math, clamp, Game, tween, Tween, Color, BoxCollider2D, UITransform, UIOpacity } from "cc"; import { Vec3, _decorator, v3, Collider2D, Contact2DType, Node, Prefab, instantiate, tween, Tween, BoxCollider2D, UITransform } 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 { mLogger } from "../common/Logger"; import { mLogger } from "../common/Logger";
import { HeroSpine } from "./HeroSpine"; import { HeroSpine } from "./HeroSpine";
import { HeroTopBarComp } from "./HeroTopBarComp";
import { BoxSet, FacSet, FightSet, NumberFormatter, TooltipTypes } from "../common/config/GameSet"; import { BoxSet, FacSet, FightSet, NumberFormatter, TooltipTypes } from "../common/config/GameSet";
import { smc } from "../common/SingletonModuleComp"; import { smc } from "../common/SingletonModuleComp";
import { SkillSet, } from "../common/config/SkillSet"; import { SkillSet, } from "../common/config/SkillSet";
@@ -37,21 +38,10 @@ export class HeroViewComp extends CCComp {
realDeadTime: number = 0.1 realDeadTime: number = 0.1
deadCD: number = 0 deadCD: number = 0
monDeadTime: number = 0.1 monDeadTime: number = 0.1
hp_height: number = 90
boss_hp_height: number = 160
// 血条显示相关 // 血条显示相关
lastBarUpdateTime: number = 0; // 最后一次血条/蓝条/护盾更新时间 lastBarUpdateTime: number = 0; // 最后一次血条/蓝条/护盾更新时间
// ==================== UI 节点引用 ==================== // ==================== UI 节点引用 ====================
private top_node: Node = null!; private topBar: HeroTopBarComp = null!;
private topOpacity: UIOpacity = null!;
private topBasePos: Vec3 = v3();
private lvLabel: Label | null = null; // 等级显示文本
private readonly barIdleOpacity: number = 153;
private readonly barActiveOpacity: number = 255;
private readonly idleOpacityDelay: number = 0.25;
private readonly restoreBarIdleOpacity = () => {
this.setTopBarOpacity(false);
};
// ==================== 直接访问 HeroAttrsComp ==================== // ==================== 直接访问 HeroAttrsComp ====================
get model() { get model() {
@@ -96,67 +86,26 @@ export class HeroViewComp extends CCComp {
this.lastBarUpdateTime = 0; this.lastBarUpdateTime = 0;
this.status_change("idle"); this.status_change("idle");
// 初始化 UI 节点 // 初始化头顶状态栏
this.initUINodes(); const topNode = this.node.getChildByName("top");
this.topBar = topNode.getComponent(HeroTopBarComp) || topNode.addComponent(HeroTopBarComp);
this.topBar.init(this.model, this.scale);
/** 方向 */ /** 方向 */
this.node.setScale(this.scale * Math.abs(this.node.scale.x), 1 * this.node.scale.y); // 确保 scale.x 为正后再乘方向 this.node.setScale(this.scale * Math.abs(this.node.scale.x), 1 * this.node.scale.y); // 确保 scale.x 为正后再乘方向
// top_node 取 abs 避免对象池复用时 scale 累乘导致血条方向反转
this.top_node.setScale(this.scale * Math.abs(this.top_node.scale.x), 1 * this.top_node.scale.y);
/* 显示角色血*/
this.top_node.getChildByName("hp").active = true;
this.top_node.getChildByName("cd").active = false;
this.top_node.getChildByName("shield").active = false;
this.top_node.getChildByName("lv").active = true;
this.top_node.active = true;
this.setTopBarOpacity(false);
// 初始化等级显示 // 🔥 重置描边
this.lv_show();
// 🔥 重置血条 UI 显示状态
if (this.model) { if (this.model) {
this.hp_show();
// 根据英雄模型中的等级数据设置描边
// 注意HeroViewComp 是挂在 node 上的,而 FlashSprite 可能挂在子节点(如 Sprite/anm 节点)
let flashSprite = this.node.getComponent(FlashSprite); let flashSprite = this.node.getComponent(FlashSprite);
if (!flashSprite) { if (!flashSprite) {
// 如果当前节点没有,尝试在所有子节点中查找
flashSprite = this.node.getComponentInChildren(FlashSprite); flashSprite = this.node.getComponentInChildren(FlashSprite);
} }
if (flashSprite) { if (flashSprite) {
flashSprite.setOutlineByLevel(this.model.lv); flashSprite.setOutlineByLevel(this.model.lv);
} }
} }
} }
/** 初始化 UI 节点引用 */
private initUINodes() {
this.top_node = this.node.getChildByName("top");
this.topOpacity = this.top_node.getComponent(UIOpacity) || this.top_node.addComponent(UIOpacity);
this.top_node.setPosition(0, this.hp_height, 0);
if (this.model.is_boss) this.top_node.setPosition(0, this.boss_hp_height, 0);
this.topBasePos = this.top_node.position.clone();
const hpNode = this.top_node.getChildByName("hp");
if (this.model.fac == FacSet.HERO) {
hpNode.getChildByName("Bar").getComponent(Sprite).color = new Color("#2ECC71")
}
// 确保血条等UI始终在最上层显示
this.top_node.setSiblingIndex(999);
// 缓存等级显示 Labellv 节点下的 Label 子节点)
const lvNode = this.top_node.getChildByName("lv");
if (lvNode) {
this.lvLabel = lvNode.getChildByName("Label")?.getComponent(Label) ?? null;
}
}
/** /**
* View 层每帧更新 * View 层每帧更新
* 注意:数据更新逻辑已移到 HeroAttrSystem这里只负责显示 * 注意:数据更新逻辑已移到 HeroAttrSystem这里只负责显示
@@ -181,18 +130,18 @@ export class HeroViewComp extends CCComp {
// ✅ 按需更新 UI脏标签模式- 只在属性变化时更新 // ✅ 按需更新 UI脏标签模式- 只在属性变化时更新
if (this.model.dirty_hp) { if (this.model.dirty_hp) {
this.hp_show(); this.topBar.hpShow();
this.model.dirty_hp = false; this.model.dirty_hp = false;
} }
if (this.model.dirty_shield) { if (this.model.dirty_shield) {
this.show_shield(this.model.shield); this.topBar.shieldShow(this.model.shield);
this.model.dirty_shield = false; this.model.dirty_shield = false;
} }
if (this.model.dirty_lv) { if (this.model.dirty_lv) {
this.lv_show(); this.topBar.lvShow();
this.model.dirty_lv = false; this.model.dirty_lv = false;
} }
} }
@@ -201,83 +150,6 @@ export class HeroViewComp extends CCComp {
return; return;
} }
/** 显示护盾 */
private show_shield(shield: number = 0) {
this.lastBarUpdateTime = Date.now() / 1000;
this.node.getChildByName("shielded").active = shield > 0;
}
/** 显示血量 */
private hp_show() {
this.lastBarUpdateTime = Date.now() / 1000;
// 不再基于血量是否满来决定显示状态,只更新进度条
let hp = this.model.hp;
let hp_max = this.model.hp_max;
// mLogger.log(this.debugMode, 'HeroViewComp', "hp_show",hp,hp_max)
let targetProgress = hp_max > 0 ? hp / hp_max : 0;
targetProgress = clamp(targetProgress, 0, 1);
let hpNode = this.top_node.getChildByName("hp");
let hpProgressBar = hpNode.getComponent(ProgressBar);
const prevProgress = hpProgressBar.progress;
if (targetProgress < hpProgressBar.progress) {
this.activateTopBar();
this.playHpBarShake();
}
hpProgressBar.progress = targetProgress;
if (targetProgress > prevProgress) {
this.setTopBarOpacity(false);
}
}
private isFullHpAndNoShield(): boolean {
if (!this.model) return false;
if (this.model.hp_max <= 0) return false;
return this.model.hp >= this.model.hp_max;
}
/** 更新等级显示 */
private lv_show() {
if (!this.lvLabel || !this.model) return;
this.lvLabel.string = String(this.model.lv);
}
private setTopBarOpacity(isActive: boolean) {
if (!this.top_node || !this.top_node.isValid) return;
if (this.topOpacity) {
this.topOpacity.opacity = this.barActiveOpacity;
}
if (isActive) {
this.top_node.active = true;
return;
}
this.top_node.active = true;
}
private activateTopBar() {
this.setTopBarOpacity(true);
this.unschedule(this.restoreBarIdleOpacity);
this.scheduleOnce(this.restoreBarIdleOpacity, this.idleOpacityDelay);
}
private playHpBarShake() {
if (!this.top_node || !this.top_node.isValid) return;
Tween.stopAllByTarget(this.top_node);
this.top_node.setPosition(this.topBasePos);
tween(this.top_node)
.by(0.04, { position: v3(-3, 0, 0) })
.by(0.04, { position: v3(6, 0, 0) })
.by(0.04, { position: v3(-5, 0, 0) })
.by(0.04, { position: v3(2, 0, 0) })
.call(() => {
this.top_node.setPosition(this.topBasePos);
})
.start();
}
/** 升级特效 */ /** 升级特效 */
public lv_up() { public lv_up() {
this.spawnTimedFx("game/skill/buff/buff_lvup", this.node, 1.0); this.spawnTimedFx("game/skill/buff/buff_lvup", this.node, 1.0);
@@ -432,7 +304,7 @@ export class HeroViewComp extends CCComp {
} }
add_shield(shield: number) { add_shield(shield: number) {
// 护盾数据更新由 Model 层处理,这里只负责视图表现 // 护盾数据更新由 Model 层处理,这里只负责视图表现
if (this.model && this.model.shield > 0) this.show_shield(this.model.shield); if (this.model && this.model.shield > 0) this.topBar.shieldShow(this.model.shield);
} }
health(hp: number = 0) { health(hp: number = 0) {
@@ -458,7 +330,7 @@ export class HeroViewComp extends CCComp {
} }
// 恢复UI // 恢复UI
this.top_node.active = true; this.topBar.show();
this.status_change("idle"); this.status_change("idle");
@@ -504,7 +376,7 @@ export class HeroViewComp extends CCComp {
} }
// 隐藏UI // 隐藏UI
this.top_node.active = false; this.topBar.hide();
// 在销毁实体前先禁用碰撞体,从源头减少"尸体"参与碰撞 // 在销毁实体前先禁用碰撞体,从源头减少"尸体"参与碰撞
const collider = this.node.getComponent(Collider2D); const collider = this.node.getComponent(Collider2D);
@@ -528,7 +400,7 @@ export class HeroViewComp extends CCComp {
/** 受击表现(伤害数字、受击动画、击退) */ /** 受击表现(伤害数字、受击动画、击退) */
do_atked(damage: number, isCrit: boolean, s_uuid: number, knockbackExtra: number = 0) { do_atked(damage: number, isCrit: boolean, s_uuid: number, knockbackExtra: number = 0) {
// 受到攻击时更新最后更新时间 // 受到攻击时更新最后更新时间
this.activateTopBar(); this.topBar.activateTopBar();
this.lastBarUpdateTime = Date.now() / 1000; this.lastBarUpdateTime = Date.now() / 1000;
if (damage <= 0) return; if (damage <= 0) return;
@@ -630,7 +502,7 @@ export class HeroViewComp extends CCComp {
if (!this.model) return; if (!this.model) return;
const damageText = NumberFormatter.formatNumber(Math.max(0, Math.floor(damage))); const damageText = NumberFormatter.formatNumber(Math.max(0, Math.floor(damage)));
this.hp_show(); this.topBar.hpShow();
if (isCrit) { if (isCrit) {
this.hp_tip(TooltipTypes.crit, damageText); this.hp_tip(TooltipTypes.crit, damageText);
} else { } else {
@@ -641,10 +513,7 @@ export class HeroViewComp extends CCComp {
// 清理残留的定时器和缓动 // 清理残留的定时器和缓动
this.unscheduleAllCallbacks(); this.unscheduleAllCallbacks();
Tween.stopAllByTarget(this.node); Tween.stopAllByTarget(this.node);
if (this.top_node && this.top_node.isValid) { this.topBar.reset();
Tween.stopAllByTarget(this.top_node);
this.top_node.setPosition(this.topBasePos);
}
// 清理碰撞器事件监听 // 清理碰撞器事件监听
const collider = this.getComponent(Collider2D); const collider = this.getComponent(Collider2D);
@@ -653,7 +522,6 @@ export class HeroViewComp extends CCComp {
} }
this.deadCD = 0 this.deadCD = 0
this.lastBarUpdateTime = 0 this.lastBarUpdateTime = 0
this.unschedule(this.restoreBarIdleOpacity);
// 清理伤害队列 // 清理伤害队列
this.damageQueue.length = 0; this.damageQueue.length = 0;