Skip to content

Instantly share code, notes, and snippets.

@kyuskoj
Created October 25, 2021 10:20
Show Gist options
  • Save kyuskoj/43095fcf1b417dd2153271cedaa4ed88 to your computer and use it in GitHub Desktop.
Save kyuskoj/43095fcf1b417dd2153271cedaa4ed88 to your computer and use it in GitHub Desktop.
Hair simulation with Unity 2D Animation
using System;
using System.Collections.Generic;
using Sirenix.OdinInspector;
using UnityEngine;
using UnityEngine.U2D;
namespace Son.Modules
{
public class Hair : MonoBehaviour
{
[SerializeField] private Transform origin;
[SerializeField] private SpriteRenderer _renderer;
[SerializeField, Range(0f, 10f)] private float gravity;
[SerializeField, Range(0f, 1f)] private float gravityFallout;
[SerializeField] private Vector2 damp;
[SerializeField, Range(0f, 1f)] private float updateInterval = .125f;
[SerializeField, FoldoutGroup("Data", order: 1)] private List<Transform> bones = new List<Transform>();
[SerializeField, FoldoutGroup("Data")] private List<Point> points = new List<Point>(8);
private float lastUpdate;
private bool flipped;
private bool onFlip;
private bool lastFlipped;
private void Start()
{
foreach (var point in points)
point.Position = origin.TransformPoint(point.StartOffset);
}
private void Update()
{
flipped = origin.forward.z < 0f;
onFlip = flipped != lastFlipped;
lastFlipped = flipped;
if (updateInterval < Time.time - lastUpdate || onFlip)
{
Simulate(onFlip ? 1f : Time.time - lastUpdate);
lastUpdate = Time.time;
}
}
[Button(size: ButtonSizes.Medium)]
public void Initialized()
{
bones.Clear();
points.Clear();
var spriteBones = _renderer.sprite.GetBones();
var spriteBoneByName = new Dictionary<string, SpriteBone>();
foreach (var bone in spriteBones)
spriteBoneByName[bone.name] = bone;
foreach (var child in origin.GetChildRecursive())
{
if (spriteBoneByName.ContainsKey(child.name))
bones.Add(child);
}
for (int i = 0; i < bones.Count; i++)
{
var spriteBone = spriteBoneByName[bones[i].name];
var startOffset = i < bones.Count - 1 ? bones[i + 1].position : bones[i].position + Vector3.down * spriteBone.length;
points.Add(new Point() { StartOffset = origin.InverseTransformPoint(startOffset), Gap = spriteBone.length });
}
}
private void Simulate(float interval)
{
Vector2 parentPos = origin.position;
for (int i = 0; i < points.Count; i++)
{
var point = points[i];
var gravity = this.gravity * (1f - (float)i / points.Count * gravityFallout); // 꼬리 쪽으로 갈수록 더 펄럭거리도록 조정.
var newPos = point.Position + point.Velocity * damp + gravity * interval * Vector2.down;
var clampedOffset = Vector2.ClampMagnitude(newPos - parentPos, point.Gap);
var lastPos = point.Position;
point.Position = parentPos + clampedOffset;
point.Velocity = point.Position - lastPos;
if (onFlip) // 좌우반전 되었을 때, 너무 펄럭이지 않도록 조정.
point.Velocity = Vector2.ClampMagnitude(point.Velocity, .1f);
parentPos = point.Position;
}
for (int i = 0; i < points.Count; i++)
{
Vector2 direction = points[i].Position - bones[i].position.V2();
float angle = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg;
bones[i].rotation = flipped ? Quaternion.Euler(180f, 0f, -angle) : Quaternion.Euler(0f, 0f, angle);
}
}
[Serializable]
public class Point
{
public Vector2 Position { get; set; }
[field: SerializeField] public Vector2 StartOffset { get; set; }
[field: SerializeField] public float Gap { get; set; }
public Vector2 Velocity { get; set; }
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment