using System.Collections.Generic; using Sirenix.OdinInspector; using UnityEngine; namespace Cielonos.MainGame.Characters { public partial class AnimationSubcontrollerBase { // ============================================================================ // BoneShakeController // ============================================================================ /// /// 创伤驱动的高频噪声骨骼微震动控制器。 /// 抛弃传统的弹簧物理积分(易爆炸、幅度大、频率低), /// 改用 Trauma 指数衰减 + 高频正弦波在三轴上叠加微小偏转, /// 实现类似《绝区零》的干脆、高频、微幅受击抽搐效果。 /// [System.Serializable] public class BoneShakeController { // ── 参数 ──────────────────────────────────────────────────────────────── [Title("Trauma Shake Settings", "高频微震动参数")] [Tooltip("单次受击注入的基础创伤值(0~1)。受击强度(intensity)会与此值相乘。")] [Range(0f, 1f)] public float traumaPerHit = 0.8f; [Tooltip("创伤值每秒的线性衰减速度。数值越大,抖动停止得越干脆。")] public float traumaDecay = 2.5f; [Tooltip("单根骨骼的最大偏转角度(度)。控制'微震动'的幅度,推荐 2~6 度。")] public float maxShakeAngle = 3f; [Tooltip("正弦波采样频率系数。越大则单位时间内振荡次数越多(推荐 40~80)。")] public float shakeFrequency = 40f; [Tooltip("参与骨骼震动的骨骼列表。可通过 AnimationSubcontrollerBase 上的按钮一键填充 Humanoid 常用骨骼。")] public List shakableBones = new List(); [Tooltip("弱震动骨骼列表(如 Hips,以防止滑步太严重)。")] public List weakShakableBones = new List(); [Tooltip("弱震动骨骼的幅度乘数(0~1)。推荐 0.5。")] [Range(0f, 1f)] public float weakShakeMultiplier = 0.5f; // ── 内部状态 ───────────────────────────────────────────────────────────── private class BoneShakeState { public Transform bone; public float trauma; public bool isWeak; /// 各轴正弦采样的时间种子,确保多骨骼不同步,避免整体共振。 public float seedX, seedY, seedZ; } private List _activeShakes = new List(); // ── 公开 API ───────────────────────────────────────────────────────────── /// 对 shakableBones 中所有骨骼施加一次受击创伤冲击。 /// 受击强度(0~1)。 /// 受击方向(世界空间,保留供未来按方向差异化各骨骼幅度)。 public void ApplyShake(float intensity, Vector3 direction = default) { foreach (var bone in shakableBones) { if (bone == null) continue; ApplyShakeToBone(bone, intensity, false); } foreach (var bone in weakShakableBones) { if (bone == null) continue; ApplyShakeToBone(bone, intensity, true); } } /// 对单根骨骼施加创伤冲击,可用于精确控制特定部位。 public void ApplyShakeToBone(Transform bone, float intensity, bool isWeak = false) { if (bone == null) return; var state = _activeShakes.Find(s => s.bone == bone); if (state == null) { state = new BoneShakeState { bone = bone, isWeak = isWeak, seedX = bone.GetInstanceID() * 0.001f, seedY = bone.GetInstanceID() * 0.002f + 13.7f, seedZ = bone.GetInstanceID() * 0.003f + 27.3f, }; _activeShakes.Add(state); } else { state.isWeak = isWeak; // Update in case it changed } state.trauma = Mathf.Min(1f, state.trauma + traumaPerHit * intensity); } /// 必须在 LateUpdate 中调用(Animator 写完骨骼后再叠加偏转)。 public void LateUpdate(float deltaTime) { for (int i = _activeShakes.Count - 1; i >= 0; i--) { var state = _activeShakes[i]; if (state.bone == null) { _activeShakes.RemoveAt(i); continue; } // ① 线性衰减 state.trauma = Mathf.Max(0f, state.trauma - traumaDecay * deltaTime); if (state.trauma < 0.005f) { _activeShakes.RemoveAt(i); continue; } // ③ 三次方幂运算,让震动前期剧烈、后期极速收敛 float p = state.trauma * state.trauma * state.trauma; // 计算最终幅度 float currentMaxAngle = state.isWeak ? maxShakeAngle * weakShakeMultiplier : maxShakeAngle; // ③ 高频正弦波,三轴不同频率 + 骨骼唯一种子 float t = deltaTime * shakeFrequency; float aX = currentMaxAngle * p * Mathf.Sin(t + state.seedX); float aY = currentMaxAngle * p * Mathf.Sin(t * 1.3f + state.seedY); float aZ = currentMaxAngle * p * Mathf.Sin(t * 0.7f + state.seedZ); // ④ 叠加到骨骼本地旋转 state.bone.localRotation *= Quaternion.Euler(aX, aY, aZ); } } } } #if UNITY_EDITOR public partial class AnimationSubcontrollerBase { [TitleGroup("Bone Shake Settings")] [Button("Test Bone Shake", ButtonSizes.Medium)] private void TestShake(float force) { if (boneShake.shakableBones.Count == 0) { Debug.LogWarning("No bones to shake. Please populate shakableBones first."); return; } boneShake.ApplyShake(force); } [TitleGroup("Bone Shake Settings")] [Button("Auto Populate Humanoid Shake Bones", ButtonSizes.Medium)] [HideInPlayMode] private void AutoPopulateHumanoidShakeBones() { if (animator == null) animator = GetComponentInChildren(); if (animator == null || !animator.isHuman) { Debug.LogWarning("Cannot auto-populate: Animator is either missing or not set to Humanoid rig."); return; } boneShake.shakableBones.Clear(); boneShake.weakShakableBones.Clear(); HumanBodyBones[] normalBones = { HumanBodyBones.Spine, HumanBodyBones.Chest, HumanBodyBones.UpperChest, HumanBodyBones.Neck, HumanBodyBones.Head, HumanBodyBones.LeftShoulder, HumanBodyBones.RightShoulder, HumanBodyBones.LeftUpperArm, HumanBodyBones.RightUpperArm }; foreach (var b in normalBones) { Transform t = animator.GetBoneTransform(b); if (t != null && !boneShake.shakableBones.Contains(t)) boneShake.shakableBones.Add(t); } Transform hips = animator.GetBoneTransform(HumanBodyBones.Hips); if (hips != null && !boneShake.weakShakableBones.Contains(hips)) { boneShake.weakShakableBones.Add(hips); } UnityEditor.EditorUtility.SetDirty(this); Debug.Log( $"Auto-populated {boneShake.shakableBones.Count} normal bones and {boneShake.weakShakableBones.Count} weak bones for hit shaking."); } } #endif }