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