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.
419 lines
16 KiB
419 lines
16 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.
|
|
/// 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.
|
|
/// </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("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<Agent> 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
|
|
}
|
|
|