using UnityEngine;
namespace Cielonos.MainGame.Characters
{
///
/// 描述单次击退/击飞的脉冲数据。
/// 每个轴(XZ / Y)独立管理各自的脉冲,可以同时存在击退和击飞。
///
public struct ImpulseData
{
/// 归一化方向(XZ 为水平方向,Y 轴为 Vector3.up)。
public Vector3 Direction;
/// 初始速度(m/s)。
public float InitialSpeed;
/// 脉冲总持续时间(秒)。抛物线模式下设为 float.MaxValue,由落地检测终止。
public float Duration;
/// 速度衰减曲线,横轴 [0,1] 归一化时间,纵轴 [0,1] 速度倍率。仅曲线模式使用。
public AnimationCurve Curve;
/// 已经过的时间(秒)。
public float ElapsedTime;
/// 是否使用抛物线物理公式(v = v0 - g*t)。击飞专用。
public bool IsParabolic;
/// 抛物线模式下的重力加速度(m/s²)。
public float Gravity;
/// 当前脉冲是否仍在生效。
public bool IsActive => ElapsedTime < Duration && Duration > 0f;
///
/// 采样当前帧的速度绝对值(m/s)。
/// 抛物线模式返回 |v0 - g*t|,用于脉冲替换判定。
///
public float SampleSpeed()
{
if (!IsActive) return 0f;
if (IsParabolic)
{
return Mathf.Abs(InitialSpeed - Gravity * ElapsedTime);
}
float t = Mathf.Clamp01(ElapsedTime / Duration);
float curveValue = Curve != null ? Curve.Evaluate(t) : (1f - t);
return InitialSpeed * curveValue;
}
///
/// 采样当前帧的带符号速度(m/s)。仅抛物线模式有意义,正值上升、负值下落。
/// 曲线模式始终返回正值。
///
public float SampleSignedSpeed()
{
if (!IsActive) return 0f;
if (IsParabolic)
{
return InitialSpeed - Gravity * ElapsedTime;
}
return SampleSpeed();
}
/// 推进时间并返回本帧的位移向量。
public Vector3 Advance(float deltaTime)
{
if (!IsActive) return Vector3.zero;
if (IsParabolic)
{
// v(t) = v0 - g*t,正值上升、负值下落,产生真实抛物线
float currentVelocity = InitialSpeed - Gravity * ElapsedTime;
ElapsedTime += deltaTime;
return Direction * (currentVelocity * deltaTime);
}
float speed = SampleSpeed();
ElapsedTime += deltaTime;
return Direction * (speed * deltaTime);
}
/// 清除脉冲数据,使其失效。
public void Clear()
{
ElapsedTime = float.MaxValue;
InitialSpeed = 0f;
Duration = 0f;
IsParabolic = false;
Gravity = 0f;
}
}
///
/// 管理角色受击时的附加力(击退 / 击飞)。
/// 使用脉冲替换机制,每个轴独立管理一个 ImpulseData。
///
public class ImpulseSubmodule : SubmoduleBase
{
private const float DEFAULT_KNOCKBACK_DURATION = 0.5f;
private const float SPEED_EPSILON = 0.01f;
private CharacterBase character => owner.owner;
/// 水平击退脉冲(XZ 平面)。
public ImpulseData knockbackImpulse;
/// 垂直击飞脉冲(Y 轴)。
public ImpulseData launchImpulse;
///
/// 默认的 EaseOut 衰减曲线。
/// 起始速度最大,后半段缓慢归零,模拟"被推开后站稳"的手感。
///
private static readonly AnimationCurve DefaultEaseOutCurve = new AnimationCurve(
new Keyframe(0f, 1f, 0f, -0.5f),
new Keyframe(1f, 0f, -2f, 0f)
);
public ImpulseSubmodule(MovementSubcontrollerBase movementSc) : base(movementSc)
{
knockbackImpulse = new ImpulseData();
launchImpulse = new ImpulseData();
}
// ────────────────────────────────────────────────────────────────────
// Public API
// ────────────────────────────────────────────────────────────────────
///
/// 施加一次击退脉冲(XZ 平面)。
/// 使用"取较大值替换"策略:如果新脉冲的初始速度大于当前剩余速度,则替换;否则忽略。
///
/// 击退方向(仅使用 XZ 分量,会自动归一化)。
/// 初始速度(m/s)。
/// 持续时间(秒)。传入 0 或负数时使用默认值。
/// 衰减曲线。传入 null 时使用默认 EaseOut 曲线。
/// 是否无视 ImpactResistance 抗性。
public void ApplyKnockback(Vector3 direction, float initialSpeed,
float duration = DEFAULT_KNOCKBACK_DURATION, AnimationCurve curve = null,
bool ignoreResistance = false)
{
float rawDuration = duration > 0f ? duration : DEFAULT_KNOCKBACK_DURATION;
ResistanceResult result = ApplyResistance(initialSpeed, rawDuration, ignoreResistance);
if (result.Speed <= SPEED_EPSILON) return;
Vector3 flatDir = new Vector3(direction.x, 0f, direction.z).normalized;
if (flatDir.sqrMagnitude < 0.001f) return;
AnimationCurve effectiveCurve = curve ?? DefaultEaseOutCurve;
// 取较大值替换:新脉冲速度 > 当前剩余速度时才替换
float remainingSpeed = knockbackImpulse.SampleSpeed();
if (result.Speed > remainingSpeed)
{
knockbackImpulse = new ImpulseData
{
Direction = flatDir,
InitialSpeed = result.Speed,
Duration = result.Duration,
Curve = effectiveCurve,
ElapsedTime = 0f
};
}
}
///
/// 施加一次击飞脉冲(Y 轴)。
/// 当 isParabolic 为 true 时使用物理公式 v = v0 - g*t 产生真实抛物线轨迹,
/// 脉冲由落地检测终止而非固定时长;为 false 时使用曲线衰减。
///
/// 初始上升速度(m/s)。
/// 持续时间(秒)。抛物线模式下忽略此参数。
/// 衰减曲线。抛物线模式下忽略此参数。
/// 是否无视 ImpactResistance 抗性。
/// 是否使用抛物线物理。默认 false。
public void ApplyLaunch(float initialSpeed, float duration,
AnimationCurve curve = null, bool ignoreResistance = true,
bool isParabolic = false)
{
float rawDuration = duration > 0f ? duration : DEFAULT_KNOCKBACK_DURATION;
ResistanceResult result = ApplyResistance(initialSpeed, rawDuration, ignoreResistance);
if (result.Speed <= SPEED_EPSILON) return;
float remainingSpeed = launchImpulse.SampleSpeed();
if (result.Speed <= remainingSpeed) return;
if (isParabolic)
{
// 清除跳跃速度,避免 jumpVelocity 与击飞脉冲叠加
owner.jumpVelocity = 0f;
}
launchImpulse = new ImpulseData
{
Direction = Vector3.up,
InitialSpeed = result.Speed,
// 抛物线模式:重力接管衰减,Duration 设为极大值,由落地检测终止
Duration = isParabolic ? float.MaxValue : result.Duration,
Curve = isParabolic ? null : (curve ?? DefaultEaseOutCurve),
ElapsedTime = 0f,
IsParabolic = isParabolic,
Gravity = isParabolic ? owner.normalGravity : 0f
};
}
///
/// 施加一次完整的力(自动拆分为 XZ 击退 + Y 击飞)。
/// 兼容旧的 force 矢量调用方式。
///
/// 力的矢量(XZ 分量为击退,Y 分量为击飞)。
/// 是否无视 ImpactResistance 抗性。
/// 持续时间(秒)。
/// 衰减曲线。
/// Y 分量是否使用抛物线物理。
public void ApplyImpulse(Vector3 force, bool ignoreResistance = false,
float duration = DEFAULT_KNOCKBACK_DURATION, AnimationCurve curve = null,
bool isParabolic = false)
{
Vector3 flatForce = new Vector3(force.x, 0f, force.z);
float xzSpeed = flatForce.magnitude;
if (xzSpeed > SPEED_EPSILON)
{
ApplyKnockback(flatForce, xzSpeed, duration, curve, ignoreResistance);
}
if (Mathf.Abs(force.y) > SPEED_EPSILON)
{
ApplyLaunch(force.y, duration, curve, ignoreResistance, isParabolic);
}
}
///
/// 每帧调用,推进脉冲时间并返回本帧的总位移量。
///
public Vector3 Update(float deltaTime)
{
Vector3 displacement = Vector3.zero;
if (knockbackImpulse.IsActive)
{
displacement += knockbackImpulse.Advance(deltaTime);
}
if (launchImpulse.IsActive)
{
displacement += launchImpulse.Advance(deltaTime);
}
return displacement;
}
///
/// 立即清除所有正在生效的脉冲。
///
public void ClearAll()
{
knockbackImpulse.Clear();
launchImpulse.Clear();
}
///
/// 立即清除 Y 轴击飞脉冲。
///
public void ClearLaunch()
{
launchImpulse.Clear();
}
/// 当前是否有任何脉冲在生效。
public bool HasActiveImpulse => knockbackImpulse.IsActive || launchImpulse.IsActive;
/// 当前 XZ 击退的剩余速度。
public float CurrentKnockbackSpeed => knockbackImpulse.SampleSpeed();
/// 当前 Y 击飞的剩余速度(绝对值)。
public float CurrentLaunchSpeed => launchImpulse.SampleSpeed();
// ────────────────────────────────────────────────────────────────────
// Internal
// ────────────────────────────────────────────────────────────────────
///
/// 抗性缩放结果:同时包含速度和持续时间的缩放值。
///
private readonly struct ResistanceResult
{
public readonly float Speed;
public readonly float Duration;
public ResistanceResult(float speed, float duration)
{
Speed = speed;
Duration = duration;
}
}
///
/// 根据角色 ImpulseResistance 属性缩放脉冲参数。
/// 速度线性缩放;持续时间使用平方根缩放,使高抗性敌人更快停下而非慢慢滑行。
///
/// 原始速度。
/// 原始持续时间。
/// 是否无视抗性(击飞等)。
private ResistanceResult ApplyResistance(float speed, float duration, bool ignoreResistance)
{
if (ignoreResistance) return new ResistanceResult(speed, duration);
float resistance = character.attributeSm["ImpulseResistance"];
float multiplier = Mathf.Clamp01(1f - (resistance / 100f));
float scaledSpeed = speed * multiplier;
// 持续时间使用 sqrt 缩放:resistance 75 → speed 25%, duration 50%
// 重型敌人会快速站稳,而非以低速缓缓滑行
float scaledDuration = duration * Mathf.Sqrt(multiplier);
return new ResistanceResult(scaledSpeed, scaledDuration);
}
}
}