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.
 
 
 

264 lines
9.7 KiB

using System.Collections.Generic;
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.")]
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
}