Files
aibis-dream/Assets/Scripts/SceneManagement/SubwayCarMotion.cs
T

185 lines
5.6 KiB
C#

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