Files
Cielonos/Assets/Scripts/MainGame/Characters/Base/Subcontrollers/Animation/BoneShakeController.cs
SoulliesOfficial 9a9e48f8a5
2026-06-27 12:52:03 -04:00

204 lines
8.8 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 System.Collections.Generic;
using Sirenix.OdinInspector;
using UnityEngine;
namespace Cielonos.MainGame.Characters
{
public partial class AnimationSubcontrollerBase
{
// ============================================================================
// BoneShakeController
// ============================================================================
/// <summary>
/// 创伤驱动的高频噪声骨骼微震动控制器。
/// 抛弃传统的弹簧物理积分(易爆炸、幅度大、频率低),
/// 改用 Trauma 指数衰减 + 高频正弦波在三轴上叠加微小偏转,
/// 实现类似《绝区零》的干脆、高频、微幅受击抽搐效果。
/// </summary>
[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<Transform> shakableBones = new List<Transform>();
[Tooltip("弱震动骨骼列表(如 Hips以防止滑步太严重。")] public List<Transform> weakShakableBones = new List<Transform>();
[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;
/// <summary>各轴正弦采样的时间种子,确保多骨骼不同步,避免整体共振。</summary>
public float seedX, seedY, seedZ;
}
private List<BoneShakeState> _activeShakes = new List<BoneShakeState>();
// ── 公开 API ─────────────────────────────────────────────────────────────
/// <summary>对 shakableBones 中所有骨骼施加一次受击创伤冲击。</summary>
/// <param name="intensity">受击强度0~1。</param>
/// <param name="direction">受击方向(世界空间,保留供未来按方向差异化各骨骼幅度)。</param>
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);
}
}
/// <summary>对单根骨骼施加创伤冲击,可用于精确控制特定部位。</summary>
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);
}
/// <summary>必须在 LateUpdate 中调用Animator 写完骨骼后再叠加偏转)。</summary>
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 = Time.time * 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<Animator>();
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
}