效果看链接
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);}
}



