You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
279 lines
9.4 KiB
279 lines
9.4 KiB
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);
|
|
}
|
|
} |