326 lines
13 KiB
C#
326 lines
13 KiB
C#
using UnityEngine;
|
||
|
||
namespace Cielonos.MainGame.Characters
|
||
{
|
||
/// <summary>
|
||
/// 描述单次击退/击飞的脉冲数据。
|
||
/// 每个轴(XZ / Y)独立管理各自的脉冲,可以同时存在击退和击飞。
|
||
/// </summary>
|
||
public struct ImpulseData
|
||
{
|
||
/// <summary>归一化方向(XZ 为水平方向,Y 轴为 Vector3.up)。</summary>
|
||
public Vector3 Direction;
|
||
|
||
/// <summary>初始速度(m/s)。</summary>
|
||
public float InitialSpeed;
|
||
|
||
/// <summary>脉冲总持续时间(秒)。抛物线模式下设为 float.MaxValue,由落地检测终止。</summary>
|
||
public float Duration;
|
||
|
||
/// <summary>速度衰减曲线,横轴 [0,1] 归一化时间,纵轴 [0,1] 速度倍率。仅曲线模式使用。</summary>
|
||
public AnimationCurve Curve;
|
||
|
||
/// <summary>已经过的时间(秒)。</summary>
|
||
public float ElapsedTime;
|
||
|
||
/// <summary>是否使用抛物线物理公式(v = v0 - g*t)。击飞专用。</summary>
|
||
public bool IsParabolic;
|
||
|
||
/// <summary>抛物线模式下的重力加速度(m/s²)。</summary>
|
||
public float Gravity;
|
||
|
||
/// <summary>当前脉冲是否仍在生效。</summary>
|
||
public bool IsActive => ElapsedTime < Duration && Duration > 0f;
|
||
|
||
/// <summary>
|
||
/// 采样当前帧的速度绝对值(m/s)。
|
||
/// 抛物线模式返回 |v0 - g*t|,用于脉冲替换判定。
|
||
/// </summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 采样当前帧的带符号速度(m/s)。仅抛物线模式有意义,正值上升、负值下落。
|
||
/// 曲线模式始终返回正值。
|
||
/// </summary>
|
||
public float SampleSignedSpeed()
|
||
{
|
||
if (!IsActive) return 0f;
|
||
|
||
if (IsParabolic)
|
||
{
|
||
return InitialSpeed - Gravity * ElapsedTime;
|
||
}
|
||
|
||
return SampleSpeed();
|
||
}
|
||
|
||
/// <summary>推进时间并返回本帧的位移向量。</summary>
|
||
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);
|
||
}
|
||
|
||
/// <summary>清除脉冲数据,使其失效。</summary>
|
||
public void Clear()
|
||
{
|
||
ElapsedTime = float.MaxValue;
|
||
InitialSpeed = 0f;
|
||
Duration = 0f;
|
||
IsParabolic = false;
|
||
Gravity = 0f;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 管理角色受击时的附加力(击退 / 击飞)。
|
||
/// 使用脉冲替换机制,每个轴独立管理一个 ImpulseData。
|
||
/// </summary>
|
||
public class ImpulseSubmodule : SubmoduleBase<MovementSubcontrollerBase>
|
||
{
|
||
private const float DEFAULT_KNOCKBACK_DURATION = 0.5f;
|
||
private const float SPEED_EPSILON = 0.01f;
|
||
|
||
private CharacterBase character => owner.owner;
|
||
|
||
/// <summary>水平击退脉冲(XZ 平面)。</summary>
|
||
public ImpulseData knockbackImpulse;
|
||
|
||
/// <summary>垂直击飞脉冲(Y 轴)。</summary>
|
||
public ImpulseData launchImpulse;
|
||
|
||
/// <summary>
|
||
/// 默认的 EaseOut 衰减曲线。
|
||
/// 起始速度最大,后半段缓慢归零,模拟"被推开后站稳"的手感。
|
||
/// </summary>
|
||
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
|
||
// ────────────────────────────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// 施加一次击退脉冲(XZ 平面)。
|
||
/// 使用"取较大值替换"策略:如果新脉冲的初始速度大于当前剩余速度,则替换;否则忽略。
|
||
/// </summary>
|
||
/// <param name="direction">击退方向(仅使用 XZ 分量,会自动归一化)。</param>
|
||
/// <param name="initialSpeed">初始速度(m/s)。</param>
|
||
/// <param name="duration">持续时间(秒)。传入 0 或负数时使用默认值。</param>
|
||
/// <param name="curve">衰减曲线。传入 null 时使用默认 EaseOut 曲线。</param>
|
||
/// <param name="ignoreResistance">是否无视 ImpactResistance 抗性。</param>
|
||
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
|
||
};
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 施加一次击飞脉冲(Y 轴)。
|
||
/// 当 isParabolic 为 true 时使用物理公式 v = v0 - g*t 产生真实抛物线轨迹,
|
||
/// 脉冲由落地检测终止而非固定时长;为 false 时使用曲线衰减。
|
||
/// </summary>
|
||
/// <param name="initialSpeed">初始上升速度(m/s)。</param>
|
||
/// <param name="duration">持续时间(秒)。抛物线模式下忽略此参数。</param>
|
||
/// <param name="curve">衰减曲线。抛物线模式下忽略此参数。</param>
|
||
/// <param name="ignoreResistance">是否无视 ImpactResistance 抗性。</param>
|
||
/// <param name="isParabolic">是否使用抛物线物理。默认 false。</param>
|
||
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
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 施加一次完整的力(自动拆分为 XZ 击退 + Y 击飞)。
|
||
/// 兼容旧的 force 矢量调用方式。
|
||
/// </summary>
|
||
/// <param name="force">力的矢量(XZ 分量为击退,Y 分量为击飞)。</param>
|
||
/// <param name="ignoreResistance">是否无视 ImpactResistance 抗性。</param>
|
||
/// <param name="duration">持续时间(秒)。</param>
|
||
/// <param name="curve">衰减曲线。</param>
|
||
/// <param name="isParabolic">Y 分量是否使用抛物线物理。</param>
|
||
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);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 每帧调用,推进脉冲时间并返回本帧的总位移量。
|
||
/// </summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 立即清除所有正在生效的脉冲。
|
||
/// </summary>
|
||
public void ClearAll()
|
||
{
|
||
knockbackImpulse.Clear();
|
||
launchImpulse.Clear();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 立即清除 Y 轴击飞脉冲。
|
||
/// </summary>
|
||
public void ClearLaunch()
|
||
{
|
||
launchImpulse.Clear();
|
||
}
|
||
|
||
/// <summary>当前是否有任何脉冲在生效。</summary>
|
||
public bool HasActiveImpulse => knockbackImpulse.IsActive || launchImpulse.IsActive;
|
||
|
||
/// <summary>当前 XZ 击退的剩余速度。</summary>
|
||
public float CurrentKnockbackSpeed => knockbackImpulse.SampleSpeed();
|
||
|
||
/// <summary>当前 Y 击飞的剩余速度(绝对值)。</summary>
|
||
public float CurrentLaunchSpeed => launchImpulse.SampleSpeed();
|
||
|
||
// ────────────────────────────────────────────────────────────────────
|
||
// Internal
|
||
// ────────────────────────────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// 抗性缩放结果:同时包含速度和持续时间的缩放值。
|
||
/// </summary>
|
||
private readonly struct ResistanceResult
|
||
{
|
||
public readonly float Speed;
|
||
public readonly float Duration;
|
||
|
||
public ResistanceResult(float speed, float duration)
|
||
{
|
||
Speed = speed;
|
||
Duration = duration;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 根据角色 ImpulseResistance 属性缩放脉冲参数。
|
||
/// 速度线性缩放;持续时间使用平方根缩放,使高抗性敌人更快停下而非慢慢滑行。
|
||
/// </summary>
|
||
/// <param name="speed">原始速度。</param>
|
||
/// <param name="duration">原始持续时间。</param>
|
||
/// <param name="ignoreResistance">是否无视抗性(击飞等)。</param>
|
||
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);
|
||
}
|
||
}
|
||
}
|