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
}