匀速+旋转 的bezier运动

效果看链接
https://www.bilibili.com/video/BV1f8QUYMEEd/?vd_source=a6ba2f806c0fbc75f155b7a1cf9a9620
正常情况下,,用tween实现bezier公式计算的运动,在转弯地方,运动是非常缓慢的,直线的地方,运动会很快。
而且,使用tween,没法实现运动的时候自动转弯。

一般的使用方法:tween

修改后 匀速 + 旋转的bezier动画


不再传入运动时间,而是传入运动速度。

如何使用

具体思路

///------------------------------------------------------
import { _decorator, CCFloat, Component, Node, Quat, Vec3 } from ‘cc’;
const { ccclass, property } = _decorator;

export enum XS_BEZIER_COM_STATE {
Idle = 0,
Moving,
Pause,
End,
}

/**

  • 贝塞尔曲线组件

  • 匀速 + 旋转 的移动
    */
    @ccclass(‘BezierCom’)
    export class BezierCom extends Component {

    @property(CCFloat)
    moveSpeed = 100;

    protected totalLength; //贝塞尔曲线轨迹长度
    protected traveledLength; //已经走过的长度

    protected pStar; //起点
    protected pCtrl1; //控制点1
    protected pCtrl2; //控制点2
    protected pEnd; //终点

    protected isStart = false; //是否开始移动

    protected actionState = XS_BEZIER_COM_STATE.Idle; //状态

    protected callbackOfEndAction: Function = null; //回调函数

    private _autoDestroy = false; //自动销毁

    /**

    • 是否自动销毁
    • true:播放完会自动销毁
    • false:组件需要自己管理
      */
      set autoDestroy(value: boolean) {
      this._autoDestroy = value;
      }

    /**
    *

    • @param pStar 起点
    • @param pCtrl1 控制点1
    • @param pCtrl2 控制点2
    • @param pEnd 终点
    • @param moveSpeed 移动速度
    • @param callbackOfEndAction 回调函数,动画播放完会调用
      */
      public setBezierPath(pStar: Vec3, pCtrl1: Vec3, pCtrl2: Vec3, pEnd: Vec3, moveSpeed?: number, callbackOfEndAction?: Function) {
      this.pStar = pStar;
      this.pCtrl1 = pCtrl1;
      this.pCtrl2 = pCtrl2;
      this.pEnd = pEnd;
      this.moveSpeed = moveSpeed;
      this.totalLength = this.calculateBezierLength(this.pStar, this.pCtrl1, this.pCtrl2, this.pEnd, 1000);
      this.traveledLength = 0;
      this.callbackOfEndAction = callbackOfEndAction;
      return {
      duration: this.totalLength / this.moveSpeed,
      distance: this.totalLength,
      }
      }

    //获取动作状态
    getActionState() {
    return this.actionState;
    }

    //随时都可以更改动作速度
    setMoveSpeed(moveSpeed) {
    this.moveSpeed = moveSpeed;
    }

    //
    /**

    • 设置回调函数,运动过程中,可能切换回调,可以这个,但是如果动画已经播放完了,才设置,就不会生效
    • 设置前,先判断状态再设置
    • @param callbackOfEndAction
    • @returns
      */
      setCallbackOfEndAction(callbackOfEndAction): boolean {
      if (this.moveSpeed == XS_BEZIER_COM_STATE.End) {
      return false; //已经播放完了,设置无效
      }
      this.callbackOfEndAction = callbackOfEndAction;
      return true;
      }

    play() {
    this.traveledLength = 0;
    this.isStart = true;
    this.actionState = XS_BEZIER_COM_STATE.Moving;
    }

    pause() {
    this.isStart = false;
    this.actionState = XS_BEZIER_COM_STATE.Pause;
    }

    continue() {
    this.isStart = true;
    this.actionState = XS_BEZIER_COM_STATE.Moving;
    }

    stop() {
    this.isStart = false;
    this.traveledLength = 0;
    this.actionState = XS_BEZIER_COM_STATE.End;
    this.callbackOfEndAction && this.callbackOfEndAction();

     if (this._autoDestroy) {                //如果自动销毁,动画结束则会删除组件
         this.destroy();
     }
    

    }

    update(deltaTime: number) {
    if (!this.isStart) return;

     // 每次更新需要移动的弧长(基于速度)
     let stepLength = this.moveSpeed * deltaTime;  // 匀速移动的距离
    
     // 限制最大距离为路径总长度
     if (this.traveledLength + stepLength > this.totalLength) {
         stepLength = this.totalLength - this.traveledLength;
     }
    
     // 累积已走的弧长
     this.traveledLength += stepLength;
    
     // 计算根据当前弧长所对应的 t 值
     let t = this.calculateTFromLength(this.traveledLength);
    
     // 计算物体在贝塞尔曲线上的位置
     let position = this.getBezierPoint3D(t, this.pStar, this.pCtrl1, this.pCtrl2, this.pEnd);
    
     //根据当前的坐标与目标坐标的差值,计算旋转角度 
     // 计算切线方向
     let tangent = this.calculateTangent(t);
    
     // 根据切线方向计算旋转
     this.adjustRotation(tangent);
    
     // 设置物体位置
     this.node.setWorldPosition(position);
    
     // 如果到达终点,停止运动
     if (this.traveledLength >= this.totalLength) {
         this.stop();
     }
    

    }

    //计算轨迹长度
    private calculateBezierLength(p1: Vec3, cp1: Vec3, cp2: Vec3, p2: Vec3, numSegments = 200) {

     let length = 0;
     let lastPos = p1;
    
     for (let i = 0; i <= numSegments; i++) {
         let t = i / numSegments;
         let x = (1 - t) * (1 - t) * (1 - t) * p1.x + 3 * t * (1 - t) * (1 - t) * cp1.x + 3 * t * t * (1 - t) * cp2.x + t * t * t * p2.x;
         let y = (1 - t) * (1 - t) * (1 - t) * p1.y + 3 * t * (1 - t) * (1 - t) * cp1.y + 3 * t * t * (1 - t) * cp2.y + t * t * t * p2.y;
         let z = (1 - t) * (1 - t) * (1 - t) * p1.z + 3 * t * (1 - t) * (1 - t) * cp1.z + 3 * t * t * (1 - t) * cp2.z + t * t * t * p2.z;
    
         let subX = x - lastPos.x;
         let subY = y - lastPos.y;
         let subZ = z - lastPos.z;
    
         let distance = Math.sqrt(subX * subX + subY * subY + subZ * subZ);
         length += distance;
    
         lastPos = new Vec3(x, y, z); //记录上一次的位置
     }
    
     return length;
    

    }

    //获取时间t刻的贝塞尔坐标点
    private getBezierPoint3D(t, p1, cp1, cp2, p2) {
    //优化计算 todo ,提前计算 1- t
    let x = (1 - t) * (1 - t) * (1 - t) * p1.x + 3 * t * (1 - t) * (1 - t) * cp1.x + 3 * t * t * (1 - t) * cp2.x + t * t * t * p2.x;
    let y = (1 - t) * (1 - t) * (1 - t) * p1.y + 3 * t * (1 - t) * (1 - t) * cp1.y + 3 * t * t * (1 - t) * cp2.y + t * t * t * p2.y;
    let z = (1 - t) * (1 - t) * (1 - t) * p1.z + 3 * t * (1 - t) * (1 - t) * cp1.z + 3 * t * t * (1 - t) * cp2.z + t * t * t * p2.z;
    return new Vec3(x, y, z);
    }

    // 根据已走弧长计算 t
    private calculateTFromLength(traveledLength: number): number {
    let accumulatedLength = 0;
    let numSegments = 1000; // 使用更高精度来细分路径
    let t = 0;

     // 累加路径上的弧长,直到总弧长超过 traveledLength
     for (let i = 0; i < numSegments; i++) {
         let segmentT = i / numSegments;
         let nextSegmentT = (i + 1) / numSegments;
    
         // 计算每段弧长
         let segmentLength = this.calculateSegmentLength(segmentT, nextSegmentT);
    
         accumulatedLength += segmentLength;
    
         // 当累计的弧长大于等于 traveledLength 时,返回对应的 t 值
         if (accumulatedLength >= traveledLength) {
             t = nextSegmentT;
             break;
         }
     }
    
     return t;
    

    }

    // 计算贝塞尔曲线段之间的弧长(细分成小段)
    private calculateSegmentLength(t1: number, t2: number): number {
    const p1 = this.getBezierPoint3D(t1, this.pStar, this.pCtrl1, this.pCtrl2, this.pEnd);
    const p2 = this.getBezierPoint3D(t2, this.pStar, this.pCtrl1, this.pCtrl2, this.pEnd);
    return p1.subtract(p2).length(); // 返回两点之间的直线距离
    }

    // 计算曲线在t位置的切线方向
    private calculateTangent(t: number): Vec3 {
    const epsilon = 0.001; // 小的偏移量,用于计算切线

     // 计算 t 处和 t+epsilon 处的贝塞尔曲线坐标
     let p1 = this.getBezierPoint3D(t, this.pStar, this.pCtrl1, this.pCtrl2, this.pEnd);
     let p2 = this.getBezierPoint3D(t + epsilon, this.pStar, this.pCtrl1, this.pCtrl2, this.pEnd);
    
     // 计算切线方向
     let tangent = p2.subtract(p1).normalize(); // 方向向量
     return tangent;
    

    }

    // 根据切线方向调整物体的旋转
    private adjustRotation(tangent: Vec3) {
    // 计算物体当前的朝向(假设物体初始朝向沿着z轴)
    let forward = new Vec3(1, 0, 0);

     // 计算物体旋转角度:使用向量的夹角来计算
     let angle = Vec3.angle(forward, tangent); // 获取朝向与切线之间的角度
    
     // 计算旋转轴:即两个向量的叉积
     let axis = forward.cross(tangent).normalize();
    
     // 创建一个四元数对象
     let quat = new Quat();
    
     // 使用旋转轴和角度来构造四元数
     Quat.fromAxisAngle(quat, axis, angle);
    
     // 将旋转应用到物体
     this.node.setWorldRotation(quat);
    

    }

}

9赞

哎哟,不错哦。坐等组件代码。

厉害,Mark

markk

实现过,甚至可以多段曲线连续匀速运动 :rofl: :rofl: :rofl:Cocos Store

2赞

处理过贝塞尔曲线匀速问题,一般使用三种解决方案。
1.根据每两个路径点之间的距离和配置速度计算运动时间,使用tween进行移动。注意ease设置为linear。(推荐)
2.控制物体朝向路径点移动,接近或到达路径点则切换下一个目标。这个不建议,如果速度过快或路径点之间距离太小容易出现判断不到的情况,类似物理穿透问题。
3.实现等距路径点的贝塞尔曲线算法。但是只能做到相对等距,不过肉眼观测不出差距,如果不是要求非常精确可以使用。

为什么没法转弯呢?朝向目标路径点不可以吗

可以肯定可以,只是个人觉得用那个不方便,不如自己封装好一个满足自己需求