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(); 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(); } 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; } } }