Files
Cielonos/Assets/Scripts/MainGame/Characters/Base/Submodules/ImpulseSubmodule.cs
SoulliesOfficial 649b7a5ddc 更新
2026-05-23 08:27:50 -04:00

326 lines
13 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
}
}