You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
266 lines
9.8 KiB
266 lines
9.8 KiB
using System.Collections.Generic;
|
|
using Sirenix.OdinInspector;
|
|
using UnityEngine;
|
|
|
|
/// <summary>
|
|
/// Simulates a single lane of traffic by recycling a fixed pool of vehicle GameObjects.
|
|
/// Vehicles travel along local +Z, with organic lateral drift driven by Perlin noise.
|
|
///
|
|
/// Usage:
|
|
/// 1. Place this component on an empty GameObject aligned with the road lane.
|
|
/// 2. Assign vehicle prefabs, tune speed and drift.
|
|
/// 3. Duplicate and mirror-X for the opposite lane.
|
|
/// </summary>
|
|
public class TrafficLane : MonoBehaviour
|
|
{
|
|
// ── Route ─────────────────────────────────────────────────────────────────
|
|
|
|
[Header("Route")]
|
|
[Tooltip("Length of the lane in local +Z units. Vehicles are recycled at the far end.")]
|
|
public float routeLength = 80f;
|
|
|
|
[Tooltip("Local X position of the lane centre.")]
|
|
public float laneCenter = 0f;
|
|
|
|
// ── Vehicle Pool ──────────────────────────────────────────────────────────
|
|
|
|
[Header("Vehicle Pool")]
|
|
[Tooltip("Prefabs to randomly draw from when spawning. Can contain different vehicle types.")]
|
|
[ListDrawerSettings(ShowFoldout = false)]
|
|
public GameObject[] vehiclePrefabs;
|
|
|
|
[Tooltip("Total number of vehicle instances kept alive at once.")]
|
|
[Range(1, 60)]
|
|
public int poolSize = 12;
|
|
|
|
// ── Speed ─────────────────────────────────────────────────────────────────
|
|
|
|
[Header("Speed")]
|
|
[Tooltip("Average forward speed in units / second.")]
|
|
public float baseSpeed = 6f;
|
|
|
|
[Tooltip("Per-vehicle speed spread as a fraction of baseSpeed (±).")]
|
|
[Range(0f, 0.5f)]
|
|
public float speedVariance = 0.2f;
|
|
|
|
// ── Spawn ─────────────────────────────────────────────────────────────────
|
|
|
|
[Header("Spawn")]
|
|
[Tooltip("Average seconds between consecutive spawns.")]
|
|
public float spawnInterval = 2.5f;
|
|
|
|
[Tooltip("Randomisation of spawn interval as a fraction (±).")]
|
|
[Range(0f, 0.9f)]
|
|
public float spawnIntervalVariance = 0.4f;
|
|
|
|
[Tooltip("Minimum clearance required at Z=0 before the next vehicle may enter.")]
|
|
public float minSpawnGap = 4f;
|
|
|
|
// ── Lateral Drift ─────────────────────────────────────────────────────────
|
|
|
|
[Header("Lateral Drift")]
|
|
[Tooltip("Maximum left / right deviation from lane centre (units).")]
|
|
public float driftAmplitude = 0.25f;
|
|
|
|
[Tooltip("Spatial frequency of the drift wave — lower = longer, lazier curves.")]
|
|
[Range(0.01f, 1f)]
|
|
public float driftFrequency = 0.08f;
|
|
|
|
// ── Private state ─────────────────────────────────────────────────────────
|
|
|
|
private class Agent
|
|
{
|
|
public GameObject go;
|
|
public float progress; // distance travelled along Z this trip
|
|
public float speed; // units / second for this trip
|
|
public float noiseRow; // unique Y coordinate in Perlin space for this trip
|
|
public bool active;
|
|
}
|
|
|
|
private readonly List<Agent> pool = new();
|
|
private float spawnTimer;
|
|
private float nextSpawnDelay;
|
|
|
|
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
|
|
|
private void Start()
|
|
{
|
|
if (vehiclePrefabs == null || vehiclePrefabs.Length == 0)
|
|
{
|
|
Debug.LogWarning("[TrafficLane] No vehicle prefabs assigned.", this);
|
|
enabled = false;
|
|
return;
|
|
}
|
|
|
|
BuildPool();
|
|
ScheduleNextSpawn();
|
|
|
|
// Scatter a few vehicles along the lane so it doesn't look empty at start.
|
|
PrewarmLane();
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
TickAgents();
|
|
TickSpawn();
|
|
}
|
|
|
|
// ── Pool construction ─────────────────────────────────────────────────────
|
|
|
|
private void BuildPool()
|
|
{
|
|
for (var i = 0; i < poolSize; i++)
|
|
{
|
|
var prefab = vehiclePrefabs[Random.Range(0, vehiclePrefabs.Length)];
|
|
var go = Instantiate(prefab, transform);
|
|
go.SetActive(false);
|
|
pool.Add(new Agent { go = go, noiseRow = Random.Range(0f, 100f) });
|
|
}
|
|
}
|
|
|
|
private void PrewarmLane()
|
|
{
|
|
// Spread inactive vehicles evenly so the lane looks populated from frame 1.
|
|
var step = routeLength / Mathf.Max(poolSize * 0.5f, 1f);
|
|
var z = Random.Range(0f, step);
|
|
foreach (var agent in pool)
|
|
{
|
|
if (z >= routeLength)
|
|
{
|
|
break;
|
|
}
|
|
Activate(agent, z);
|
|
z += step * Random.Range(0.7f, 1.3f);
|
|
}
|
|
}
|
|
|
|
// ── Per-frame tick ────────────────────────────────────────────────────────
|
|
|
|
private void TickAgents()
|
|
{
|
|
foreach (var agent in pool)
|
|
{
|
|
if (!agent.active)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
agent.progress += agent.speed * Time.deltaTime;
|
|
|
|
if (agent.progress >= routeLength)
|
|
{
|
|
Deactivate(agent);
|
|
continue;
|
|
}
|
|
|
|
// Perlin noise: x-axis = position along route, y-axis = per-agent seed.
|
|
var noise = Mathf.PerlinNoise(agent.progress * driftFrequency, agent.noiseRow);
|
|
var lateral = laneCenter + (noise * 2f - 1f) * driftAmplitude;
|
|
|
|
var prev = agent.go.transform.localPosition;
|
|
var next = new Vector3(lateral, 0f, agent.progress);
|
|
agent.go.transform.localPosition = next;
|
|
|
|
// Face direction of travel, ignoring tiny sub-frame deltas.
|
|
var delta = next - prev;
|
|
if (delta.sqrMagnitude > 1e-6f)
|
|
{
|
|
agent.go.transform.localRotation = Quaternion.LookRotation(delta, Vector3.up);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void TickSpawn()
|
|
{
|
|
spawnTimer += Time.deltaTime;
|
|
if (spawnTimer < nextSpawnDelay)
|
|
{
|
|
return;
|
|
}
|
|
|
|
spawnTimer = 0f;
|
|
ScheduleNextSpawn();
|
|
|
|
// Block spawn if another vehicle is still near the entry point.
|
|
foreach (var agent in pool)
|
|
{
|
|
if (agent.active && agent.progress < minSpawnGap)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
SpawnNext(0f);
|
|
}
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
|
|
private void Activate(Agent agent, float startProgress)
|
|
{
|
|
agent.progress = startProgress;
|
|
agent.speed = baseSpeed * (1f + Random.Range(-speedVariance, speedVariance));
|
|
agent.noiseRow = Random.Range(0f, 100f);
|
|
agent.go.SetActive(true);
|
|
agent.active = true;
|
|
|
|
// Teleport silently to start position before enabling.
|
|
var noise = Mathf.PerlinNoise(startProgress * driftFrequency, agent.noiseRow);
|
|
var lateral = laneCenter + (noise * 2f - 1f) * driftAmplitude;
|
|
agent.go.transform.localPosition = new Vector3(lateral, 0f, startProgress);
|
|
}
|
|
|
|
private void SpawnNext(float startProgress)
|
|
{
|
|
foreach (var agent in pool)
|
|
{
|
|
if (!agent.active)
|
|
{
|
|
Activate(agent, startProgress);
|
|
return;
|
|
}
|
|
}
|
|
// Pool exhausted — skip this cycle.
|
|
}
|
|
|
|
private static void Deactivate(Agent agent)
|
|
{
|
|
agent.go.SetActive(false);
|
|
agent.active = false;
|
|
}
|
|
|
|
private void ScheduleNextSpawn()
|
|
{
|
|
var variance = spawnInterval * spawnIntervalVariance;
|
|
nextSpawnDelay = Mathf.Max(0.1f, spawnInterval + Random.Range(-variance, variance));
|
|
}
|
|
|
|
// ── Editor visualisation ──────────────────────────────────────────────────
|
|
|
|
#if UNITY_EDITOR
|
|
private void OnDrawGizmosSelected()
|
|
{
|
|
var start = transform.TransformPoint(new Vector3(laneCenter, 0f, 0f));
|
|
var end = transform.TransformPoint(new Vector3(laneCenter, 0f, routeLength));
|
|
|
|
Gizmos.color = Color.yellow;
|
|
Gizmos.DrawLine(start, end);
|
|
|
|
// Drift envelope.
|
|
Gizmos.color = new Color(1f, 1f, 0f, 0.25f);
|
|
var left = transform.TransformPoint(new Vector3(laneCenter - driftAmplitude, 0f, 0f));
|
|
var right = transform.TransformPoint(new Vector3(laneCenter + driftAmplitude, 0f, 0f));
|
|
var leftEnd = transform.TransformPoint(new Vector3(laneCenter - driftAmplitude, 0f, routeLength));
|
|
var rightEnd = transform.TransformPoint(new Vector3(laneCenter + driftAmplitude, 0f, routeLength));
|
|
Gizmos.DrawLine(left, leftEnd);
|
|
Gizmos.DrawLine(right, rightEnd);
|
|
Gizmos.DrawLine(left, right);
|
|
Gizmos.DrawLine(leftEnd, rightEnd);
|
|
|
|
// Entry / exit markers.
|
|
Gizmos.color = Color.green;
|
|
Gizmos.DrawSphere(start, 0.3f);
|
|
Gizmos.color = Color.red;
|
|
Gizmos.DrawSphere(end, 0.3f);
|
|
}
|
|
#endif
|
|
}
|
|
|