using System; using System.Collections.Generic; using System.Linq; using SLSFramework.General; using SLSUtilities.FunctionalAnimation; using UnityEngine; using UnityEngine.Serialization; using Random = UnityEngine.Random; namespace Cielonos.MainGame.Characters { public partial class AnimationSubcontrollerBase : SubcontrollerBase { public Animator animator; public Dictionary disruptionStatus; public bool isDuringRootMotion; public bool isDisablingMoveXZ; public bool isDisablingMoveY; public BaseAnimationGroup defaultAnimationGroup; public FunctionalAnimationSubmodule fullBodyFuncAnimSm; public Dictionary> registeredFunctions; public List lastFrameIntervals { get; private set; } public List currentIntervals { get; private set; } public override void Initialize() { base.Initialize(); fullBodyFuncAnimSm = new FunctionalAnimationSubmodule(this, "FullBodyAction"); registeredFunctions = new Dictionary>(); defaultAnimationGroup?.SetUp(this); disruptionStatus = new Dictionary() { { DisruptionType.NormalExternal, false }, { DisruptionType.NormalAction, false }, { DisruptionType.Movement, false } }; lastFrameIntervals = new List(); currentIntervals = new List(); RegisterDefaultFunctions(); } protected virtual void Update() { if (owner.statusSm.isDead) { return; } fullBodyFuncAnimSm?.UpdateTime(); UpdateIntervalInfo(); } protected virtual void LateUpdate() { fullBodyFuncAnimSm?.UpdateEvents(); BoneShakeLateUpdate(); } } public partial class AnimationSubcontrollerBase { protected virtual void UpdateIntervalInfo() { if (fullBodyFuncAnimSm?.currentRuntimeFuncAnim == null) { isDuringRootMotion = false; return; } lastFrameIntervals = currentIntervals; currentIntervals = fullBodyFuncAnimSm.currentRuntimeFuncAnim.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); isDuringRootMotion = currentIntervals.Any(interval => interval.intervalType == IntervalType.RootMotion); } } public partial class AnimationSubcontrollerBase { private class BoneShakeState { public Transform bone; public Vector3 shakeAxis; // 震动轴 // 物理变量 public float currentAngle = 0f; // 当前偏离角度 (位移 x) public float velocity = 0f; // 当前震动速度 (速度 v) } [Header("Bone Shake Settings")] // 刚度:越大越硬,回弹越快。对于重型机甲,建议 150-300;轻型无人机,建议 80-150。 public float stiffness = 200f; // 阻尼:越大停得越快。建议 10-20。如果太小,机器人会像果冻一样晃。 public float damping = 15f; // 冲击力倍率:将受击力度转化为弹簧的初始速度 public float impactForceMultiplier = 500f; public List testShakeBones; // 用于测试的骨骼列表 private List activeShakes = new List(); 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 // force = -stiffness * displacement - damping * velocity float force = -stiffness * state.currentAngle - damping * state.velocity; // a = F / m (假设质量为1,简化计算) // v += a * dt state.velocity += force * dt; // x += v * dt state.currentAngle += state.velocity * dt; // --- 应用旋转 --- // 将计算出的角度应用到轴向上 Quaternion shakeRot = Quaternion.AngleAxis(state.currentAngle, state.shakeAxis); state.bone.localRotation = state.bone.localRotation * shakeRot; // --- 移除条件 --- // 当能量非常小(速度和位移都接近0)时移除,节省性能 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; } } public partial class AnimationSubcontrollerBase { public virtual void RegisterDefaultFunctions() { registeredFunctions.Add("SetRootMotionMoveXMultiplier", anim => { owner.movementSc.rootMotionMoveXMultiplier = anim.runtimeVariables.GetVariable("RootMotionMoveXMultiplier"); }); registeredFunctions.Add("SetRootMotionMoveYMultiplier", anim => { owner.movementSc.rootMotionMoveYMultiplier = anim.runtimeVariables.GetVariable("SetRootMotionMoveYMultiplier"); }); registeredFunctions.Add("SetRootMotionMoveZMultiplier", anim => { owner.movementSc.rootMotionMoveZMultiplier = anim.runtimeVariables.GetVariable("SetRootMotionMoveZMultiplier"); }); } } public partial class AnimationSubcontrollerBase { public virtual bool SetGetHitDisruption(DisruptionType disruptionType, BreakthroughType breakthroughType) { if (owner.reactionSc.breakthroughResistances[breakthroughType]) { return false; } if (fullBodyFuncAnimSm.Stop(disruptionType)) { return true; } return false; } 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); } } protected virtual void PlayGetHitAnimation(string getHitAnimPrefix, out float animDuration, Vector3 direction = default) { int fullBodyActionIndex = animator.GetLayerIndex("FullBodyAction"); string getHitFrontAnim = getHitAnimPrefix + "Front"; string getHitAnim = getHitAnimPrefix + "Front"; float normalizedTransitionDuration = 0.1f / animator.GetCurrentAnimatorStateInfo(fullBodyActionIndex).length; animDuration = 0f; if (animator.HasState(fullBodyActionIndex, Animator.StringToHash(getHitAnim))) { direction.y = 0; direction = direction.normalized; float angle = Vector3.SignedAngle(transform.forward, direction, Vector3.up); string directionStr = angle switch { > -45f and <= 45f => "Back", > 45f and <= 135f => "Left", > -135f and <= -45f => "Right", _ => "Front" }; getHitAnim = getHitAnimPrefix + directionStr; if (direction == default || !animator.HasState(fullBodyActionIndex, Animator.StringToHash(getHitAnim))) { animator.CrossFade(getHitFrontAnim, normalizedTransitionDuration, fullBodyActionIndex, 0); } else { animator.CrossFade(getHitAnim, normalizedTransitionDuration, fullBodyActionIndex, 0); } animDuration = animator.GetCurrentAnimatorStateInfo(fullBodyActionIndex).length; } else if (animator.HasState(fullBodyActionIndex, Animator.StringToHash(getHitAnimPrefix))) { getHitAnim = getHitAnimPrefix; animator.CrossFade(getHitAnim, normalizedTransitionDuration, fullBodyActionIndex, 0); animDuration = animator.GetCurrentAnimatorStateInfo(fullBodyActionIndex).length; } else { if (animator.HasState(fullBodyActionIndex, Animator.StringToHash("GetHit"))) { animator.CrossFade("GetHit", normalizedTransitionDuration, fullBodyActionIndex, 0); animDuration = animator.GetCurrentAnimatorStateInfo(fullBodyActionIndex).length; } else { animator.CrossFade("Empty", 0.1f, fullBodyActionIndex, 0); } } } public virtual void PlayGetHitMediumAnimation(out float animDuration, Vector3 direction = default) { PlayGetHitAnimation("GetHitMedium", out animDuration, direction); } public virtual void PlayGetHitHeavyAnimation(out float animDuration, Vector3 direction = default) { PlayGetHitAnimation("GetHitHeavy", out animDuration, direction); } public virtual void PlayGetHitDisruptionAnimation(out float animDuration, Vector3 direction = default) { PlayGetHitAnimation("GetHitDisruption", out animDuration, direction); } } }