feat(英雄列表): 添加轮播动画效果以提升用户体验

- 新增两个英雄图标节点用于扩展轮播视图
- 实现平滑的轮播动画,点击前后按钮时图标会滑动切换
- 添加动画状态锁防止动画冲突
- 重构英雄信息更新逻辑,支持五节点轮播布局
- 使用tween实现位置动画,优化视觉流畅度
This commit is contained in:
panw
2026-04-01 17:25:59 +08:00
parent edf3d4713c
commit 10acb8c068
2 changed files with 1067 additions and 581 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
import { _decorator, Animation, AnimationClip, Button, Event, Label, Node, NodeEventType, Sprite, resources } from "cc";
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";
import { HeroInfo, HeroList } from "../common/config/heroSet";
@@ -19,6 +19,10 @@ export class HListComp extends CCComp {
@property(Node)
nhero_icon=null!
@property(Node)
phero1_icon=null!
@property(Node)
nhero1_icon=null!
@property(Node)
ap_node=null!
@property(Node)
hp_node=null!
@@ -35,6 +39,9 @@ export class HListComp extends CCComp {
huuid:number=null!
private currentIndex: number = 0;
private iconVisualTokens: Map<Node, number> = new Map();
private isAnimating: boolean = false;
private carouselNodes: Node[] = [];
private fixedPositions: Vec3[] = [];
debugMode: boolean = false;
onLoad() {
@@ -43,49 +50,96 @@ export class HListComp extends CCComp {
}
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.updateHeroView();
this.initAllNodes();
this.updateHeroInfo();
}
}
private onPreClick() {
if (!HeroList || HeroList.length === 0) return;
if (!HeroList || HeroList.length === 0 || this.isAnimating || this.carouselNodes.length < 5) return;
this.isAnimating = true;
this.currentIndex = (this.currentIndex - 1 + HeroList.length) % HeroList.length;
this.updateHeroView();
const [n0, n1, n2, n3, n4] = this.carouselNodes;
// n4 instantly jumps from rightmost to leftmost position to get ready to slide in
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(() => {
this.carouselNodes = [n4, n0, n1, n2, n3];
this.isAnimating = false;
})
.start();
}
private onNextClick() {
if (!HeroList || HeroList.length === 0) return;
if (!HeroList || HeroList.length === 0 || this.isAnimating || this.carouselNodes.length < 5) return;
this.isAnimating = true;
this.currentIndex = (this.currentIndex + 1) % HeroList.length;
this.updateHeroView();
const [n0, n1, n2, n3, n4] = this.carouselNodes;
// n0 instantly jumps from leftmost to rightmost position to get ready to slide in
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(() => {
this.carouselNodes = [n1, n2, n3, n4, n0];
this.isAnimating = false;
})
.start();
}
private updateHeroView() {
this.huuid = HeroList[this.currentIndex];
private getHeroUuid(offset: number): number {
const len = HeroList.length;
return HeroList[(this.currentIndex + offset + len * 5) % len];
}
private updateNodeAnimationByOffset(node: Node, offset: number) {
const uuid = this.getHeroUuid(offset);
this.updateHeroAnimation(node, uuid);
}
private updateHeroInfo() {
this.huuid = this.getHeroUuid(0);
const hero = HeroInfo[this.huuid];
if (!hero) return;
// 获取前后英雄的 uuid
const preIndex = (this.currentIndex - 1 + HeroList.length) % HeroList.length;
const nextIndex = (this.currentIndex + 1) % HeroList.length;
const pUuid = HeroList[preIndex];
const nUuid = HeroList[nextIndex];
// 更新基础属性标签
this.setLabelText(this.name_node, hero.name);
this.setLabelText(this.ap_node, `攻击力: ${hero.ap}`);
this.setLabelText(this.hp_node, `生命值: ${hero.hp}`);
// 更新动画
this.updateHeroAnimation(this.hero_icon, this.huuid);
this.updateHeroAnimation(this.phero_icon, pUuid);
this.updateHeroAnimation(this.nhero_icon, nUuid);
// 更新技能列表
this.updateSkillInfo(hero);
}
private initAllNodes() {
if (this.carouselNodes.length < 5) return;
this.updateNodeAnimationByOffset(this.carouselNodes[0], -2);
this.updateNodeAnimationByOffset(this.carouselNodes[1], -1);
this.updateNodeAnimationByOffset(this.carouselNodes[2], 0);
this.updateNodeAnimationByOffset(this.carouselNodes[3], 1);
this.updateNodeAnimationByOffset(this.carouselNodes[4], 2);
}
private setLabelText(node: Node, text: string) {
if (!node) return;
const label = node.getComponent(Label) || node.getComponentInChildren(Label);