消消方块阵换皮表情
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

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);
}
}