Files
heros/assets/script/game/BezierMove/BezierMove.ts
2025-06-12 16:24:23 +08:00

577 lines
23 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { _decorator, Component, Node, view, UITransform, Vec3, math, EventHandler, Graphics, Color, TweenEasing, Enum } from 'cc';
import { SkillCom } from '../skills/SkillCom';
const { ccclass, property } = _decorator;
// 定义缓动类型枚举
export enum EasingType {
linear = 'linear',
smooth = 'smooth',
fade = 'fade',
quadIn = 'quadIn',
quadOut = 'quadOut',
quadInOut = 'quadInOut',
quadOutIn = 'quadOutIn',
cubicIn = 'cubicIn',
cubicOut = 'cubicOut',
cubicInOut = 'cubicInOut',
cubicOutIn = 'cubicOutIn',
quartIn = 'quartIn',
quartOut = 'quartOut',
quartInOut = 'quartInOut',
quartOutIn = 'quartOutIn',
quintIn = 'quintIn',
quintOut = 'quintOut',
quintInOut = 'quintInOut',
quintOutIn = 'quintOutIn',
sineIn = 'sineIn',
sineOut = 'sineOut',
sineInOut = 'sineInOut',
sineOutIn = 'sineOutIn',
expoIn = 'expoIn',
expoOut = 'expoOut',
expoInOut = 'expoInOut',
expoOutIn = 'expoOutIn',
circIn = 'circIn',
circOut = 'circOut',
circInOut = 'circInOut',
circOutIn = 'circOutIn',
elasticIn = 'elasticIn',
elasticOut = 'elasticOut',
elasticInOut = 'elasticInOut',
elasticOutIn = 'elasticOutIn',
backIn = 'backIn',
backOut = 'backOut',
backInOut = 'backInOut',
backOutIn = 'backOutIn',
bounceIn = 'bounceIn',
bounceOut = 'bounceOut',
bounceInOut = 'bounceInOut',
bounceOutIn = 'bounceOutIn',
}
export enum ControlPointSideType {
Left = 1,
Right = -1,
Random = 0,
}
@ccclass('BezierMove')
export class BezierMove extends Component {
@property({ displayName: '速度' })
speed: number = 600; // 移动速度,单位:像素/秒
@property({ displayName: '控制点方向', tooltip: '0=随机, 1=左侧,-1=右侧(相对于从起点到终点的方向)' })
controlPointSide: ControlPointSideType = ControlPointSideType.Left; // 控制点位置0=随机1=左侧,-1=右侧(相对于从起点到终点的方向)
@property({ displayName: '控制点偏移系数', tooltip: '0~1之间,控制点偏移系数,值越大曲线越弯曲' })
controlPointOffset: number = 0.5; // 控制点偏移系数,值越大曲线越弯曲
@property({ displayName: '控制点随机性', tooltip: '0~1之间,控制点随机性,值越大随机性越强' })
controlPointRandomness: number = 0.3; // 控制点随机性,值越大随机性越强
@property({ displayName: '自动旋转' })
autoRotate: boolean = true; // 是否自动旋转以面向移动方向
@property({ displayName: '显示轨迹' })
showTrajectory: boolean = true; // 是否显示移动轨迹
@property({ displayName: '轨迹颜色' })
trajectoryColor: Color = new Color(0, 255, 0, 255); // 轨迹颜色,默认绿色
@property({ displayName: '轨迹宽度' })
trajectoryWidth: number = 3; // 轨迹宽度
@property({ displayName: '缓动函数', type: Enum(EasingType) })
easing: TweenEasing = EasingType.linear; // 缓动效果类型,默认为线性
@property({ displayName: '角度平滑系数', tooltip: '0~1之间,值越小旋转越平滑,0表示不旋转,1表示立即旋转' })
rotationSmoothness: number = 0.6; // 角度平滑系数,值越小旋转越平滑
private _graphics: Graphics | null = null; // Graphics组件引用
private _currentAngle: number = 0; // 当前角度
private _startPoint: Vec3 = new Vec3();
private _endPoint: Vec3 = new Vec3();
private _controlPoint: Vec3 = new Vec3();
private _t: number = 0; // Bezier曲线参数 (0 到 1)
private _totalTime: number = 0; // 完成当前曲线所需的时间
private _isMoving: boolean = false; // 是否正在移动
private _moveEndCallback: Function = null; // 移动完成回调函数
private _moveEndTarget: any; // 移动完成回调函数的目标对象
private _moveStartCallback: Function = null; // 移动开始回调函数, (totalTime: number) => void
private _moveStartTarget: any; // 移动开始回调函数的目标对象
onEnable() {
// 初始化起点为当前位置
this._startPoint.set(this.node.position);
// 初始化当前角度为节点当前角度
this._currentAngle = this.node.angle;
// 初始化Graphics组件用于绘制轨迹
this._initGraphics();
}
onDisable() {
// 清理Graphics资源
if (this._graphics) {
this._graphics.clear();
// 确保Graphics节点也被销毁
if (this._graphics.node && this._graphics.node.isValid) {
// 查找并销毁与当前实例关联的唯一轨迹节点
const uniqueNodeName = `TrajectoryGraphics_${this.node.uuid}`;
this._graphics.node.destroy();
}
}
}
onMoveComplete(callback: Function, target: any) {
this._moveEndCallback = callback;
this._moveEndTarget = target;
}
onMoveStart(callback: Function, target: any) {
this._moveStartCallback = callback;
this._moveStartTarget = target;
}
update(deltaTime: number) {
if (!this._isMoving || this._totalTime <= 0) return; // 如果没有移动或路径生成失败,则返回
// 更新进度
this._t += deltaTime / this._totalTime;
// 应用缓动效果
let easedT = this._applyEasing(this._t);
if (this._t >= 1.0) {
// 到达终点
this.node.setPosition(this._endPoint);
this._isMoving = false;
// 触发移动完成事件
console.log("onMoveComplete")
let skill=this.node.getComponent(SkillCom)
if(skill){
skill.doDestroy()
}
this._moveEndCallback && this._moveEndCallback.apply(this._moveEndTarget);
// 清除轨迹
if (this.showTrajectory && this._graphics) {
this._graphics.clear();
// 确保Graphics节点回到原始父节点
if (this._graphics.node.parent !== this.node) {
const graphicsNode = this._graphics.node;
if (graphicsNode.parent) {
graphicsNode.removeFromParent();
}
this.node.addChild(graphicsNode);
}
}
} else {
// 计算贝塞尔曲线上的当前位置使用缓动后的t值
const currentPos = this._calculateQuadraticBezierPoint(easedT, this._startPoint, this._controlPoint, this._endPoint);
this.node.setPosition(currentPos);
// 如果启用了自动旋转,计算切线(方向)进行旋转
if (this.autoRotate) {
const tangent = this._calculateQuadraticBezierTangent(easedT, this._startPoint, this._controlPoint, this._endPoint);
if (tangent.lengthSqr() > 0.001) { // 避免除以零或NaN角度
// 计算角度。Atan2给出弧度。
// 加90度是因为角度0指向上方在Cocos Creator 2D中沿Y轴
const targetAngle = math.toDegree(Math.atan2(tangent.y, tangent.x)) - 90;
// 使用平滑插值方法计算新角度
this._currentAngle = this._smoothAngle(this._currentAngle, targetAngle, this.rotationSmoothness);
this.node.angle = this._currentAngle;
}
}
}
}
/**
* 移动到指定位置
* @param targetPos 目标位置
*/
public moveTo(targetPos: Vec3): void {
// 设置起点为当前位置
this._startPoint.set(this.node.position);
// 设置终点为目标位置
this._endPoint.set(targetPos);
// 记录当前角度,用于平滑过渡
this._currentAngle = this.node.angle;
// 自动生成控制点
this._generateControlPoint();
// 估算曲线长度以计算总时间
const approxLength = Vec3.distance(this._startPoint, this._controlPoint) + Vec3.distance(this._controlPoint, this._endPoint);
this._totalTime = approxLength / this.speed;
if (this._totalTime < 0.1) { // 确保最小持续时间
this._totalTime = 0.1;
}
// 如果启用了轨迹显示,绘制轨迹
if (this.showTrajectory) {
this._drawTrajectory();
}
// 重置进度并开始移动
this._t = 0;
this._isMoving = true;
this._moveStartCallback && this._moveStartCallback.apply(this._moveStartTarget, [this._totalTime]);
}
/**
* 自动生成控制点
*/
private _generateControlPoint(): void {
// 计算起点和终点的中点
const midPoint = new Vec3();
Vec3.lerp(midPoint, this._startPoint, this._endPoint, 0.5);
// 计算从起点到终点的向量
const direction = new Vec3();
Vec3.subtract(direction, this._endPoint, this._startPoint);
// 计算垂直于方向的向量在2D中交换x和y并取反其中一个
const perpendicular = new Vec3(-direction.y, direction.x, 0);
Vec3.normalize(perpendicular, perpendicular);
// 根据controlPointSide参数决定控制点在哪一侧
let sideMultiplier = 1;
if (this.controlPointSide === ControlPointSideType.Random) {
// 随机选择一侧(与原来的行为一致)
sideMultiplier = Math.random() < 0.5 ? 1 : -1;
} else {
// 使用指定的侧面
sideMultiplier = this.controlPointSide;
}
// 根据控制点偏移系数和随机性计算控制点
const distance = Vec3.distance(this._startPoint, this._endPoint);
const offset = distance * this.controlPointOffset;
const randomFactor = (Math.random() * 2 - 1) * this.controlPointRandomness;
// 设置控制点 = 中点 + 垂直向量 * 偏移 * (1 + 随机因子) * 侧面乘数
this._controlPoint.set(
midPoint.x + perpendicular.x * offset * (1 + randomFactor) * sideMultiplier,
midPoint.y + perpendicular.y * offset * (1 + randomFactor) * sideMultiplier,
0
);
}
/**
* 停止当前移动
*/
stopMoving(): void {
this._isMoving = false;
}
/**
* 计算二次贝塞尔曲线上的点
*/
private _calculateQuadraticBezierPoint(t: number, p0: Vec3, p1: Vec3, p2: Vec3): Vec3 {
const u = 1 - t;
const tt = t * t;
const uu = u * u;
const p = new Vec3();
// p = (u^2 * p0) + (2 * u * t * p1) + (t^2 * p2)
Vec3.multiplyScalar(p, p0, uu);
const temp1 = new Vec3();
Vec3.multiplyScalar(temp1, p1, 2 * u * t);
Vec3.add(p, p, temp1);
const temp2 = new Vec3();
Vec3.multiplyScalar(temp2, p2, tt);
Vec3.add(p, p, temp2);
return p;
}
/**
* 计算二次贝塞尔曲线的切线(导数)
*/
private _calculateQuadraticBezierTangent(t: number, p0: Vec3, p1: Vec3, p2: Vec3): Vec3 {
const u = 1 - t;
const tangent = new Vec3();
// tangent = 2 * (1 - t) * (p1 - p0) + 2 * t * (p2 - p1)
const term1 = new Vec3();
Vec3.subtract(term1, p1, p0);
Vec3.multiplyScalar(term1, term1, 2 * u);
const term2 = new Vec3();
Vec3.subtract(term2, p2, p1);
Vec3.multiplyScalar(term2, term2, 2 * t);
Vec3.add(tangent, term1, term2);
return tangent;
}
/**
* 应用缓动效果到t值
* @param t 原始t值0-1之间
* @returns 应用缓动后的t值
*/
private _applyEasing(t: number): number {
// 确保t在0-1范围内
t = Math.max(0, Math.min(1, t));
// 使用tween.easing函数应用缓动
switch (this.easing) {
case EasingType.linear:
return t; // 线性不需要额外处理
case EasingType.smooth:
return t * t * (3 - 2 * t); // 平滑过渡
case EasingType.fade:
return t * t * (3 - 2 * t); // 平滑过渡
case EasingType.quadIn:
return t * t; // 二次方加速
case EasingType.quadOut:
return t * (2 - t); // 二次方减速
case EasingType.quadInOut:
return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; // 二次方加速然后减速
case EasingType.quadOutIn:
return t < 0.5 ? 0.5 * (1 - Math.pow(-2 * t + 1, 2)) : 0.5 * Math.pow(2 * t - 1, 2) + 0.5; // 二次方减速然后加速
case EasingType.cubicIn:
return t * t * t; // 三次方加速
case EasingType.cubicOut:
return (--t) * t * t + 1; // 三次方减速
case EasingType.cubicInOut:
return t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1; // 三次方加速然后减速
case EasingType.cubicOutIn:
return t < 0.5 ? 0.5 * ((t = t * 2 - 1) * t * t + 1) : 0.5 * (t = t * 2 - 1) * t * t + 0.5; // 三次方减速然后加速
case EasingType.quartIn:
return t * t * t * t; // 四次方加速
case EasingType.quartOut:
return 1 - (--t) * t * t * t; // 四次方减速
case EasingType.quartInOut:
return t < 0.5 ? 8 * t * t * t * t : 1 - 8 * (--t) * t * t * t; // 四次方加速然后减速
case EasingType.quartOutIn:
return t < 0.5 ? 0.5 * (1 - Math.pow(-2 * t + 1, 4)) : 0.5 * Math.pow(2 * t - 1, 4) + 0.5; // 四次方减速然后加速
case EasingType.quintIn:
return t * t * t * t * t; // 五次方加速
case EasingType.quintOut:
return 1 + (--t) * t * t * t * t; // 五次方减速
case EasingType.quintInOut:
return t < 0.5 ? 16 * t * t * t * t * t : 1 + 16 * (--t) * t * t * t * t; // 五次方加速然后减速
case EasingType.quintOutIn:
return t < 0.5 ? 0.5 * (1 - Math.pow(-2 * t + 1, 5)) : 0.5 * Math.pow(2 * t - 1, 5) + 0.5; // 五次方减速然后加速
case EasingType.sineIn:
return 1 - Math.cos(t * Math.PI / 2); // 正弦加速
case EasingType.sineOut:
return Math.sin(t * Math.PI / 2); // 正弦减速
case EasingType.sineInOut:
return 0.5 * (1 - Math.cos(Math.PI * t)); // 正弦加速然后减速
case EasingType.sineOutIn:
return t < 0.5 ? 0.5 * Math.sin(t * Math.PI) : 0.5 - 0.5 * Math.cos((t * 2 - 1) * Math.PI / 2); // 正弦减速然后加速
case EasingType.circIn:
return 1 - Math.sqrt(1 - t * t); // 圆形加速
case EasingType.circOut:
return Math.sqrt(1 - (t - 1) * (t - 1)); // 圆形减速
case EasingType.circInOut:
return t < 0.5
? (1 - Math.sqrt(1 - 4 * t * t)) / 2
: (Math.sqrt(1 - Math.pow(-2 * t + 2, 2)) + 1) / 2; // 圆形加速然后减速
case EasingType.circOutIn:
return t < 0.5 ? 0.5 * Math.sqrt(1 - Math.pow(-2 * t + 1, 2)) : 0.5 * (2 - Math.sqrt(1 - Math.pow(2 * t - 1, 2))); // 圆形减速然后加速
case EasingType.expoIn:
return t === 0 ? 0 : Math.pow(2, 10 * t - 10); // 指数加速
case EasingType.expoOut:
return t === 1 ? 1 : 1 - Math.pow(2, -10 * t); // 指数减速
case EasingType.expoInOut:
return t === 0 ? 0 : t === 1 ? 1 : t < 0.5
? Math.pow(2, 20 * t - 10) / 2
: (2 - Math.pow(2, -20 * t + 10)) / 2; // 指数加速然后减速
case EasingType.expoOutIn:
return t === 0 ? 0 : t === 1 ? 1 : t < 0.5
? 0.5 * (1 - Math.pow(2, -20 * t))
: 0.5 * Math.pow(2, 20 * (t - 0.5) - 10) + 0.5; // 指数减速然后加速
case EasingType.backIn:
const s = 1.70158;
return t * t * ((s + 1) * t - s); // 回弹加速
case EasingType.backOut:
const s2 = 1.70158;
return (t = t - 1) * t * ((s2 + 1) * t + s2) + 1; // 回弹减速
case EasingType.backInOut:
const s3 = 1.70158 * 1.525;
if (t < 0.5) {
return (t * 2) * (t * 2) * ((s3 + 1) * (t * 2) - s3) / 2;
} else {
return ((t * 2 - 2) * (t * 2 - 2) * ((s3 + 1) * (t * 2 - 2) + s3) + 2) / 2;
} // 回弹加速然后减速
case EasingType.backOutIn:
const s4 = 1.70158;
return t < 0.5
? 0.5 * ((t = t * 2 - 1) * t * ((s4 + 1) * t + s4) + 1)
: 0.5 * (t = t * 2 - 1) * t * ((s4 + 1) * t - s4) + 0.5; // 回弹减速然后加速
case EasingType.elasticIn:
return t === 0 ? 0 : t === 1 ? 1 : -Math.pow(2, 10 * (t - 1)) * Math.sin((t - 1.1) * 5 * Math.PI); // 弹性加速
case EasingType.elasticOut:
return t === 0 ? 0 : t === 1 ? 1 : Math.pow(2, -10 * t) * Math.sin((t - 0.1) * 5 * Math.PI) + 1; // 弹性减速
case EasingType.elasticInOut:
if (t === 0) return 0;
if (t === 1) return 1;
if (t < 0.5) {
return -0.5 * Math.pow(2, 20 * t - 10) * Math.sin((20 * t - 11.125) * Math.PI / 2.25);
} else {
return 0.5 * Math.pow(2, -20 * t + 10) * Math.sin((20 * t - 11.125) * Math.PI / 2.25) + 1;
} // 弹性加速然后减速
case EasingType.elasticOutIn:
if (t === 0) return 0;
if (t === 1) return 1;
if (t < 0.5) {
return 0.5 * Math.pow(2, -20 * t) * Math.sin((20 * t - 0.5) * 5 * Math.PI) + 0.5;
} else {
return 0.5 * -Math.pow(2, 10 * (2 * t - 1.5)) * Math.sin((2 * t - 1.6) * 5 * Math.PI) + 0.5;
} // 弹性减速然后加速
case EasingType.bounceIn:
return 1 - this._bounceOut(1 - t); // 弹跳加速
case EasingType.bounceOut:
return this._bounceOut(t); // 弹跳减速
case EasingType.bounceInOut:
return t < 0.5
? (1 - this._bounceOut(1 - 2 * t)) / 2
: (1 + this._bounceOut(2 * t - 1)) / 2; // 弹跳加速然后减速
case EasingType.bounceOutIn:
return t < 0.5
? this._bounceOut(t * 2) / 2
: (1 - this._bounceOut(2 - 2 * t)) / 2 + 0.5; // 弹跳减速然后加速
default:
return t; // 默认线性
}
}
/**
* 辅助函数:弹跳减速效果
*/
private _bounceOut(t: number): number {
if (t < 1 / 2.75) {
return 7.5625 * t * t;
} else if (t < 2 / 2.75) {
return 7.5625 * (t -= 1.5 / 2.75) * t + 0.75;
} else if (t < 2.5 / 2.75) {
return 7.5625 * (t -= 2.25 / 2.75) * t + 0.9375;
} else {
return 7.5625 * (t -= 2.625 / 2.75) * t + 0.984375;
}
}
/**
* 初始化Graphics组件
*/
private _initGraphics(): void {
// 为每个MovingTarget实例创建唯一的轨迹节点名称避免多个实例共享同一个轨迹节点
const uniqueNodeName = `TrajectoryGraphics_${this.node.uuid}`;
// 尝试查找已存在的轨迹节点基于唯一ID
let graphicsNode = this.node.parent?.getChildByName(uniqueNodeName);
// 如果不存在,则创建新的轨迹节点
if (!graphicsNode) {
graphicsNode = new Node(uniqueNodeName);
// 初始时先添加到场景中但不设置父节点会在_drawTrajectory中设置
if (this.node.parent) {
this.node.parent.addChild(graphicsNode);
} else {
this.node.addChild(graphicsNode);
}
}
// 获取或添加Graphics组件到节点
this._graphics = graphicsNode.getComponent(Graphics);
if (!this._graphics) {
this._graphics = graphicsNode.addComponent(Graphics);
}
}
/**
* 清除轨迹
*/
public clearTrajectory(): void {
if (!this._graphics) return;
// 清除之前的轨迹
this._graphics.clear();
}
/**
* 绘制轨迹
*/
private _drawTrajectory(): void {
if (!this.showTrajectory) return;
if (!this._graphics) return;
// 清除之前的轨迹
this._graphics.clear();
// 设置轨迹样式
this._graphics.lineWidth = this.trajectoryWidth;
this._graphics.strokeColor = this.trajectoryColor;
// 获取Graphics所在节点
const graphicsNode = this._graphics.node;
// 将Graphics节点设置为与主节点的父节点相同确保轨迹在世界坐标系中绘制
if (this.node.parent) {
graphicsNode.parent = this.node.parent;
// 移动到起点(世界坐标系中的实际位置)
this._graphics.moveTo(this._startPoint.x, this._startPoint.y);
// 直接使用控制点和终点的世界坐标绘制贝塞尔曲线
this._graphics.quadraticCurveTo(
this._controlPoint.x, this._controlPoint.y,
this._endPoint.x, this._endPoint.y
);
}
// 应用绘制
this._graphics.stroke();
}
/**
* 平滑角度插值,确保选择最短路径旋转
* @param currentAngle 当前角度
* @param targetAngle 目标角度
* @param smoothFactor 平滑系数0-1值越小越平滑
* @returns 插值后的新角度
*/
private _smoothAngle(currentAngle: number, targetAngle: number, smoothFactor: number): number {
// 确保平滑系数在有效范围内
smoothFactor = Math.max(0.001, Math.min(1, smoothFactor));
// 标准化角度到 0-360 范围
const normalizeAngle = (angle: number): number => {
angle = angle % 360;
return angle < 0 ? angle + 360 : angle;
};
// 标准化当前角度和目标角度
const normCurrent = normalizeAngle(currentAngle);
const normTarget = normalizeAngle(targetAngle);
// 计算最短路径旋转方向
let delta = normTarget - normCurrent;
// 如果角度差大于180度选择另一个方向旋转最短路径
if (delta > 180) {
delta -= 360;
} else if (delta < -180) {
delta += 360;
}
// 应用平滑系数进行插值
return currentAngle + delta * smoothFactor;
}
}