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
}