|
|
|
|
@ -4,12 +4,13 @@ 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. |
|
|
|
|
/// 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 vehicle prefabs, tune speed and drift. |
|
|
|
|
/// 3. Duplicate and mirror-X for the opposite 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 |
|
|
|
|
{ |
|
|
|
|
@ -24,11 +25,28 @@ public class TrafficLane : MonoBehaviour |
|
|
|
|
|
|
|
|
|
// ── Vehicle Pool ────────────────────────────────────────────────────────── |
|
|
|
|
|
|
|
|
|
[Header("Vehicle Pool")] |
|
|
|
|
[Tooltip("Prefabs to randomly draw from when spawning. Can contain different vehicle types.")] |
|
|
|
|
[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; |
|
|
|
|
@ -39,9 +57,18 @@ public class TrafficLane : MonoBehaviour |
|
|
|
|
[Tooltip("Average forward speed in units / second.")] |
|
|
|
|
public float baseSpeed = 6f; |
|
|
|
|
|
|
|
|
|
[Tooltip("Per-vehicle speed spread as a fraction of baseSpeed (±).")] |
|
|
|
|
[Tooltip("Per-vehicle speed randomisation as a fraction of baseSpeed (±).")] |
|
|
|
|
[Range(0f, 0.5f)] |
|
|
|
|
public float speedVariance = 0.2f; |
|
|
|
|
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 ───────────────────────────────────────────────────────────────── |
|
|
|
|
|
|
|
|
|
@ -66,14 +93,25 @@ public class TrafficLane : MonoBehaviour |
|
|
|
|
[Range(0.01f, 1f)] |
|
|
|
|
public float driftFrequency = 0.08f; |
|
|
|
|
|
|
|
|
|
// ── Private state ───────────────────────────────────────────────────────── |
|
|
|
|
[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 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 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; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -85,7 +123,7 @@ public class TrafficLane : MonoBehaviour |
|
|
|
|
|
|
|
|
|
private void Start() |
|
|
|
|
{ |
|
|
|
|
if (vehiclePrefabs == null || vehiclePrefabs.Length == 0) |
|
|
|
|
if (!HasAnyPrefab()) |
|
|
|
|
{ |
|
|
|
|
Debug.LogWarning("[TrafficLane] No vehicle prefabs assigned.", this); |
|
|
|
|
enabled = false; |
|
|
|
|
@ -94,8 +132,6 @@ public class TrafficLane : MonoBehaviour |
|
|
|
|
|
|
|
|
|
BuildPool(); |
|
|
|
|
ScheduleNextSpawn(); |
|
|
|
|
|
|
|
|
|
// Scatter a few vehicles along the lane so it doesn't look empty at start. |
|
|
|
|
PrewarmLane(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -111,16 +147,28 @@ public class TrafficLane : MonoBehaviour |
|
|
|
|
{ |
|
|
|
|
for (var i = 0; i < poolSize; i++) |
|
|
|
|
{ |
|
|
|
|
var prefab = vehiclePrefabs[Random.Range(0, vehiclePrefabs.Length)]; |
|
|
|
|
var go = Instantiate(prefab, transform); |
|
|
|
|
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, noiseRow = Random.Range(0f, 100f) }); |
|
|
|
|
|
|
|
|
|
pool.Add(new Agent |
|
|
|
|
{ |
|
|
|
|
go = go, |
|
|
|
|
type = isMotorcycle ? VehicleType.Motorcycle : VehicleType.Car, |
|
|
|
|
noiseRow = Random.Range(0f, 100f), |
|
|
|
|
currentLateral = laneCenter |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
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) |
|
|
|
|
@ -145,7 +193,45 @@ public class TrafficLane : MonoBehaviour |
|
|
|
|
continue; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
agent.progress += agent.speed * Time.deltaTime; |
|
|
|
|
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) |
|
|
|
|
{ |
|
|
|
|
@ -153,15 +239,28 @@ public class TrafficLane : MonoBehaviour |
|
|
|
|
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; |
|
|
|
|
// ── 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(lateral, 0f, agent.progress); |
|
|
|
|
var next = new Vector3(agent.currentLateral, 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) |
|
|
|
|
{ |
|
|
|
|
@ -181,7 +280,6 @@ public class TrafficLane : MonoBehaviour |
|
|
|
|
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) |
|
|
|
|
@ -195,27 +293,51 @@ public class TrafficLane : MonoBehaviour |
|
|
|
|
|
|
|
|
|
// ── Helpers ─────────────────────────────────────────────────────────────── |
|
|
|
|
|
|
|
|
|
private void Activate(Agent agent, float startProgress) |
|
|
|
|
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) |
|
|
|
|
{ |
|
|
|
|
agent.progress = startProgress; |
|
|
|
|
agent.speed = baseSpeed * (1f + Random.Range(-speedVariance, speedVariance)); |
|
|
|
|
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; |
|
|
|
|
|
|
|
|
|
// 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) |
|
|
|
|
private void SpawnNext(float startZ) |
|
|
|
|
{ |
|
|
|
|
foreach (var agent in pool) |
|
|
|
|
{ |
|
|
|
|
if (!agent.active) |
|
|
|
|
{ |
|
|
|
|
Activate(agent, startProgress); |
|
|
|
|
Activate(agent, startZ); |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
@ -234,6 +356,26 @@ public class TrafficLane : MonoBehaviour |
|
|
|
|
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 |
|
|
|
|
@ -245,18 +387,29 @@ public class TrafficLane : MonoBehaviour |
|
|
|
|
Gizmos.color = Color.yellow; |
|
|
|
|
Gizmos.DrawLine(start, end); |
|
|
|
|
|
|
|
|
|
// Drift envelope. |
|
|
|
|
// 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. |
|
|
|
|
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; |
|
|
|
|
|