using System; using System.Collections.Generic; using System.Linq; using Sirenix.OdinInspector; using SLSUtilities.General; using SLSUtilities.FunctionalAnimation; using UnityEngine; using UnityEngine.Serialization; using Random = UnityEngine.Random; namespace Cielonos.MainGame.Characters { /// /// 动画子控制器基类。 /// 负责管理角色的核心 Animator、动作覆盖、打断状态以及受击时的骨骼抖动物理效果。 /// public partial class AnimationSubcontrollerBase : SubcontrollerBase { #region Fields & Properties [TitleGroup("Animator Settings", "底层状态机配置", Alignment = TitleAlignments.Centered)] public Animator animator; public AnimatorStateMapper mapper; [TitleGroup("Functional Animation Submodules", "动作播放子模块", Alignment = TitleAlignments.Centered)] public FuncAnimSubmodule fullBodyFuncAnimSm; public FuncAnimSubmodule upperBodyFuncAnimSm; [TitleGroup("State Flags & Intervals", "打断状态与控制标志", Alignment = TitleAlignments.Centered)] public Dictionary disruptionStatus; public bool isDuringRootMotion; public bool isDisablingMoveXZ; public bool isDisablingMoveY; public List LastFrameIntervals { get; private set; } public List CurrentIntervals { get; private set; } [TitleGroup("Callbacks", "事件回调注册表", Alignment = TitleAlignments.Centered)] public Dictionary> registeredFunctions; #region Bone Shake Fields [TitleGroup("Bone Shake Settings", "受击骨骼震动物理参数", Alignment = TitleAlignments.Centered)] [Tooltip("刚度:越大回弹越快。对于重型机甲,建议 150-300;轻型无人机建议 80-150。")] public float stiffness = 200f; [Tooltip("阻尼:越大停得越快,建议 10-20。")] public float damping = 15f; [Tooltip("冲击力倍率:将受击强度转化为震动初速度")] public float impactForceMultiplier = 500f; [Tooltip("参与受击震动的测试骨骼列表")] public List testShakeBones; private List activeShakes = new List(); private class BoneShakeState { public Transform bone; public Vector3 shakeAxis; public float currentAngle = 0f; public float velocity = 0f; } #endregion #endregion #region Initialization public override void Initialize() { base.Initialize(); fullBodyFuncAnimSm = new FuncAnimSubmodule(this, "FullBodyAction"); upperBodyFuncAnimSm = new FuncAnimSubmodule(this, "UpperBodyAction"); registeredFunctions = new Dictionary>(); disruptionStatus = new Dictionary() { { DisruptionType.ForcedAction , false}, { DisruptionType.ForcedExternal , false}, { DisruptionType.NormalExternal, false }, { DisruptionType.NormalAction, false }, { DisruptionType.Movement, false } }; LastFrameIntervals = new List(); CurrentIntervals = new List(); RegisterDefaultFunctions(); } public virtual void RegisterDefaultFunctions() { // 在派生类中扩展具体的动作事件回调注册 } #endregion #region Unity Lifecycle (Update & LateUpdate) protected virtual void Update() { if (owner.statusSm.isDead) { return; } fullBodyFuncAnimSm?.UpdateTime(); UpdateIntervalInfo(); } protected virtual void LateUpdate() { fullBodyFuncAnimSm?.UpdateEvents(); BoneShakeLateUpdate(); } #endregion #region Interval Evaluation protected virtual void UpdateIntervalInfo() { if (fullBodyFuncAnimSm?.currentRuntimeFuncAnim == null) { isDuringRootMotion = false; return; } RuntimeFuncAnim currentFuncAnim = fullBodyFuncAnimSm.currentRuntimeFuncAnim; LastFrameIntervals = CurrentIntervals; CurrentIntervals = currentFuncAnim.GetEnablingIntervals(); disruptionStatus[DisruptionType.NormalExternal] = CurrentIntervals.Any(interval => interval.intervalType == IntervalType.ExternalDisruption); disruptionStatus[DisruptionType.NormalAction] = CurrentIntervals.Any(interval => interval.intervalType == IntervalType.ActionDisruption); disruptionStatus[DisruptionType.Movement] = CurrentIntervals.Any(interval => interval.intervalType == IntervalType.MovementDisruption); if (currentFuncAnim.HasIntervalType(IntervalType.ForcedActionDisruption)) { disruptionStatus[DisruptionType.ForcedAction] = CurrentIntervals.Any(interval => interval.intervalType == IntervalType.ForcedActionDisruption); } else { disruptionStatus[DisruptionType.ForcedAction] = true; } if (currentFuncAnim.HasIntervalType(IntervalType.ForcedExternalDisruption)) { disruptionStatus[DisruptionType.ForcedExternal] = CurrentIntervals.Any(interval => interval.intervalType == IntervalType.ForcedExternalDisruption); } else { disruptionStatus[DisruptionType.ForcedExternal] = true; } isDuringRootMotion = currentFuncAnim.funcAnimData.animInfo.useRootMotion && CurrentIntervals.Any(interval => interval.intervalType == IntervalType.RootMotion); } #endregion #region Bone Shake Core private void BoneShakeLateUpdate() { float dt = owner.selfTimeSm.DeltaTime; for (int i = activeShakes.Count - 1; i >= 0; i--) { var state = activeShakes[i]; if (state.bone == null) { activeShakes.RemoveAt(i); continue; } // 阻尼弹簧物理公式 (Hooke's Law + Damping): F = -k * x - d * v float force = -stiffness * state.currentAngle - damping * state.velocity; state.velocity += force * dt; state.currentAngle += state.velocity * dt; // 应用偏转角度 Quaternion shakeRot = Quaternion.AngleAxis(state.currentAngle, state.shakeAxis); state.bone.localRotation = state.bone.localRotation * shakeRot; // 能量极小时剔除,避免持续旋转计算开销 if (Mathf.Abs(state.currentAngle) < 0.1f && Mathf.Abs(state.velocity) < 0.1f) { activeShakes.RemoveAt(i); } } } public void ApplyBoneShake(Transform hitBone, Vector3 hitDirection, float intensity) { var state = activeShakes.Find(x => x.bone == hitBone); if (state == null) { state = new BoneShakeState(); state.bone = hitBone; activeShakes.Add(state); } // 给予物理弹簧初速度 (Impulse) state.velocity += intensity * impactForceMultiplier; // 计算垂直于受击方向的震动轴 Vector3 axis = Vector3.Cross(hitDirection, Vector3.up).normalized; if (axis == Vector3.zero) axis = Vector3.right; state.shakeAxis = axis; } #endregion #region Hit Feedbacks public virtual void PlayGetHitBoneShake(float intensity, Vector3 direction = default) { if (direction == default) { direction = owner.transform.right; } else { direction = Quaternion.Euler(0, 90, 0) * direction; } foreach (Transform bone in testShakeBones) { ApplyBoneShake(bone, direction, intensity); } } public virtual void PlayGetHitAnimation(string funcAnimName, out float animDuration, Vector3 direction = default) { animDuration = 0.2f; if (fullBodyFuncAnimSm.Play(funcAnimName)) { FuncAnimInterval interval = fullBodyFuncAnimSm.currentData.Interval(IntervalType.MovementDisruption); animDuration = MathExtensions.Average(interval.StartTime, interval.EndTime); } } #endregion } }