import { _decorator, Component, Node, view, UITransform, Vec3, math, EventHandler, Graphics, Color, TweenEasing, Enum } from 'cc'; import { SkillCom } from '../skills/SkillCom'; import { smc } from '../common/SingletonModuleComp'; 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||smc.mission.pause||!smc.mission.play) 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; } }