using System.Collections.Generic; using Sirenix.OdinInspector; using UnityEngine; /// /// 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. /// 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 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 }