327 lines
11 KiB
TypeScript
327 lines
11 KiB
TypeScript
const { ccclass, property } = cc._decorator;
|
||
|
||
@ccclass
|
||
export default class Test extends cc.Component {
|
||
|
||
@property({ tooltip: "整体动画倍速(1为原速,2为两倍速)" })
|
||
animationSpeed: number = 3;
|
||
|
||
@property({ tooltip: "每段完整动画结束后的观察间隔(秒)" })
|
||
nextStepDelay: number = 1.5;
|
||
|
||
private slotPositions: cc.Vec3[] = [];
|
||
private path: cc.Node[] = [];
|
||
private retractGlow: cc.Node = null;
|
||
|
||
start(): void {
|
||
// Test 下的藤蔓节点按 Hierarchy 顺序组成从起点到终点的路径。
|
||
this.path = this.node.children.filter(node => node.active && !!this.getBodySprite(node));
|
||
|
||
if (this.path.length < 2) {
|
||
cc.warn("Test 节点下至少需要两个有效藤蔓节点");
|
||
return;
|
||
}
|
||
|
||
const lastIndex = this.path.length - 1;
|
||
if (this.path[0].name.indexOf("end_") === 0
|
||
&& this.path[lastIndex].name.indexOf("start_") === 0) {
|
||
this.path.reverse();
|
||
}
|
||
|
||
this.slotPositions = this.path.map(node => node.position.clone());
|
||
if (!this.validatePath() || !this.validateSprites()) return;
|
||
|
||
for (let index = 0; index < lastIndex; index++) {
|
||
const sprite = this.getFirstChildSprite(this.path[index]);
|
||
if (sprite) {
|
||
sprite.fillRange = 0;
|
||
sprite.node.opacity = 0;
|
||
}
|
||
}
|
||
|
||
const tailPosition = this.slotPositions[lastIndex];
|
||
const previousPosition = this.slotPositions[lastIndex - 1];
|
||
const glowStart = cc.v3(
|
||
tailPosition.x + (tailPosition.x - previousPosition.x) / 2,
|
||
tailPosition.y + (tailPosition.y - previousPosition.y) / 2
|
||
);
|
||
|
||
this.retractGlow = this.createRetractGlow(glowStart);
|
||
this.playRound(lastIndex);
|
||
}
|
||
|
||
private playRound(tailIndex: number): void {
|
||
const tail = this.path[tailIndex];
|
||
const lastIndex = this.path.length - 1;
|
||
const duration = this.getStepDuration();
|
||
|
||
if (tailIndex < lastIndex) {
|
||
this.retractGlow.setPosition(this.slotPositions[tailIndex]);
|
||
}
|
||
this.retractGlow.active = true;
|
||
this.moveRetractGlowThroughRound(tailIndex);
|
||
if (tailIndex === lastIndex) {
|
||
this.animateFillRange(tail);
|
||
} else {
|
||
this.animateFirstChildFillRange(tail);
|
||
}
|
||
|
||
cc.tween(this.node)
|
||
.delay(duration)
|
||
.call(() => this.playNextSegment(tailIndex - 1))
|
||
.start();
|
||
}
|
||
|
||
private playNextSegment(nextIndex: number): void {
|
||
if (nextIndex >= 0) {
|
||
const nextSegment = this.path[nextIndex];
|
||
const duration = this.getStepDuration();
|
||
|
||
this.animateFirstChildAppear(nextSegment);
|
||
this.animateFillRange(nextSegment);
|
||
cc.tween(this.node)
|
||
.delay(duration)
|
||
.call(() => this.finishRound(nextIndex))
|
||
.start();
|
||
return;
|
||
}
|
||
|
||
this.retractGlow.active = false;
|
||
}
|
||
|
||
private finishRound(nextTailIndex: number): void {
|
||
this.retractGlow.active = false;
|
||
this.scheduleOnce(() => this.playRound(nextTailIndex), this.nextStepDelay);
|
||
}
|
||
|
||
private animateFillRange(segment: cc.Node): void {
|
||
const sprite = this.getBodySprite(segment);
|
||
if (!sprite) {
|
||
cc.warn(`藤蔓 ${segment.name} 缺少用于消失动画的 Sprite 子节点`);
|
||
return;
|
||
}
|
||
|
||
const duration = this.getStepDuration();
|
||
const fullRange = sprite.fillRange < 0 ? -1 : 1;
|
||
const targetRange = sprite.fillType === cc.Sprite.FillType.RADIAL
|
||
? fullRange * 0.8
|
||
: 0;
|
||
|
||
sprite.fillRange = fullRange;
|
||
cc.tween(sprite)
|
||
.to(duration, { fillRange: targetRange }, { easing: "linear" })
|
||
.start();
|
||
}
|
||
|
||
private animateFirstChildAppear(segment: cc.Node): void {
|
||
const sprite = this.getFirstChildSprite(segment);
|
||
if (!sprite) return;
|
||
|
||
const duration = this.getStepDuration();
|
||
sprite.fillRange = 0;
|
||
sprite.node.opacity = 0;
|
||
cc.tween(sprite)
|
||
.delay(duration / 3)
|
||
.to(duration / 2, { fillRange: 1 }, { easing: "linear" })
|
||
.start();
|
||
cc.tween(sprite.node)
|
||
.delay(duration / 3)
|
||
.to(duration / 2, { opacity: 255 }, { easing: "linear" })
|
||
.start();
|
||
}
|
||
|
||
private animateFirstChildFillRange(segment: cc.Node): void {
|
||
const sprite = this.getFirstChildSprite(segment);
|
||
if (!sprite) return;
|
||
|
||
const duration = this.getStepDuration();
|
||
cc.tween(sprite)
|
||
.to(duration, { fillRange: 0 }, { easing: "linear" })
|
||
.start();
|
||
}
|
||
|
||
private getFirstChildSprite(segment: cc.Node): cc.Sprite {
|
||
const firstChild = segment.children[0];
|
||
const sprite = firstChild && firstChild.getComponent(cc.Sprite);
|
||
if (!sprite) {
|
||
cc.warn(`藤蔓 ${segment.name} 缺少第一个 Sprite 子节点`);
|
||
}
|
||
|
||
return sprite;
|
||
}
|
||
|
||
private getBodyNode(segment: cc.Node): cc.Node {
|
||
return segment.children[2] || segment.children[1];
|
||
}
|
||
|
||
private getBodySprite(segment: cc.Node): cc.Sprite {
|
||
const bodyNode = this.getBodyNode(segment);
|
||
return bodyNode && bodyNode.getComponent(cc.Sprite);
|
||
}
|
||
|
||
private getStepDuration(): number {
|
||
return 1 / Math.max(this.animationSpeed, 0.01);
|
||
}
|
||
|
||
private validatePath(): boolean {
|
||
for (let index = 1; index < this.slotPositions.length; index++) {
|
||
const previous = this.slotPositions[index - 1];
|
||
const current = this.slotPositions[index];
|
||
const deltaX = Math.abs(current.x - previous.x);
|
||
const deltaY = Math.abs(current.y - previous.y);
|
||
|
||
if ((deltaX < 0.01 && deltaY < 0.01)
|
||
|| (deltaX >= 0.01 && deltaY >= 0.01)) {
|
||
cc.warn(`藤蔓路径 ${this.path[index - 1].name} -> ${this.path[index].name} 不是有效的上下左右相邻关系`);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
private validateSprites(): boolean {
|
||
const lastIndex = this.path.length - 1;
|
||
|
||
for (let index = 0; index <= lastIndex; index++) {
|
||
const segment = this.path[index];
|
||
const bodySprite = this.getBodySprite(segment);
|
||
if (!bodySprite || bodySprite.type !== cc.Sprite.Type.FILLED) {
|
||
cc.warn(`藤蔓 ${segment.name} 的主体 Sprite 必须使用 FILLED 类型`);
|
||
return false;
|
||
}
|
||
|
||
if (this.isCorner(index) && bodySprite.fillType !== cc.Sprite.FillType.RADIAL) {
|
||
cc.warn(`拐角藤蔓 ${segment.name} 的主体 Sprite 必须使用 RADIAL 填充`);
|
||
return false;
|
||
}
|
||
|
||
if (index < lastIndex) {
|
||
const firstSprite = this.getFirstChildSprite(segment);
|
||
if (!firstSprite || firstSprite.type !== cc.Sprite.Type.FILLED) {
|
||
cc.warn(`藤蔓 ${segment.name} 的第一个 Sprite 子节点必须使用 FILLED 类型`);
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
private getTowardStartPosition(index: number): cc.Vec3 {
|
||
if (index > 0) return this.slotPositions[index - 1];
|
||
|
||
const start = this.slotPositions[0];
|
||
const next = this.slotPositions[1];
|
||
return cc.v3(start.x + start.x - next.x, start.y + start.y - next.y);
|
||
}
|
||
|
||
private isCorner(index: number): boolean {
|
||
if (index <= 0 || index >= this.slotPositions.length - 1) return false;
|
||
|
||
const current = this.slotPositions[index];
|
||
const towardEnd = this.slotPositions[index + 1];
|
||
const towardStart = this.slotPositions[index - 1];
|
||
const endX = towardEnd.x - current.x;
|
||
const endY = towardEnd.y - current.y;
|
||
const startX = towardStart.x - current.x;
|
||
const startY = towardStart.y - current.y;
|
||
|
||
return Math.abs(endX * startY - endY * startX) > 0.01;
|
||
}
|
||
|
||
private moveRetractGlowThroughRound(tailIndex: number): void {
|
||
const tween = cc.tween(this.retractGlow);
|
||
const lastMovementIndex = Math.max(0, tailIndex - 1);
|
||
const duration = this.getStepDuration();
|
||
|
||
for (let index = tailIndex; index >= lastMovementIndex; index--) {
|
||
const segment = this.path[index];
|
||
const from = this.slotPositions[index];
|
||
const toward = this.getTowardStartPosition(index);
|
||
const isCorner = this.isCorner(index);
|
||
const fillNode = this.getBodyNode(segment);
|
||
const stopsAtCenter = index !== tailIndex
|
||
&& fillNode
|
||
&& fillNode.name.indexOf("start_") === 0;
|
||
|
||
if (stopsAtCenter) {
|
||
tween.to(duration, { position: from }, { easing: "linear" });
|
||
} else if (isCorner && index !== tailIndex) {
|
||
tween
|
||
.to(duration / 2, { position: from }, { easing: "linear" })
|
||
.to(duration / 2, {
|
||
position: this.getBoundaryPosition(from, toward)
|
||
}, { easing: "linear" });
|
||
} else {
|
||
tween.to(duration, {
|
||
position: this.getBoundaryPosition(from, toward)
|
||
}, { easing: "linear" });
|
||
}
|
||
}
|
||
|
||
tween.start();
|
||
}
|
||
|
||
private getBoundaryPosition(from: cc.Vec3, toward: cc.Vec3): cc.Vec3 {
|
||
return cc.v3((from.x + toward.x) / 2, (from.y + toward.y) / 2);
|
||
}
|
||
|
||
private createRetractGlow(position: cc.Vec3): cc.Node {
|
||
const root = new cc.Node("retract_glow");
|
||
const pulse = new cc.Node("pulse");
|
||
const graphics = pulse.addComponent(cc.Graphics);
|
||
const orbit = new cc.Node("orbit");
|
||
|
||
graphics.fillColor = cc.color(145, 255, 45, 45);
|
||
graphics.circle(0, 0, 30);
|
||
graphics.fill();
|
||
graphics.fillColor = cc.color(190, 255, 70, 95);
|
||
graphics.circle(0, 0, 19);
|
||
graphics.fill();
|
||
graphics.fillColor = cc.color(245, 255, 170, 240);
|
||
graphics.circle(0, 0, 7);
|
||
graphics.fill();
|
||
|
||
for (let index = 0; index < 4; index++) {
|
||
const spark = new cc.Node("spark");
|
||
const sparkGraphics = spark.addComponent(cc.Graphics);
|
||
const angle = Math.PI * index / 2;
|
||
|
||
spark.setPosition(Math.cos(angle) * 24, Math.sin(angle) * 24);
|
||
sparkGraphics.fillColor = cc.color(220, 255, 95, 210);
|
||
sparkGraphics.circle(0, 0, 3);
|
||
sparkGraphics.fill();
|
||
orbit.addChild(spark);
|
||
}
|
||
|
||
root.setPosition(position);
|
||
root.zIndex = 2000;
|
||
root.addChild(pulse);
|
||
root.addChild(orbit);
|
||
this.node.addChild(root);
|
||
|
||
cc.tween(pulse)
|
||
.repeatForever(
|
||
cc.tween()
|
||
.to(0.4, { scale: 1.18, opacity: 150 }, { easing: "sineInOut" })
|
||
.to(0.4, { scale: 0.9, opacity: 255 }, { easing: "sineInOut" })
|
||
)
|
||
.start();
|
||
cc.tween(orbit)
|
||
.by(1.2, { angle: 360 })
|
||
.repeatForever()
|
||
.start();
|
||
|
||
return root;
|
||
}
|
||
|
||
onDestroy(): void {
|
||
this.unscheduleAllCallbacks();
|
||
cc.Tween.stopAllByTarget(this.node);
|
||
if (this.retractGlow && cc.isValid(this.retractGlow)) {
|
||
cc.Tween.stopAllByTarget(this.retractGlow);
|
||
}
|
||
}
|
||
}
|