204 lines
8.8 KiB
C#
204 lines
8.8 KiB
C#
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
|
||
} |