import { _decorator, CCFloat, Component, Node, Vec3, tween, Quat } from 'cc'; const { ccclass, property } = _decorator; @ccclass('ConstantSpeedParabola') export class ConstantSpeedParabola extends Component { // 抛物线高度系数(值越大,抛物线越高) @property({ tooltip: "控制抛物线高度,建议值0.2-0.5", min: 0.1, max: 1 }) heightRatio: number = 0.3; // 固定移动速度(单位:距离/秒) @property({ tooltip: "物体移动的固定速度(像素/秒)", min: 1 }) speed: number = 500; // 默认500像素/秒 @property({ tooltip: "控制抛物线形状,最高点放大的比例,建议值1-1.5", }) shapeScale: number = 1; /**旋转偏移(度)。大多数武器美术默认“朝向 +Y”,此时应为 -90;若美术朝向 +X 则填 0。 */ @property(CCFloat) rotationOffsetDeg: number = -90; /**是否让抛物线过程跟随方向旋转(剑这类看起来不舒服时可关掉) */ @property enableRotation: boolean = true; @property /**抛物线类型1:向上(取最高控制点) 2:向下(取最低控制点) 3:Y范围内随机控制点高度 */ parabolaType: number = 1; /**是否缩放 */ @property({ tooltip: "是否缩放", }) isScale: boolean = true; // 起点位置(可动态设置) private startPos: Vec3 = new Vec3(); // 终点位置(可动态设置) private endPos: Vec3 = new Vec3(); // 动态计算的控制点 private controlPoint1: Vec3 = new Vec3(); private controlPoint2: Vec3 = new Vec3(); // 动态计算的运动时长 private calculatedDuration: number = 0; // 缓存:避免 tween onUpdate 高频 new Vec3 造成 GC private _tmpBezierCurrent: Vec3 = new Vec3(); private _tmpBezierNext: Vec3 = new Vec3(); private _tmpDir: Vec3 = new Vec3(); start() { // // 开始运动 // this.startMotion(); } /** * 动态设置起点和终点,自动计算控制点和运动时长 * @param start 新起点 * @param end 新终点 */ setStartEndPos(start: Vec3, end: Vec3) { this.startPos = start.clone(); this.endPos = end.clone(); // 计算控制点 this.calculateControlPoints(); // 计算运动时长(核心:根据距离和速度) this.calculateDuration(); } /** * 计算运动时长(距离 ÷ 速度) */ private calculateDuration() { // 计算起点到终点的直线距离(作为基准) const distance = Vec3.distance(this.startPos, this.endPos); // 防止速度为0导致的错误 if (this.speed <= 0) { console.warn("速度必须大于0"); this.calculatedDuration = 0.1; // 最小时长 return; } // 时长 = 距离 ÷ 速度 this.calculatedDuration = distance / this.speed; // 确保时长不会过小(避免运动瞬间完成) if (this.calculatedDuration < 0.1) { this.calculatedDuration = 0.1; } } /** * 根据起点和终点计算控制点(确保抛物线形状) */ private calculateControlPoints() { const distanceX = this.endPos.x - this.startPos.x; const maxY = Math.max(this.startPos.y, this.endPos.y); const minY = Math.min(this.startPos.y, this.endPos.y); // 控制点X坐标(1/3和2/3处) const controlX1 = this.startPos.x + distanceX * 0.33; const controlX2 = this.startPos.x + distanceX * 0.67; // 控制点Y坐标(基于水平距离的比例) let peakHeight = Math.abs(distanceX) * this.heightRatio; let controlY1 = maxY + peakHeight; let controlY2 = maxY + peakHeight * 0.8; if (this.parabolaType == 2) { controlY1 = maxY - peakHeight; controlY2 = maxY - peakHeight * 0.8; } if (this.parabolaType == 3) { peakHeight = Math.random() * peakHeight; let up = Math.random() < 0.5; if (!up && this.startPos.y < -250) up = true; if (up) { controlY1 = maxY + peakHeight; controlY2 = maxY + peakHeight * 0.8; } else { controlY1 = minY - peakHeight; controlY2 = minY - peakHeight * 0.8; } } const z = this.startPos.z; this.controlPoint1.set(controlX1, controlY1, z); this.controlPoint2.set(controlX2, controlY2, z); } /** * 开始抛物线运动(使用动态计算的时长) */ startMotion(self: Node, targetPos: Vec3, speed: number, onComplete?: Function, finishTime: number = -1) { // 需要让 self 跟随运动方向更新角度(通常武器原始朝向为 +Y) this.speed = speed * gg.game.CurentBattle.TimeScale; // 示例:初始设置起点和终点 let startPos = self.position.clone() let endPos = targetPos.clone() this.setStartEndPos( startPos, // 左侧起点 endPos // 右侧终点 ); if (finishTime != -1) { this.calculatedDuration = finishTime; const distance = Vec3.distance(this.startPos, this.endPos); this.speed = distance / this.calculatedDuration; } else { //重新计算时间和speed } this.node.position = this.startPos; this.node.rotation = Quat.IDENTITY; // 重置缩放到原始大小 this.node.setScale(1, 1, 1); tween(this.node) // 使用计算出的时长,而非固定值 .to(this.calculatedDuration, {}, { onUpdate: (target: Node, ratio: number) => { const currentPos = this.calculateCubicBezierTo( this.startPos, this.controlPoint1, this.controlPoint2, this.endPos, ratio, this._tmpBezierCurrent ); target.position = currentPos; this.updateRotationWithCurrentPos(currentPos, ratio); // 根据运动进度计算缩放比例 // 贝塞尔曲线大约在0.5的进度时达到最高点 if (this.isScale) { const scaleFactor = this.calculateScaleFactor(ratio); target.setScale(scaleFactor, scaleFactor, 1); } } }) .call(() => { // 确保动画结束时恢复原始大小 this.node.setScale(1, 1, 1); onComplete?.(); // console.log(`曲线运动结束,实际时长: ${this.calculatedDuration.toFixed(2)}秒`); }) .start(); } /** * 根据运动进度计算缩放因子 * 在抛物线最高点附近放大,其他位置逐渐恢复 */ private calculateScaleFactor(ratio: number): number { // 贝塞尔曲线在0.5左右达到最高点 // 使用正弦函数创建平滑的缩放曲线,在0.5处达到最大值 const peakRatio = 0.5; // 计算当前ratio相对于peakRatio的距离,创建一个平滑的缩放曲线 const distanceFromPeak = Math.abs(ratio - peakRatio); // 使用指数衰减函数,使缩放变化更自然 const scaleFactor = 1 + (this.shapeScale - 1) * Math.exp(-distanceFromPeak * 8); return scaleFactor; } /** * 计算三次贝塞尔曲线上的点 */ private calculateCubicBezierTo( p0: Vec3, p1: Vec3, p2: Vec3, p3: Vec3, t: number, out: Vec3, ): Vec3 { const mt = 1 - t; const mt2 = mt * mt; const mt3 = mt2 * mt; const t2 = t * t; const t3 = t2 * t; out.x = mt3 * p0.x + 3 * mt2 * t * p1.x + 3 * mt * t2 * p2.x + t3 * p3.x; out.y = mt3 * p0.y + 3 * mt2 * t * p1.y + 3 * mt * t2 * p2.y + t3 * p3.y; out.z = mt3 * p0.z + 3 * mt2 * t * p1.z + 3 * mt * t2 * p2.z + t3 * p3.z; return out; } /** * 更新节点旋转(跟随运动方向) */ private updateRotationWithCurrentPos(currentPos: Vec3, t: number) { if (!this.enableRotation) return; // 终点附近用差分会出现 nextPos≈currentPos,方向向量趋近 0,导致 atan2(0,0) 角度跳变 // 因此在接近终点时不再更新旋转(保持上一帧角度),避免“最后一下拐一下”。 const epsilon = 0.001; if (t >= 1 - epsilon) return; const nextT = t + epsilon; const nextPos = this.calculateCubicBezierTo( this.startPos, this.controlPoint1, this.controlPoint2, this.endPos, nextT, this._tmpBezierNext ); Vec3.subtract(this._tmpDir, nextPos, currentPos); const dirLenSq = this._tmpDir.x * this._tmpDir.x + this._tmpDir.y * this._tmpDir.y; if (dirLenSq < 1e-8) return; // atan2 的 0 度在 +X 方向;美术若默认朝 +Y,需要额外 -90 度偏移 const angle = Math.atan2(this._tmpDir.y, this._tmpDir.x) * 180 / Math.PI + this.rotationOffsetDeg; this.node.setRotationFromEuler(0, 0, angle); } }