using System.Collections.Generic; using Sirenix.OdinInspector; using UnityEngine; /// /// Simulates a single lane of traffic by recycling a fixed pool of vehicle GameObjects. /// Cars decelerate to maintain a safe following gap; motorcycles overtake by shifting sideways. /// Vehicles travel along local +Z with organic Perlin-noise lateral drift. /// /// Usage: /// 1. Place this component on an empty GameObject aligned with the road lane. /// 2. Assign car / motorcycle prefabs, tune speed and drift. /// 3. Duplicate and mirror-X (or rotate 180°) 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("Cars")] [Tooltip("Car prefabs — randomly chosen at spawn.")] [ListDrawerSettings(ShowFoldout = false)] public GameObject[] vehiclePrefabs; [Header("Motorcycles")] [Tooltip("Motorcycle prefabs — randomly chosen at spawn. Leave empty to disable motorcycles.")] [ListDrawerSettings(ShowFoldout = false)] public GameObject[] motorcyclePrefabs; [Tooltip("Fraction of pool slots assigned to motorcycles.")] [Range(0f, 1f)] public float motorcycleRatio = 0.2f; [Tooltip("Motorcycles travel this many times faster than baseSpeed on average.")] [Range(1f, 2.5f)] public float motorcycleSpeedMultiplier = 1.3f; [Tooltip("How far sideways (units) a motorcycle shifts when overtaking.")] public float overtakeWidth = 0.5f; [Header("Pool")] [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 randomisation as a fraction of baseSpeed (±).")] [Range(0f, 0.5f)] public float speedVariance = 0.3f; // ── Following ───────────────────────────────────────────────────────────── [Header("Following")] [Tooltip("Distance at which a vehicle starts reacting to the vehicle ahead.")] public float followDistance = 8f; [Tooltip("Minimum comfortable gap. At this distance the follower matches the leader's speed exactly.")] public float safeGap = 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; [Tooltip("How quickly vehicles adjust their lateral position (affects drift smoothness and overtake transitions).")] [Range(0.5f, 10f)] public float lateralSmoothSpeed = 2f; // ── Internals ───────────────────────────────────────────────────────────── private enum VehicleType { Car, Motorcycle } private class Agent { public GameObject go; public VehicleType type; public float progress; // distance travelled along Z this trip public float targetSpeed; // natural cruising speed assigned at spawn public float currentSpeed; // actual speed this frame (may be reduced by following) public float noiseRow; // unique Y coordinate in Perlin space public float currentLateral; // smoothed lateral X position public bool overtaking; // motorcycle is currently overtaking public float overtakeLateral; // lateral target while overtaking public bool active; } private readonly List pool = new(); private float spawnTimer; private float nextSpawnDelay; // ── Lifecycle ───────────────────────────────────────────────────────────── private void Start() { if (!HasAnyPrefab()) { Debug.LogWarning("[TrafficLane] No vehicle prefabs assigned.", this); enabled = false; return; } BuildPool(); ScheduleNextSpawn(); PrewarmLane(); } private void Update() { TickAgents(); TickSpawn(); } // ── Pool construction ───────────────────────────────────────────────────── private void BuildPool() { for (var i = 0; i < poolSize; i++) { var isMotorcycle = HasMotorcyclePrefabs() && Random.value < motorcycleRatio; var prefabs = PickPrefabArray(isMotorcycle); if (prefabs == null || prefabs.Length == 0) { continue; } var go = Instantiate(prefabs[Random.Range(0, prefabs.Length)], transform); go.SetActive(false); pool.Add(new Agent { go = go, type = isMotorcycle ? VehicleType.Motorcycle : VehicleType.Car, noiseRow = Random.Range(0f, 100f), currentLateral = laneCenter }); } } private void PrewarmLane() { 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; } FindLeader(agent, out var leaderGap, out var leaderSpeed, out var leaderLateral); var hasSlowerLeader = leaderGap < followDistance && leaderSpeed < agent.targetSpeed * 0.98f; // ── Longitudinal speed ──────────────────────────────────────────── float effectiveSpeed; if (agent.type == VehicleType.Motorcycle && hasSlowerLeader) { // Motorcycle keeps speed and moves sideways to overtake. effectiveSpeed = agent.targetSpeed; agent.overtaking = true; var side = leaderLateral >= laneCenter ? -1f : 1f; agent.overtakeLateral = laneCenter + side * overtakeWidth; } else { // Car (or motorcycle with no slow leader ahead): follow and decelerate. if (leaderGap < followDistance) { var t = Mathf.Clamp01( (leaderGap - safeGap) / Mathf.Max(followDistance - safeGap, 0.01f)); effectiveSpeed = Mathf.Lerp(leaderSpeed, agent.targetSpeed, t); } else { effectiveSpeed = agent.targetSpeed; } if (agent.overtaking && leaderGap >= followDistance) { agent.overtaking = false; } } agent.currentSpeed = Mathf.Max(effectiveSpeed, 0f); agent.progress += agent.currentSpeed * Time.deltaTime; if (agent.progress >= routeLength) { Deactivate(agent); continue; } // ── Lateral position ────────────────────────────────────────────── float targetLateral; if (agent.overtaking) { targetLateral = agent.overtakeLateral; } else { var noise = Mathf.PerlinNoise(agent.progress * driftFrequency, agent.noiseRow); targetLateral = laneCenter + (noise * 2f - 1f) * driftAmplitude; } agent.currentLateral = Mathf.Lerp( agent.currentLateral, targetLateral, Time.deltaTime * lateralSmoothSpeed); // ── Apply transform ─────────────────────────────────────────────── var prev = agent.go.transform.localPosition; var next = new Vector3(agent.currentLateral, 0f, agent.progress); agent.go.transform.localPosition = next; 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(); foreach (var agent in pool) { if (agent.active && agent.progress < minSpawnGap) { return; } } SpawnNext(0f); } // ── Helpers ─────────────────────────────────────────────────────────────── private void FindLeader(Agent self, out float gap, out float speed, out float lateral) { gap = float.MaxValue; speed = baseSpeed; lateral = laneCenter; foreach (var other in pool) { if (!other.active || other == self) { continue; } var g = other.progress - self.progress; if (g > 0f && g < gap) { gap = g; speed = other.currentSpeed; lateral = other.currentLateral; } } } private void Activate(Agent agent, float startZ) { var speedMult = agent.type == VehicleType.Motorcycle ? motorcycleSpeedMultiplier : 1f; agent.progress = startZ; agent.targetSpeed = baseSpeed * speedMult * (1f + Random.Range(-speedVariance, speedVariance)); agent.currentSpeed = agent.targetSpeed; agent.noiseRow = Random.Range(0f, 100f); agent.overtaking = false; var noise = Mathf.PerlinNoise(startZ * driftFrequency, agent.noiseRow); agent.currentLateral = laneCenter + (noise * 2f - 1f) * driftAmplitude; agent.go.transform.localPosition = new Vector3(agent.currentLateral, 0f, startZ); agent.go.SetActive(true); agent.active = true; } private void SpawnNext(float startZ) { foreach (var agent in pool) { if (!agent.active) { Activate(agent, startZ); 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)); } private bool HasAnyPrefab() => (vehiclePrefabs != null && vehiclePrefabs.Length > 0) || (motorcyclePrefabs != null && motorcyclePrefabs.Length > 0); private bool HasMotorcyclePrefabs() => motorcyclePrefabs != null && motorcyclePrefabs.Length > 0; private GameObject[] PickPrefabArray(bool motorcycle) { if (motorcycle && HasMotorcyclePrefabs()) { return motorcyclePrefabs; } if (vehiclePrefabs != null && vehiclePrefabs.Length > 0) { return vehiclePrefabs; } return motorcyclePrefabs; } // ── 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 ll = transform.TransformPoint(new Vector3(laneCenter - driftAmplitude, 0f, 0f)); var rl = transform.TransformPoint(new Vector3(laneCenter + driftAmplitude, 0f, 0f)); var lle = transform.TransformPoint(new Vector3(laneCenter - driftAmplitude, 0f, routeLength)); var rle = transform.TransformPoint(new Vector3(laneCenter + driftAmplitude, 0f, routeLength)); Gizmos.DrawLine(ll, lle); Gizmos.DrawLine(rl, rle); Gizmos.DrawLine(ll, rl); Gizmos.DrawLine(lle, rle); // Overtake envelope (shown only when motorcycles are configured) if (HasMotorcyclePrefabs() && motorcycleRatio > 0f) { Gizmos.color = new Color(0.4f, 0.8f, 1f, 0.15f); var ml = transform.TransformPoint(new Vector3(laneCenter - overtakeWidth, 0f, 0f)); var mr = transform.TransformPoint(new Vector3(laneCenter + overtakeWidth, 0f, 0f)); var mle = transform.TransformPoint(new Vector3(laneCenter - overtakeWidth, 0f, routeLength)); var mre = transform.TransformPoint(new Vector3(laneCenter + overtakeWidth, 0f, routeLength)); Gizmos.DrawLine(ml, mle); Gizmos.DrawLine(mr, mre); } Gizmos.color = Color.green; Gizmos.DrawSphere(start, 0.3f); Gizmos.color = Color.red; Gizmos.DrawSphere(end, 0.3f); } #endif }