using UnityEngine; namespace AibisDream { public enum SubwayCarMotionPhase { Stopped, Cruise, Braking } public sealed class SubwayCarMotion : MonoBehaviour { [Header("Cruise Jolts")] [SerializeField] private float calmIntervalMin = 0.7f; [SerializeField] private float calmIntervalMax = 2.2f; [SerializeField] private float joltPeakMin = 0.6f; [SerializeField] private float joltPeakMax = 1.4f; [SerializeField] private float joltDurationMin = 0.18f; [SerializeField] private float joltDurationMax = 0.32f; [SerializeField] private Vector2Int joltBurstCount = new Vector2Int(1, 3); [SerializeField] private float joltBurstGapMin = 0.12f; [SerializeField] private float joltBurstGapMax = 0.28f; [SerializeField] private float calmNoiseAmplitude = 0.03f; [Header("Braking")] [SerializeField] private float brakePeakAccel = 1.6f; [SerializeField] private AnimationCurve brakeAccelCurve = new AnimationCurve( new Keyframe(0f, 0f, 0f, 4f), new Keyframe(0.12f, 1f), new Keyframe(0.5f, 0.65f, -0.5f, -0.5f), new Keyframe(1f, 0f, -1.5f, 0f)); [Header("Signal Filtering")] [SerializeField] private float accelSmoothTime = 0.025f; private float targetAccelX; private float accelVelocity; private float brakeDuration = 1f; private float brakeTimer; private float nextCruiseEventTime; private int burstRemaining; private float joltTimer; private float joltDuration = 0.2f; private float joltPeak = 1f; private float joltSign = 1f; private float noiseSeed; public float AccelX { get; private set; } public SubwayCarMotionPhase Phase { get; private set; } = SubwayCarMotionPhase.Cruise; private void Awake() { noiseSeed = Random.value * 1000f; ScheduleNextCruiseEvent(); } private void Update() { switch (Phase) { case SubwayCarMotionPhase.Cruise: UpdateCruise(); break; case SubwayCarMotionPhase.Braking: UpdateBraking(); break; default: targetAccelX = 0f; break; } AccelX = Mathf.SmoothDamp(AccelX, targetAccelX, ref accelVelocity, accelSmoothTime); } public void BeginBraking(float duration) { ClearCruiseJolt(); Phase = SubwayCarMotionPhase.Braking; brakeDuration = Mathf.Max(0.01f, duration); brakeTimer = 0f; } public void SetCruise() { ClearCruiseJolt(); Phase = SubwayCarMotionPhase.Cruise; ScheduleNextCruiseEvent(); } public void SetStopped() { ClearCruiseJolt(); Phase = SubwayCarMotionPhase.Stopped; targetAccelX = 0f; AccelX = 0f; accelVelocity = 0f; brakeTimer = 0f; } private void UpdateCruise() { if (joltTimer > 0f) { joltTimer -= Time.deltaTime; float normalizedTime = Mathf.Clamp01(1f - joltTimer / joltDuration); targetAccelX = EvaluateBiphasicPulse(normalizedTime) * joltPeak * joltSign; if (joltTimer <= 0f) { if (burstRemaining > 0) { nextCruiseEventTime = Time.time + Random.Range(joltBurstGapMin, joltBurstGapMax); } else { ScheduleNextCruiseEvent(); } } return; } float noise = (Mathf.PerlinNoise(noiseSeed, Time.time * 0.45f) - 0.5f) * 2f; targetAccelX = noise * calmNoiseAmplitude; if (Time.time >= nextCruiseEventTime) { StartJolt(); } } private void UpdateBraking() { brakeTimer += Time.deltaTime; float t = Mathf.Clamp01(brakeTimer / brakeDuration); targetAccelX = -brakePeakAccel * brakeAccelCurve.Evaluate(t); if (brakeTimer >= brakeDuration) { SetStopped(); } } private static float EvaluateBiphasicPulse(float t) { if (t < 0.5f) { return Mathf.Sin(t * Mathf.PI * 2f); } return -Mathf.Sin((t - 0.5f) * Mathf.PI * 2f); } private void StartJolt() { if (burstRemaining <= 0) { int maxExclusive = Mathf.Max(joltBurstCount.x + 1, joltBurstCount.y + 1); burstRemaining = Random.Range(joltBurstCount.x, maxExclusive); } burstRemaining--; joltTimer = Random.Range(joltDurationMin, joltDurationMax); joltDuration = Mathf.Max(0.01f, joltTimer); joltPeak = Random.Range(joltPeakMin, joltPeakMax); joltSign = Random.value < 0.5f ? -1f : 1f; } private void ScheduleNextCruiseEvent() { burstRemaining = 0; nextCruiseEventTime = Time.time + Random.Range(calmIntervalMin, calmIntervalMax); } private void ClearCruiseJolt() { burstRemaining = 0; joltTimer = 0f; targetAccelX = 0f; } } }