Files

129 lines
4.2 KiB
C#

using UnityEngine;
namespace AibisDream
{
[RequireComponent(typeof(SpriteRenderer))]
public sealed class SubwayHandStrap : MonoBehaviour
{
[SerializeField] private SubwayCarMotion carMotion;
[SerializeField] private float length = 0.42f;
[SerializeField] private float gravity = 9.81f;
[SerializeField] private float dampingRatio = 0.16f;
[SerializeField] private float maxAngle = 25f;
[SerializeField] private float maxAngularSpeed = 180f;
[SerializeField] private float maxIntegrationStep = 1f / 120f;
[Header("Instance Variation")]
[SerializeField] private bool randomizeOnAwake = true;
[SerializeField] private float lengthVariation = 0.03f;
[SerializeField] private Vector2 dampingRatioRange = new Vector2(0.14f, 0.18f);
[SerializeField] private float initialAngleRange = 0.5f;
[Header("Pivot")]
[SerializeField] private Vector3 hangPointLocalOffset;
private SpriteRenderer spriteRenderer;
private Vector3 restLocalPosition;
private float angle;
private float angularVelocity;
private float baseLength;
public float Angle => angle;
public float Length => length;
private void Awake()
{
spriteRenderer = GetComponent<SpriteRenderer>();
restLocalPosition = transform.localPosition;
baseLength = length;
if (hangPointLocalOffset == Vector3.zero && spriteRenderer.sprite != null)
{
hangPointLocalOffset = new Vector3(0f, spriteRenderer.sprite.bounds.max.y, 0f);
}
if (carMotion == null)
{
carMotion = GetComponentInParent<SubwayCarMotion>();
}
if (randomizeOnAwake)
{
RandomizeInstance();
}
ApplyPose();
}
private void Update()
{
float remainingTime = Time.deltaTime;
while (remainingTime > 0f)
{
float step = Mathf.Min(remainingTime, maxIntegrationStep);
Simulate(step);
remainingTime -= step;
}
ApplyPose();
}
private void RandomizeInstance()
{
Random.State previousState = Random.state;
Random.InitState(GetInstanceID());
length = baseLength * Random.Range(1f - lengthVariation, 1f + lengthVariation);
dampingRatio = Random.Range(dampingRatioRange.x, dampingRatioRange.y);
angle = Random.Range(-initialAngleRange, initialAngleRange);
angularVelocity = 0f;
Random.state = previousState;
}
private void Simulate(float deltaTime)
{
float safeLength = Mathf.Max(0.01f, length);
float angleRadians = angle * Mathf.Deg2Rad;
float angularVelocityRadians = angularVelocity * Mathf.Deg2Rad;
float naturalFrequency = Mathf.Sqrt(gravity / safeLength);
float accelX = carMotion != null ? carMotion.AccelX : 0f;
float angularAcceleration =
-(gravity / safeLength) * Mathf.Sin(angleRadians)
- (accelX / safeLength) * Mathf.Cos(angleRadians)
- 2f * dampingRatio * naturalFrequency * angularVelocityRadians;
angularVelocityRadians += angularAcceleration * deltaTime;
angularVelocity = Mathf.Clamp(angularVelocityRadians * Mathf.Rad2Deg, -maxAngularSpeed, maxAngularSpeed);
angle += angularVelocity * deltaTime;
ClampAngleLimit();
}
private void ClampAngleLimit()
{
if (Mathf.Abs(angle) <= maxAngle)
{
return;
}
float limitSign = Mathf.Sign(angle);
angle = limitSign * maxAngle;
if (Mathf.Sign(angularVelocity) == limitSign)
{
angularVelocity = 0f;
}
}
private void ApplyPose()
{
Quaternion rotation = Quaternion.Euler(0f, 0f, angle);
transform.localRotation = rotation;
transform.localPosition = restLocalPosition + hangPointLocalOffset - rotation * hangPointLocalOffset;
}
}
}