Skip to content

Instantly share code, notes, and snippets.

@SolarianZ
Last active November 20, 2023 13:39
Show Gist options
  • Save SolarianZ/84068f7642262a52b0bce8e56a5509b3 to your computer and use it in GitHub Desktop.
Save SolarianZ/84068f7642262a52b0bce8e56a5509b3 to your computer and use it in GitHub Desktop.
{"category": "Unity/Runtime/Utility/Animation", "keywords": "Animation, Hit feedback, Bone Vibrate, Simple Harmonic Motion, SHM, Second Order System"} A simple single-skeleton vibrator for simple hit feedback.
// NOTE: The 'SimpleHarmonicMotion.cs' is here: https://gist.github.com/SolarianZ/78f9b22d9663d77b6e6c1b0c60cc6322
// You need to change `System.Numerics.Vector3` to `UnityEngine.Vector3` in SimpleHarmonicMotion.cs
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
[DisallowMultipleComponent]
public class BoneVibrator : MonoBehaviour
{
public enum Axis { None = 0, X, Y, Z, }
[Tooltip("The rotation axis in local space of the bone. " +
"After applying force, the bone will rotate clockwise around this axis.")]
public Axis localRotationAxis = Axis.None;
[Range(0, 10)]
public float frequency = 3.75f;
[Range(0, 2)]
public float damping = 0.35f;
[Range(-180, 0)]
public float minAngle = -10f;
[Range(0, 180)]
public float maxAngle = 10f;
public float forceToAngle = 1f;
public bool invertForce;
private SimpleHarmonicMotion _shm;
public void ApplyForce(float force)
{
if (!enabled)
{
return;
}
if (invertForce)
{
force = -force;
}
var angle = Mathf.Clamp(force * forceToAngle, minAngle, maxAngle);
Vector3 rotation;
switch (localRotationAxis)
{
case Axis.None:
Debug.LogError($"The rotation axis cannot be '{Axis.None}'.", this);
return;
case Axis.X:
rotation = new Vector3(angle, 0, 0);
break;
case Axis.Y:
rotation = new Vector3(0, angle, 0);
break;
case Axis.Z:
rotation = new Vector3(0, 0, angle);
break;
default:
throw new System.Exception($"Unknown rotation axis: {localRotationAxis}.");
}
_shm.Position = rotation;
}
private void Start()
{
if (localRotationAxis == Axis.None)
{
Debug.LogError($"The rotation axis cannot be '{Axis.None}'.", this);
}
_shm = new SimpleHarmonicMotion(frequency, damping, Vector3.zero);
}
// NOTE: This may conflict with IK, so adjust the execution order of script as needed
private void LateUpdate()
{
_shm.Frequency = frequency;
_shm.Damping = damping;
var rotation = _shm.Tick(Time.deltaTime, Vector3.zero);
// NOTE: This assumes that the animation has updated the skeleton pose at this point
// For non-animated objects, toggle the comments below
// For animated object:
transform.Rotate(rotation, Space.Self);
// For non-animated object(Can be moved to the Update method):
//transform.localEulerAngles = rotation;
}
#if UNITY_EDITOR
[CustomEditor(typeof(BoneVibrator))]
class Editor : UnityEditor.Editor
{
private const float GUI_AXIS_H = 10f;
private const float GUI_AXIS_V = 1f;
private BoneVibrator _target => (BoneVibrator)target;
private SimpleHarmonicMotion _debugShm;
private float _debugForce = 10;
private void OnEnable()
{
_debugShm = new SimpleHarmonicMotion(_target.frequency, _target.damping, default);
}
public override void OnInspectorGUI()
{
if (_target.localRotationAxis == Axis.None)
{
EditorGUILayout.HelpBox($"The rotation axis cannot be '{Axis.None}'.", MessageType.Error);
}
base.OnInspectorGUI();
if (!Application.isPlaying)
{
return;
}
EditorGUILayout.Space();
_debugForce = EditorGUILayout.FloatField("Debug Force", _debugForce);
if (GUILayout.Button("[Debug] Apply Force"))
{
_target.ApplyForce(_debugForce);
}
}
public override bool HasPreviewGUI()
{
return true;
}
public override GUIContent GetPreviewTitle()
{
var title = base.GetPreviewTitle();
title.text = ObjectNames.NicifyVariableName(nameof(BoneVibrator)) + " Preview";
return title;
}
public override void OnPreviewGUI(Rect r, GUIStyle background)
{
base.OnPreviewGUI(r, background);
DrawShmGraph(r);
}
private void DrawShmGraph(Rect area, float padding = 4f)
{
// Padding
area.x += padding;
area.y += padding;
area.width -= padding * 2;
area.height -= padding * 2;
// Axes
Handles.color = Color.green;
Handles.DrawLine(area.position, area.position + new Vector2(0, area.height));
Handles.DrawLine(area.position + new Vector2(0, area.height), area.position + new Vector2(area.width, area.height));
// Points
const short STEPS = 2000;
var startPos = Vector3.zero;
var targetPos = startPos + Vector3.up * GUI_AXIS_V + Vector3.right * GUI_AXIS_H;
var lastPoint = WorldToShmGraphSpace(area, startPos);
_debugShm.Frequency = _target.frequency;
_debugShm.Damping = _target.damping;
_debugShm.ResetPosition(startPos);
var interval = GUI_AXIS_H / STEPS;
for (short i = 0; i < STEPS; i++)
{
var linePos = _debugShm.Tick(interval, targetPos);
var x = interval * i * GUI_AXIS_H;
var y = (linePos - startPos).magnitude / (targetPos - startPos).magnitude;
var point = WorldToShmGraphSpace(area, new Vector3(x, y, 0));
Handles.DrawLine(lastPoint, point);
lastPoint = point;
}
}
private Vector3 WorldToShmGraphSpace(Rect graphArea, Vector3 point)
{
var ratioX = point.x / GUI_AXIS_H;
var ratioY = point.y / GUI_AXIS_V;
var posX = graphArea.position.x + graphArea.width * ratioX;
var posY = graphArea.position.y + graphArea.height - graphArea.height * ratioY / 2f;
var graphPoint = new Vector3(posX, posY, 0);
return graphPoint;
}
}
#endif
}
@SolarianZ
Copy link
Author

bone_vibrator

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment