docs: 为游戏地图模块添加详细的代码注释
为游戏地图模块的脚本文件添加全面的注释,说明每个组件的职责、关键设计、依赖关系和使用方式。注释覆盖了英雄信息面板、技能卡槽位管理器、排行榜弹窗、卡牌控制器、背景滚动组件等核心功能模块,提高了代码的可读性和维护性。 同时修复了英雄预制体的激活状态和技能效果预制体的尺寸参数。
This commit is contained in:
@@ -1,3 +1,23 @@
|
||||
/**
|
||||
* @file HlistComp.ts
|
||||
* @description 英雄列表轮播组件(UI 视图层)
|
||||
*
|
||||
* 职责:
|
||||
* 1. 在主页界面以 **5 格轮播** 形式展示英雄图鉴。
|
||||
* 2. 支持左右切换,用 tween 动画平滑过渡节点位置。
|
||||
* 3. 点击切换时自动更新当前选中英雄的名称、AP、HP 和技能信息。
|
||||
*
|
||||
* 关键设计:
|
||||
* - carouselNodes[0..4] 对应 5 个展示位(最左-2 到最右+2),
|
||||
* 中间位 [2] 为当前选中英雄。
|
||||
* - 切换时:将即将移出屏幕的节点瞬间跳转到另一端,再用 tween 滑入。
|
||||
* - 切换完成后重排 carouselNodes 数组保持逻辑顺序。
|
||||
* - iconVisualTokens 按节点独立管理竞态令牌,防止异步动画回调错乱。
|
||||
*
|
||||
* 依赖:
|
||||
* - HeroInfo / HeroList(heroSet)—— 英雄静态配置与全量英雄 UUID 列表
|
||||
* - SkillSet / IType(SkillSet)—— 技能配置与类型枚举
|
||||
*/
|
||||
import { _decorator, Animation, AnimationClip, Button, Event, Label, Node, NodeEventType, Sprite, resources, tween, Vec3 } from "cc";
|
||||
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
|
||||
import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp";
|
||||
@@ -8,53 +28,83 @@ import { mLogger } from "../common/Logger";
|
||||
|
||||
const {property, ccclass } = _decorator;
|
||||
|
||||
/** 视图层对象 */
|
||||
/**
|
||||
* HListComp —— 英雄图鉴轮播视图组件
|
||||
*
|
||||
* 在任务主页展示所有可用英雄,玩家可左右滑动查看:
|
||||
* - 中间位显示完整信息(名称 / AP / HP / 技能列表)
|
||||
* - 两侧位显示缩略 idle 动画
|
||||
*/
|
||||
@ccclass('HListComp')
|
||||
@ecs.register('HListComp', false)
|
||||
export class HListComp extends CCComp {
|
||||
// ======================== 编辑器绑定节点 ========================
|
||||
|
||||
/** 中间位英雄 idle 图标节点 */
|
||||
@property(Node)
|
||||
hero_icon=null!
|
||||
/** 左侧第 1 位英雄图标 */
|
||||
@property(Node)
|
||||
phero_icon=null!
|
||||
/** 右侧第 1 位英雄图标 */
|
||||
@property(Node)
|
||||
nhero_icon=null!
|
||||
/** 左侧第 2 位英雄图标(最远) */
|
||||
@property(Node)
|
||||
phero1_icon=null!
|
||||
/** 右侧第 2 位英雄图标(最远) */
|
||||
@property(Node)
|
||||
nhero1_icon=null!
|
||||
/** 攻击力标签节点 */
|
||||
@property(Node)
|
||||
ap_node=null!
|
||||
/** 生命值标签节点 */
|
||||
@property(Node)
|
||||
hp_node=null!
|
||||
/** 技能信息容器节点(包含 Line1~Line5 子节点) */
|
||||
@property(Node)
|
||||
info_node=null!
|
||||
/** 英雄名称标签节点 */
|
||||
@property(Node)
|
||||
name_node=null!
|
||||
|
||||
/** 向左切换按钮 */
|
||||
@property(Node)
|
||||
pre_btn=null!
|
||||
/** 向右切换按钮 */
|
||||
@property(Node)
|
||||
next_btn=null!
|
||||
|
||||
// ======================== 运行时状态 ========================
|
||||
|
||||
/** 当前选中英雄在 HeroList 中的索引 */
|
||||
huuid:number=null!
|
||||
/** 当前选中英雄在 HeroList 数组中的下标 */
|
||||
private currentIndex: number = 0;
|
||||
/** 各图标节点的视觉令牌映射(防止异步动画竞态) */
|
||||
private iconVisualTokens: Map<Node, number> = new Map();
|
||||
/** 是否正在播放切换动画(防止快速连点) */
|
||||
private isAnimating: boolean = false;
|
||||
/** 轮播节点数组,顺序为 [左2, 左1, 中, 右1, 右2] */
|
||||
private carouselNodes: Node[] = [];
|
||||
/** 5 个固定位置坐标(从场景中读取初始值) */
|
||||
private fixedPositions: Vec3[] = [];
|
||||
/** 调试日志开关 */
|
||||
debugMode: boolean = false;
|
||||
|
||||
onLoad() {
|
||||
// 绑定左右切换按钮事件
|
||||
this.pre_btn?.on(NodeEventType.TOUCH_END, this.onPreClick, this);
|
||||
this.next_btn?.on(NodeEventType.TOUCH_END, this.onNextClick, this);
|
||||
}
|
||||
|
||||
start() {
|
||||
// 初始化轮播节点数组和固定位置
|
||||
if (this.phero1_icon && this.phero_icon && this.hero_icon && this.nhero_icon && this.nhero1_icon) {
|
||||
this.carouselNodes = [this.phero1_icon, this.phero_icon, this.hero_icon, this.nhero_icon, this.nhero1_icon];
|
||||
this.fixedPositions = this.carouselNodes.map(n => n.position.clone());
|
||||
}
|
||||
|
||||
// 设置初始选中并加载所有位置的英雄动画
|
||||
if (HeroList && HeroList.length > 0) {
|
||||
this.currentIndex = 0;
|
||||
this.initAllNodes();
|
||||
@@ -62,6 +112,15 @@ export class HListComp extends CCComp {
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 切换逻辑 ========================
|
||||
|
||||
/**
|
||||
* 向左切换(查看上一个英雄):
|
||||
* 1. currentIndex 左移。
|
||||
* 2. 最右节点 n4 瞬间跳到最左位置,加载新英雄动画。
|
||||
* 3. 所有节点 tween 向右滑动一格。
|
||||
* 4. 动画完成后重排 carouselNodes 数组。
|
||||
*/
|
||||
private onPreClick() {
|
||||
if (!HeroList || HeroList.length === 0 || this.isAnimating || this.carouselNodes.length < 5) return;
|
||||
this.isAnimating = true;
|
||||
@@ -70,22 +129,31 @@ export class HListComp extends CCComp {
|
||||
|
||||
const [n0, n1, n2, n3, n4] = this.carouselNodes;
|
||||
|
||||
// n4 instantly jumps from rightmost to leftmost position to get ready to slide in
|
||||
// n4 瞬间跳至最左位置,准备滑入
|
||||
n4.setPosition(new Vec3(this.fixedPositions[0].x, n4.position.y, n4.position.z));
|
||||
this.updateNodeAnimationByOffset(n4, -2);
|
||||
this.updateHeroInfo();
|
||||
|
||||
// 所有节点向右滑动一格
|
||||
tween(n0).to(0.2, { position: new Vec3(this.fixedPositions[1].x, n0.position.y, n0.position.z) }).start();
|
||||
tween(n1).to(0.2, { position: new Vec3(this.fixedPositions[2].x, n1.position.y, n1.position.z) }).start();
|
||||
tween(n2).to(0.2, { position: new Vec3(this.fixedPositions[3].x, n2.position.y, n2.position.z) }).start();
|
||||
tween(n3).to(0.2, { position: new Vec3(this.fixedPositions[4].x, n3.position.y, n3.position.z) })
|
||||
.call(() => {
|
||||
// 重排数组:n4 成为新的最左节点
|
||||
this.carouselNodes = [n4, n0, n1, n2, n3];
|
||||
this.isAnimating = false;
|
||||
})
|
||||
.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 向右切换(查看下一个英雄):
|
||||
* 1. currentIndex 右移。
|
||||
* 2. 最左节点 n0 瞬间跳到最右位置,加载新英雄动画。
|
||||
* 3. 所有节点 tween 向左滑动一格。
|
||||
* 4. 动画完成后重排 carouselNodes 数组。
|
||||
*/
|
||||
private onNextClick() {
|
||||
if (!HeroList || HeroList.length === 0 || this.isAnimating || this.carouselNodes.length < 5) return;
|
||||
this.isAnimating = true;
|
||||
@@ -94,32 +162,47 @@ export class HListComp extends CCComp {
|
||||
|
||||
const [n0, n1, n2, n3, n4] = this.carouselNodes;
|
||||
|
||||
// n0 instantly jumps from leftmost to rightmost position to get ready to slide in
|
||||
// n0 瞬间跳至最右位置,准备滑入
|
||||
n0.setPosition(new Vec3(this.fixedPositions[4].x, n0.position.y, n0.position.z));
|
||||
this.updateNodeAnimationByOffset(n0, 2);
|
||||
this.updateHeroInfo();
|
||||
|
||||
// 所有节点向左滑动一格
|
||||
tween(n1).to(0.2, { position: new Vec3(this.fixedPositions[0].x, n1.position.y, n1.position.z) }).start();
|
||||
tween(n2).to(0.2, { position: new Vec3(this.fixedPositions[1].x, n2.position.y, n2.position.z) }).start();
|
||||
tween(n3).to(0.2, { position: new Vec3(this.fixedPositions[2].x, n3.position.y, n3.position.z) }).start();
|
||||
tween(n4).to(0.2, { position: new Vec3(this.fixedPositions[3].x, n4.position.y, n4.position.z) })
|
||||
.call(() => {
|
||||
// 重排数组:n0 成为新的最右节点
|
||||
this.carouselNodes = [n1, n2, n3, n4, n0];
|
||||
this.isAnimating = false;
|
||||
})
|
||||
.start();
|
||||
}
|
||||
|
||||
// ======================== 数据查询 ========================
|
||||
|
||||
/**
|
||||
* 根据偏移量获取英雄 UUID。
|
||||
* @param offset 相对于当前选中英雄的偏移(-2, -1, 0, 1, 2)
|
||||
* @returns HeroList 中对应位置的英雄 UUID
|
||||
*/
|
||||
private getHeroUuid(offset: number): number {
|
||||
const len = HeroList.length;
|
||||
return HeroList[(this.currentIndex + offset + len * 5) % len];
|
||||
}
|
||||
|
||||
/**
|
||||
* 按偏移量更新指定节点的英雄动画。
|
||||
* @param node 目标图标节点
|
||||
* @param offset 偏移量
|
||||
*/
|
||||
private updateNodeAnimationByOffset(node: Node, offset: number) {
|
||||
const uuid = this.getHeroUuid(offset);
|
||||
this.updateHeroAnimation(node, uuid);
|
||||
}
|
||||
|
||||
/** 更新当前选中英雄的详细信息(名称、AP、HP、技能列表) */
|
||||
private updateHeroInfo() {
|
||||
this.huuid = this.getHeroUuid(0);
|
||||
const hero = HeroInfo[this.huuid];
|
||||
@@ -131,6 +214,7 @@ export class HListComp extends CCComp {
|
||||
this.updateSkillInfo(hero);
|
||||
}
|
||||
|
||||
/** 初始化 5 个轮播位的英雄动画 */
|
||||
private initAllNodes() {
|
||||
if (this.carouselNodes.length < 5) return;
|
||||
this.updateNodeAnimationByOffset(this.carouselNodes[0], -2);
|
||||
@@ -140,6 +224,13 @@ export class HListComp extends CCComp {
|
||||
this.updateNodeAnimationByOffset(this.carouselNodes[4], 2);
|
||||
}
|
||||
|
||||
// ======================== UI 工具 ========================
|
||||
|
||||
/**
|
||||
* 安全设置 Label 文本
|
||||
* @param node 标签所在节点
|
||||
* @param text 文本内容
|
||||
*/
|
||||
private setLabelText(node: Node, text: string) {
|
||||
if (!node) return;
|
||||
const label = node.getComponent(Label) || node.getComponentInChildren(Label);
|
||||
@@ -148,6 +239,15 @@ export class HListComp extends CCComp {
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 英雄动画 ========================
|
||||
|
||||
/**
|
||||
* 为指定节点加载并播放英雄 idle 动画。
|
||||
* 使用 iconVisualTokens 做节点级竞态保护。
|
||||
*
|
||||
* @param node 图标节点
|
||||
* @param uuid 英雄 UUID
|
||||
*/
|
||||
private updateHeroAnimation(node: Node, uuid: number) {
|
||||
if (!node) return;
|
||||
const sprite = node.getComponent(Sprite) || node.getComponentInChildren(Sprite);
|
||||
@@ -159,6 +259,7 @@ export class HListComp extends CCComp {
|
||||
const anim = node.getComponent(Animation) || node.addComponent(Animation);
|
||||
this.clearAnimationClips(anim);
|
||||
|
||||
// 递增该节点的视觉令牌
|
||||
let token = (this.iconVisualTokens.get(node) || 0) + 1;
|
||||
this.iconVisualTokens.set(node, token);
|
||||
const path = `game/heros/hero/${hero.path}/idle`;
|
||||
@@ -168,6 +269,7 @@ export class HListComp extends CCComp {
|
||||
mLogger.log(this.debugMode, "HListComp", `load hero animation failed ${uuid}`, err);
|
||||
return;
|
||||
}
|
||||
// 竞态保护:令牌不匹配则丢弃
|
||||
if (token !== this.iconVisualTokens.get(node)) {
|
||||
return;
|
||||
}
|
||||
@@ -177,6 +279,7 @@ export class HListComp extends CCComp {
|
||||
});
|
||||
}
|
||||
|
||||
/** 移除 Animation 上的全部 clip(倒序遍历避免索引偏移) */
|
||||
private clearAnimationClips(anim: Animation) {
|
||||
const clips = anim.clips;
|
||||
if (clips && clips.length > 0) {
|
||||
@@ -187,6 +290,11 @@ export class HListComp extends CCComp {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归查找子节点(按名称)
|
||||
* @param root 起始节点
|
||||
* @param name 目标节点名
|
||||
*/
|
||||
private findNodeByName(root: Node, name: string): Node | null {
|
||||
if (!root) return null;
|
||||
if (root.name === name) return root;
|
||||
@@ -197,6 +305,15 @@ export class HListComp extends CCComp {
|
||||
return null;
|
||||
}
|
||||
|
||||
// ======================== 技能信息 ========================
|
||||
|
||||
/**
|
||||
* 更新技能信息面板:
|
||||
* 遍历英雄的技能列表,为 Line1~Line5 节点填充技能名、等级、CD、描述。
|
||||
* 同时根据技能类型(近战 / 远程 / 辅助)切换对应图标。
|
||||
*
|
||||
* @param hero 英雄配置数据(含 skills 字段)
|
||||
*/
|
||||
private updateSkillInfo(hero: any) {
|
||||
if (!this.info_node) return;
|
||||
|
||||
@@ -212,6 +329,7 @@ export class HListComp extends CCComp {
|
||||
const skillId = skill.uuid;
|
||||
const config = SkillSet[skillId];
|
||||
|
||||
// 拼接技能信息文本
|
||||
const text = config ? `${config.name} Lv.${skill.lv} CD:${skill.cd}s ${config.info}` : `未知技能 CD:${skill.cd}s`;
|
||||
|
||||
const noteNode = this.findNodeByName(line, "note");
|
||||
@@ -220,6 +338,7 @@ export class HListComp extends CCComp {
|
||||
label.string = text;
|
||||
}
|
||||
|
||||
// 切换技能类型图标
|
||||
this.updateLineTypeIcon(line, config?.IType);
|
||||
} else {
|
||||
line.active = false;
|
||||
@@ -227,6 +346,15 @@ export class HListComp extends CCComp {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新技能行的类型图标(互斥显示):
|
||||
* - Melee → 近战图标
|
||||
* - remote → 远程图标
|
||||
* - support → 辅助图标
|
||||
*
|
||||
* @param line 技能行节点
|
||||
* @param iType 技能类型枚举
|
||||
*/
|
||||
private updateLineTypeIcon(line: Node, iType?: IType) {
|
||||
const meleeNode = this.findNodeByName(line, "Melee");
|
||||
const remoteNode = this.findNodeByName(line, "remote");
|
||||
@@ -236,7 +364,7 @@ export class HListComp extends CCComp {
|
||||
if (supportNode) supportNode.active = iType === IType.support;
|
||||
}
|
||||
|
||||
/** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件是触发组件处理自定义释放逻辑 */
|
||||
/** ECS 组件移除时销毁节点 */
|
||||
reset() {
|
||||
this.node.destroy();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user