Traffic system
For my internship at Virtual Play I built a traffic system. The game needed to be playable on old laptops, so we weren’t allowed to use physics and colliders in order to save performance. The game takes place in a tile based – and procedurally generated city.
The traffic system supports cars, bicycles and pedestrians.
The approach I’ve taken is to let the waypoints handle the traffic logic. That way the vehicles themselves don’t have to know about each other and simply move from point A to B.
Waypoint connections
Every waypoint caches a previous- and next waypoint. Since the game is tile based, we manually placed – and connected the waypoints within the tile prefabs. Therefore, the only waypoints that had to be connected during runtime were the entry and exit waypoints with its neighbour tiles’ waypoints.
Vehicle reaching a waypoint
Vehicles move from waypoint to waypoint. When a vehicle reaches a waypoint, the waypoint checks if the vehicle is allowed to pass the current waypoint or not. A vehicle can be stopped at a waypoint for two reasons: The vehicle either ran into a red light/queue, or the vehicle has to wait at a blocked path. Blocked paths are paths between two waypoints that can be crossed by a vehicle that has priority, due to the traffic rules.
public virtual void VehicleArrived(VehicleController vehicleController) { StateMachine stateMachine = vehicle.GetComponent<StateMachine>(); bool canPass = UpdateVehiclesCanPass(vehicle.VehicleData); if (canPass) { INavigator navigator = vehicle.GetComponent<INavigator>(); vehicle.SetNewWaypoint(navigator.NextTargetWaypoint); /* TODO: store previous acceleration progress to continue the acceleration if (stateMachine.CurrentState.Equals(typeof(AccelerationState))) stateMachine.SwitchState(typeof(AccelerationState)); else stateMachine.SwitchState(typeof(LinearState)); */ stateMachine.SwitchState(typeof(LinearState)); vehicle.State = VehicleStates.Move; navigator.PreviousWaypoint.UpdateVehiclesCanPass(); OnVehicleReleased?.Invoke(vehicle, NextWaypoint); } else { EnqueueVehicle(vehicle); stateMachine.SwitchState(typeof(StopState)); } _vehiclesCanPass.Value = canPass; }
The bool canPass gets set to the UpdateVehiclesCanPass()’s returned value.
private bool UpdateVehiclesCanPass(VehicleData vehicle) { if (!VehiclesCanPass()) { return false; } if (!NextWaypointHasAvailableSpace(vehicle)) { return false; } return true; }
The VehiclesCanPass() method is a virtual method in the BaseWaypoint class. It can be overwritten by waypoints with specific reasons to stop a vehicle, like the TrafficLightWaypoint shown below. The JunctionWaypoint, which checks for blocked paths also overwrites this bethod.
protected override bool VehiclesCanPass() => TrafficLightState == TrafficLightState.Green;
protected override bool VehiclesCanPass() { bool canPass = true; foreach (Path blockingPath in _blockingPaths) { if (_intersection.PathIsOccupied(blockingPath)) canPass = false; } return canPass; }
After that I check if there is a queue at the next waypoint and if so, whether there is enough room to fit the vehicle between the queue and the current waypoint.
Waypoint AvailablePosition
Every waypoint stores an AvailablePosition variable, which the vehicles use as their target position. By default, the Available position is the same as waypoint’s position. However, the AvailablePosition changes when a vehicle is stopped at the current AvailablePosition.
protected virtual void UpdateAvaiablePosition(BaseVehicleController vehicle, bool carAddedToQueue) { Vector3 direction; INavigator navigator = vehicle.GetComponent<INavigator>(); BaseWaypoint previousWaypoint = navigator.PreviousWaypoint; if (previousWaypoint == null) { Debug.LogWarning("previous waypoint is null"); direction = Vector3.Normalize(vehicle.transform.position - transform.position); } else direction = Vector3.Normalize(previousWaypoint.transform.position - transform.position); if (!carAddedToQueue) direction *= -1; //if a car is dequeued, move the AvailablePosition towards the waypoint float randomSpacing = UnityEngine.Random.Range(_minRandomVehicleSpacing, _maxRandomVehicleSpacing); Vector3 offset = direction * (vehicle.VehicleData.Length + _minimumVehicleDistance + randomSpacing); AvailablePosition += offset; OnAvailablePositionChange?.Invoke(); CheckPreviousWaypointDistance(previousWaypoint); }
Whenever the AvailablePosition changes, the OnAvailablePositionChange event is invoked. Whenever a vehicle sets a new targetwaypoint they unsubscribe from the previous -event and subscribe to the new target waypoint’s OnAvailablePositionChange event. This way, all vehicles will be notified and have to readjust their TargetPosition if the AvailablePosition changes.
//called when the AvailablePosition is changed public override void RecalculateMovement() { _availablePosition = _navigator.TargetWaypoint.AvailablePosition; //calculate distance difference in percentage float distanceToNewTarget = Vector3.Distance(_startPosition, _availablePosition); float difference = _distanceToTarget - distanceToNewTarget; float percentage = difference / _distanceToTarget; //update targetTime by calculated percentage _targetTime += percentage * _targetTime; _targetPosition = _availablePosition }
Waypoint queue system
When a vehicle is stopped at a waypoint, a couple of things happen.
-The vehicle is added to the waypoint’s queue.
-The AvailablePosition is repositioned behind the queued vehicle’s position. By using the vehicle’s length to determine the new position, the queue system works with a variety of vehicles: Cars, trucks, busses, etc.
-The vehicle’s state is set to the StopState, making the vehicle do nothing but wait for the waypoint to dequeue it and change its state again.
private void EnqueueVehicle(VehicleController vehicle) { vehicle.State = VehicleStates.InQueue; _vehicles.AddLast(vehicle); { VehicleInQueue = true; } UpdateAvaiablePosition(vehicle, true); if (vehicle.State == VehicleStates.Accelerate) AvailableQueuePosition = AvailablePosition; //if the vehicle that arrived at the AvailableQueuePosition was accelerating(which means it was previously queued), move the //AvailableQueuePosition back for the other accelerating vehicles, along with the regular AvailablePosition }
When a vehicle is released from the queue, its movement state is set to the acceleration state. If there are vehicles left in the queue, the AvailablePosition is repositioned to the position of the vehicle that just left the queue. If there are no vehicles left in the queue, the AvailablePosition gets reset.
//drive off after waiting in queue protected virtual IEnumerator AccelerateVehicle() ///TODO: Change name to something more descriptive, VehicleLeavesQueue? also update VehicleState name accordingly { _acceleratingVehicle = true; yield return new WaitForSeconds(UnityEngine.Random.Range(0f, _maxAccelerationDelay)); //small delay before accelerating, like in real life NewVehicleController acceleratingVehicle = _vehicles.First.Value; _vehicles.RemoveFirst(); INavigator navigator = vehicle.GetComponent<INavigator>(); vehicle.SetNewWaypoint(navigator.NextTargetWaypoint); vehicle.SwitchState(typeof(AccelerationState)); if (_vehicles.Count == 0) { VehicleInQueue = false; ResetAvailablePosition(); ResetAvailableQueuePosition(); } acceleratingVehicle.State = VehicleStates.Accelerate; UpdateAvaiablePosition(acceleratingVehicle, false); _acceleratingVehicle = false; }
Blocked paths
When a vehicle enters a path that potentially blocks another vehicle, it calls the VehicleEntersJunction() method. This registers the path as occupied.
public override void VehicleArrived(BaseVehicleController vehicle) { base.VehicleArrived(vehicle); if (_crossingPhase == CrossingPhase.Start) { _intersection.VehicleEntersJunction(vehicle, NextWaypoint); } }
public void VehicleEntersJunction(BaseVehicleController vehicle, BaseWaypoint nextWaypoint) { INavigator navigator = vehicle.GetComponent<INavigator>(); Path path = new Path() { Start = navigator.PreviousWaypoint, End = nextWaypoint }; if(_occupiedPaths.ContainsKey(vehicle)) { _occupiedPaths.Remove(vehicle); vehicle.OnArrival -= VehicleLeavesJunction; } _occupiedPaths.Add(vehicle, path); vehicle.OnArrival += VehicleLeavesJunction; } public void VehicleLeavesJunction(BaseVehicleController vehicle) { Path pathLeft = _occupiedPaths[vehicle]; _occupiedPaths.Remove(vehicle); OnPathLeft?.Invoke(pathLeft); vehicle.OnArrival -= VehicleLeavesJunction; }
When a vehicle arrives that has to wait for the vehicle crossing its path, the VehiclesCanPass() method returns false, since the blocked path is currently occupied.
protected override bool VehiclesCanPass() { bool canPass = true; foreach (Path blockingPath in _blockingPaths) { if (_intersection.PathIsOccupied(blockingPath)) canPass = false; } return canPass; }
Vehicle prefab
The vehicle consists of a number of components.
-Vehicle navigator
The vehicle navigator is used to set the vehicle’s target waypoint. Besides changing the target waypoint, you can also change the next target waypoint. This way the vehicles can prevent driving into a jammed lane and instead choose a different lane.
public class VehicleNavigator : MonoBehaviour, INavigator { public BaseWaypoint TargetWaypoint { get; private set; }} //NextTargetWaypoint becomes the TargetWaypoint, after reaching the current TargetWaypoint public BaseWaypoint NextTargetWaypoint { get; private set; } public BaseWaypoint PreviousWaypoint { get; private set; } private VehicleController _vehicle; private void Awake() { _vehicle = GetComponent<VehicleController>(); } public void SetFirstTarget(BaseWaypoint spawnWaypoint) { PreviousWaypoint = spawnWaypoint; TargetWaypoint = spawnWaypoint.NextWaypoint; NextTargetWaypoint = TargetWaypoint.NextWaypoint; } public void SetNewTarget() { PreviousWaypoint = TargetWaypoint; TargetWaypoint = NextTargetWaypoint; NextTargetWaypoint = TargetWaypoint.NextWaypoint; //VehicleTracker vehicleTracker = TargetWaypoint.GetComponent<VehicleTracker>(); //vehicleTracker.RegisterVehicle(_vehicle); } public void ChangeNextTarget(BaseWaypoint nextTarget) { NextTargetWaypoint = nextTarget; } }
-State Machine
Updates the current state and handles switching state.
public class StateMachine : MonoBehaviour { public BaseState CurrentState => _currentState; private BaseState _currentState; private Dictionary<Type, BaseState> _states; private void Awake() { InitStates(); } public void UpdateState() { if (_currentState == null) { return; } Type nextState = _currentState.Tick(); if (nextState != null) SwitchState(nextState); } public void SwitchState(Type nextState) { if (_currentState != null) _currentState.OnStateExit(); BaseState previousState = _currentState; _currentState = _states[nextState]; _currentState.OnStateEnter(previousState); } public bool IsCurrentState(Type state) { if (_currentState != null && _currentState.GetType().Equals(state)) return true; return false; } public void InitStates() { _states = new Dictionary<Type, BaseState>(); BaseState[] states = GetComponentsInChildren<BaseState>(); foreach (BaseState state in states) { _states.Add(state.GetType(), state); } } }
-Vehicle movement states
BaseMoveState
public abstract class BaseMoveState : BaseState { [SerializeField] protected float _speed; public float Time { get; protected set; } protected float _targetTime, _distanceToTarget, _pathLength; protected Vector3 _previousWaypointPosition, _startPosition, _availablePosition, _targetPosition; protected bool _isCorner, _hasReachedWaypoint; protected INavigator _navigator; protected SpeedModifier _speedModifier; protected BezierControlPoint _controlPoint; protected BaseWaypoint _targetWaypoint; protected NewVehicleController _vehicleController; protected virtual void Awake() { _speedModifier = GetComponent<SpeedModifier>(); _navigator = GetComponent<INavigator>(); _vehicleController = GetComponent<NewVehicleController>(); _targetTime = 1f; } public override void OnStateEnter(BaseState previousState) { BaseMoveState prevState = previousState as BaseMoveState; if(prevState != null) Time = prevState.Time; _previousWaypointPosition = _navigator.PreviousWaypoint.transform.position; _startPosition = transform.position; _availablePosition = _navigator.TargetWaypoint.AvailablePosition; _isCorner = _navigator.TargetWaypoint is CornerWaypoint ? true : false; //TODO: Calculate the path length once in the waypoint class and pass it to the vehicles if(_isCorner) { CornerWaypoint targetWaypoint = (CornerWaypoint)_navigator.TargetWaypoint; int cornerIndex = targetWaypoint.GetCornerIndex(_navigator.PreviousWaypoint); _pathLength = targetWaypoint.GetCornerLength(cornerIndex); _controlPoint = _navigator.TargetWaypoint.GetComponentInChildren<BezierControlPoint>(); } else { _pathLength = Vector3.Distance(_previousWaypointPosition, _availablePosition); } _distanceToTarget = (1 - Time) * _pathLength; _speedModifier.CalculateModifier(_pathLength); if (_controlPoint != null && _controlPoint.IsStraight) { _isCorner = false; _speedModifier.CalculateModifier(_distanceToTarget); } } //called when the AvailablePosition is changed public virtual void RecalculateMovement() { } protected void UpdateTime() { Time += _speedModifier.Value * _speed; } protected void Move(float time, Vector3 targetPos) { Vector3 targetPosition = Vector3.Lerp(_previousWaypointPosition, targetPos, time); transform.LookAt(targetPosition); transform.position = targetPosition; } protected void ArriveAtWaypoint() { transform.LookAt(_availablePosition); transform.position = _availablePosition; Time = 0f; _hasReachedWaypoint = true; _navigator.TargetWaypoint.VehicleArrived(_vehicleController); } protected void MoveCorner(float time, Vector3 targetPos) { time = Mathf.Clamp01(time); Vector3 targetPosition = BezierUtilityHelper.GetQuadraticBezierPoint( time, _previousWaypointPosition, _controlPoint.transform.position, targetPos); transform.LookAt(targetPosition); transform.position = targetPosition; } protected bool HasReachedDestination() { if (Time >= _targetTime) { return true; } return false; } }
AccelerationState, LinearState, DecelerationState and StopState
public class LinearState : BaseMoveState { public override void OnStateEnter(BaseState previousState) { base.OnStateEnter(previousState); } public override Type Tick() { UpdateTime(); if (!HasReachedDestination()) { if (_isCorner) MoveCorner(Time, _availablePosition); else Move(Time, _availablePosition); return null; } ArriveAtWaypoint(); return null; } //called when the AvailablePosition is changed public override void RecalculateMovement() { _availablePosition = _navigator.TargetWaypoint.AvailablePosition; //calculate distance difference in percentage float distanceToNewTarget = Vector3.Distance(_startPosition, _availablePosition); float difference = _distanceToTarget - distanceToNewTarget; float percentage = difference / _distanceToTarget; //update targetTime by calculated percentage _targetTime += percentage * _targetTime; _targetPosition = _availablePosition } public override void OnStateExit() { base.OnStateExit(); } }
-Speed modifier
Calculates the speed modifier based on the distance to the next waypoint. The speed modifier is used to make every interpolation movement between waypoints the same speed.
Vehicle controller
temp
public class VehicleController : BaseVehicleController { [SerializeField] private float _decelerationDistance = 3.5f; private StateMachine _moveController; private INavigator _navigator; private int _decelerationCount, _count; public void Init(BaseWaypoint spawnWaypoint) { _moveController = GetComponent<StateMachine>(); InitNavigator(spawnWaypoint); InitStates(); SwitchState(typeof(AccelerationState)); } private void InitNavigator(BaseWaypoint startWaypoint) { _navigator = GetComponent<INavigator>(); _navigator.SetFirstTarget(startWaypoint); } private void InitStates() { _moveController = GetComponent<StateMachine>(); _moveController.InitStates(); } public void SwitchState(Type nextState) => _moveController.SwitchState(nextState); private void FixedUpdate() { _moveController.UpdateState(); if (_moveController.IsCurrentState(typeof(StopState))) { return; } if (_moveController.IsCurrentState(typeof(DecelerationState))) { return; } if (_navigator.TargetWaypoint.CanPass && _navigator.NextTargetWaypoint.CanPass) { return; } if (_count < _decelerationCount) { _count++; return; } float distanceToTarget = Vector3.Distance(transform.position, _navigator.TargetWaypoint.AvailablePosition); bool inRange = distanceToTarget < _decelerationDistance; if (!_navigator.TargetWaypoint.CanPass) { if (inRange) _moveController.SwitchState(typeof(DecelerationState)); else _decelerationCount = (int)((distanceToTarget - _decelerationDistance) * 2.5f); return; } } public override void SetNewWaypoint(BaseWaypoint waypoint) { if (_navigator == null) { Init(waypoint); return; } _navigator.SetNewTarget(); base.SetNewWaypoint(waypoint); if(_navigator.PreviousWaypoint != null) { _navigator.PreviousWaypoint.OnAvailablePositionChange -= UpdateAvailablePosition; } waypoint.OnAvailablePositionChange += UpdateAvailablePosition; } public void UpdateAvailablePosition() { if(_moveController.CurrentState is BaseMoveState) { BaseMoveState currentState = _moveController.CurrentState as BaseMoveState; currentState.UpdateAvailablePosition(); } } }
Custom editor scripts
I created a custom editor script to be able to place, remove and connect waypoints more easily. I created different tabs (Edit Waypoints, Connect Waypoints and Modify Waypoints) to change the behavior when clicking or dragging the waypoints.
[CustomEditor(typeof(WaypointConnector))] public class WaypointConnectorEditor : Editor { //Waypoints private SelectionInfo _selectionInfo; //toolbar private ToolbarTab _currentTab; private int _selectedTab; //GUI private bool _needsRepaint; //object references private SerializedObject _soTarget; private WaypointConnector _waypointConnector; #region Toolbar public override void OnInspectorGUI() { DrawDefaultInspector(); _soTarget.Update(); HandleToolbarSelection(); GUILayout.Space(10); DrawToolbarTab(); } private void HandleToolbarSelection() { EditorGUI.BeginChangeCheck(); _selectedTab = GUILayout.Toolbar(_selectedTab, new string[] { "Edit Waypoints", "Connect Waypoints", "Modify Waypoints" }); switch (_selectedTab) { case 0: _currentTab = ToolbarTab.EditWaypoints; break; case 1: _currentTab = ToolbarTab.ConnectWaypoints; break; case 2: _currentTab = ToolbarTab.ModifyWaypoints; break; default: _currentTab = ToolbarTab.None; break; } if (EditorGUI.EndChangeCheck()) { _soTarget.ApplyModifiedProperties(); GUI.FocusControl(null); } } private void DrawToolbarTab() { EditorGUI.BeginChangeCheck(); switch (_currentTab) { case ToolbarTab.EditWaypoints: EditorGUILayout.HelpBox("Left click to place a new waypoint, right click to destroy the hovered waypoint", MessageType.Info); break; case ToolbarTab.ConnectWaypoints: EditorGUILayout.HelpBox("Drag and drop to connect two waypoints", MessageType.Info); break; case ToolbarTab.ModifyWaypoints: EditorGUILayout.HelpBox("Select a waypoint to modify it", MessageType.Info); break; } if (EditorGUI.EndChangeCheck()) { _soTarget.ApplyModifiedProperties(); } } #endregion Toolbar private void OnSceneGUI() { Event guiEvent = Event.current; Vector3 mousePosition = GetMousePosition(guiEvent); HandleInput(guiEvent); UpdateMouseOverSelection(mousePosition); switch (_currentTab) { case ToolbarTab.EditWaypoints: EditWaypoints(guiEvent, mousePosition); break; case ToolbarTab.ConnectWaypoints: ConnectWaypoints(guiEvent, mousePosition); break; case ToolbarTab.ModifyWaypoints: ModifytWaypoints(guiEvent, mousePosition); break; } if (guiEvent.type == EventType.Layout) HandleUtility.AddDefaultControl(GUIUtility.GetControlID(FocusType.Passive)); DrawHandles(); if (_needsRepaint) { HandleUtility.Repaint(); _needsRepaint = false; } } #region HandleInput private Vector3 GetMousePosition(Event guiEvent) { Ray mouseRay = HandleUtility.GUIPointToWorldRay(guiEvent.mousePosition); float drawPlaneHeight = 0f; float distanceToDrawPlane = (drawPlaneHeight - mouseRay.origin.y) / mouseRay.direction.y; return mouseRay.GetPoint(distanceToDrawPlane); } private void UpdateMouseOverSelection(Vector3 mousePosition) { for (int i = 0; i < _waypointConnector.Waypoints.Count; i++) { BaseWaypoint waypoint = _waypointConnector.Waypoints[i]; if (Vector3.Distance(mousePosition, waypoint.Position) < _waypointConnector.HandleRadius) { _selectionInfo.HoveredWaypoint = waypoint; _selectionInfo.MouseOverWaypoint = true; return; } } _selectionInfo.HoveredWaypoint = null; _selectionInfo.MouseOverWaypoint = false; } #endregion HandleInput #region Draw Handles private void DrawHandles() { if (_waypointConnector.Waypoints == null) { return; } Handles.color = Color.white; for (int i = 0; i < _waypointConnector.Waypoints.Count; i++) { BaseWaypoint waypoint = _waypointConnector.Waypoints[i]; if (waypoint == _selectionInfo.HoveredWaypoint) Handles.color = Color.blue; else if (waypoint == _selectionInfo.SelectedWaypoint) Handles.color = Color.green; else Handles.color = Color.white; Handles.DrawSolidDisc(waypoint.Position, Vector3.up, _waypointConnector.HandleRadius); if (_waypointConnector.Waypoints[i].NextWaypoint == null) { continue; } Handles.color = Color.black; Handles.DrawDottedLine(waypoint.Position, waypoint.NextWaypoint.Position, 4f); } } #endregion Draw Handles #region Waypoint Actions private void CreateWaypoint(Vector3 position) { GameObject waypoint = (GameObject)PrefabUtility.InstantiatePrefab(_waypointConnector.WaypointPrefab, _waypointConnector.WaypointParent); waypoint.transform.position = position; Undo.RegisterCreatedObjectUndo(waypoint, "Create waypoint"); Undo.RegisterCompleteObjectUndo(_waypointConnector, "Add waypoint to list"); _waypointConnector.Waypoints.Add(waypoint.GetComponent<BaseWaypoint>()); WaypointGizmos gizmos = waypoint.GetComponent<WaypointGizmos>(); if(gizmos != null) gizmos.DrawGizmos = false; _needsRepaint = true; } private void DestroyWaypoint(Vector3 mousePosition) { if(_selectionInfo.HoveredWaypoint != null) { GameObject waypointToDestroy = _selectionInfo.HoveredWaypoint.transform.gameObject; //Undo.RecordObject(_waypointConnector, "Remove waypoint from List"); _waypointConnector.Waypoints.Remove(_selectionInfo.HoveredWaypoint); foreach(BaseWaypoint waypoint in _waypointConnector.Waypoints) { if ((BaseWaypoint)waypoint.NextWaypoint == _selectionInfo.HoveredWaypoint) waypoint.NextWaypoint = null; } Undo.DestroyObjectImmediate(waypointToDestroy); } } #endregion Waypoints Actions #region Waypoint Connector Tabs private void EditWaypoints(Event guiEvent, Vector3 mousePosition) { if (guiEvent.type == EventType.MouseDown && guiEvent.button == 0 && guiEvent.modifiers == EventModifiers.None) { //left mouse down CreateWaypoint(mousePosition); _needsRepaint = true; } if (guiEvent.type == EventType.MouseDown && guiEvent.button == 1 && guiEvent.modifiers == EventModifiers.None) { //right mouse down if(_selectionInfo.MouseOverWaypoint) DestroyWaypoint(mousePosition); _needsRepaint = true; } } private void ConnectWaypoints(Event guiEvent, Vector3 mousePosition) { if (guiEvent.type == EventType.MouseDown && guiEvent.button == 0 && guiEvent.modifiers == EventModifiers.None) { //left mouse down if (_selectionInfo.MouseOverWaypoint) { _selectionInfo.SelectedWaypoint = _selectionInfo.HoveredWaypoint; } _needsRepaint = true; } if (guiEvent.type == EventType.MouseDrag && guiEvent.button == 0 && guiEvent.modifiers == EventModifiers.None) { //left mouse drag _needsRepaint = true; } if (_selectionInfo.SelectedWaypoint != null) { Handles.color = Color.red; Handles.DrawDottedLine(mousePosition, _selectionInfo.SelectedWaypoint.Position, 4f); _needsRepaint = true; } if (guiEvent.type == EventType.MouseUp && guiEvent.button == 0 && guiEvent.modifiers == EventModifiers.None) { //left mouse up if(_selectionInfo.MouseOverWaypoint && _selectionInfo.SelectedWaypoint != null) { if(_selectionInfo.SelectedWaypoint != _selectionInfo.HoveredWaypoint) { SerializedObject so = new SerializedObject(_selectionInfo.SelectedWaypoint); SerializedProperty serializedProperty = so.FindProperty("_nextWaypoint"); serializedProperty.objectReferenceValue = _selectionInfo.HoveredWaypoint; so.ApplyModifiedProperties(); //so.FindProperty("NextWaypoint").objectReferenceValue = _selectionInfo.HoveredWaypoint; ///TODO: undo not working //Undo.RegisterCompleteObjectUndo(selectedWaypoint, "Assigned next waypoint"); } } _selectionInfo.SelectedWaypoint = null; _needsRepaint = true; } } private void ModifytWaypoints(Event guiEvent, Vector3 mousePosition) { if (guiEvent.type == EventType.MouseDown && guiEvent.button == 0 && guiEvent.modifiers == EventModifiers.None) { //left mouse down if (_selectionInfo.MouseOverWaypoint) { _selectionInfo.SelectedWaypoint = _selectionInfo.HoveredWaypoint; } else { _selectionInfo.SelectedWaypoint = null; } _needsRepaint = true; } } #endregion Waypoint Connector Tabs private void OnEnable() { _waypointConnector = target as WaypointConnector; _waypointConnector.InitWaypointConnector(); _soTarget = new SerializedObject(_waypointConnector); _selectionInfo = new SelectionInfo(); Tools.hidden = true; //hides the selected object's transform handle } private void OnDisable() { Tools.hidden = false; //shows the selected object's transform handle again } private void OnDestroy() { _waypointConnector.StopWaypointConnector(); } } public enum ToolbarTab { EditWaypoints, ConnectWaypoints, ModifyWaypoints, None }
The junction tiles have a lot of waypoints. These waypoints are positions for vehicles to stop when letting another vehicle pass. And the blocked paths, which track if a vehicle with priority is crossing its path.

Since it was very tedious and time consuming to manually connect the previous- and next waypoint of each waypoint for a number of different junction tiles, I wrote a custom editor script to do the work for me.
The junction waypoint names end with the path’s entry and exit direction. So for example: JunctionWaypointNW, is on the path from North to West.
Some waypoints are BranchingWaypoints, collored yellow, which have multiple next waypoints. While the turquoise waypoints are able to have multiple previous waypoints. Instead of using the normal direction N, E, S or W, I named the direction with the letter X to make the editor script connect the following waypoints to multiple paths.
Using the last two letters in the waypoint names I was able to separate the different paths. Using the distance from the entry waypoint to the other waypoints on that path I was able to get the order in which the waypoints need to be connected to each other.
using UnityEngine; using UnityEditor; using System.Collections.Generic; using UnityEditor.SceneManagement; using System.Linq; using System.Text; /* * Requires Waypoint-gameObject namingconvention to work: * Entry OR Exit + Wp (Waypoint) + N OR E OR S OR W (direction) * Example: ExitWpN, EntryWpE, ExitWpW, etc. */ public class WaypointConnectorWindow : EditorWindow { [MenuItem("Tools/Waypoint Connector")] private static void OpenWindow() { GetWindow<WaypointConnectorWindow>(); } [SerializeField] private Transform _waypointParent = null, _junctionWaypointParent = null, _crossingWaypointParent = null; [SerializeField] private GameObject _controlPointPrefab = null; [SerializeField] private bool _clearExistingControlPoints = false; [SerializeField] private float _controlPointDistance = 1.5f; [SerializeField] private string[] _directionConversions = null; private Vector3 _tileCenter; private List<BaseWaypoint> _entryWaypoints, _exitWaypoints, _allWaypoints; private BaseWaypoint[] _junctionWaypoints, _crossingWaypoints; private List<string> _junctionDirections, _crossingDirections; private string Entry = "Entry", Exit = "Exit"; private void OnGUI() { SerializedObject obj = new SerializedObject(this); EditorGUILayout.PropertyField(obj.FindProperty("_waypointParent")); EditorGUILayout.PropertyField(obj.FindProperty("_junctionWaypointParent")); EditorGUILayout.PropertyField(obj.FindProperty("_crossingWaypointParent")); EditorGUILayout.PropertyField(obj.FindProperty("_controlPointPrefab")); EditorGUILayout.PropertyField(obj.FindProperty("_clearExistingControlPoints")); EditorGUILayout.PropertyField(obj.FindProperty("_controlPointDistance")); EditorGUILayout.PropertyField(obj.FindProperty("_directionConversions")); if (_waypointParent == null) EditorGUILayout.HelpBox("Assign the root object that contains the waypoints", MessageType.Warning); if (_waypointParent != null) { EditorGUILayout.BeginVertical(); DrawButtons(); EditorGUILayout.EndVertical(); } DrawConnectAll(); obj.ApplyModifiedProperties(); } private void DrawButtons() { if (GUILayout.Button("Connect Waypoints")) ConnectWaypoints(); if (GUILayout.Button("Convert Directions")) { ConvertDirections(); } } private void DrawConnectAll() { if (GUILayout.Button("Connect All")) ConnectAll(); } private void ConnectAll() { EditorGUI.BeginChangeCheck(); List<WaypointParentGroup> waypointParents = new List<WaypointParentGroup>(); List<WaypointParentGroup> bicycleWaypointParents = new List<WaypointParentGroup>(); StreetTile[] tiles = FindObjectsOfType<StreetTile>(); foreach (StreetTile tile in tiles) { WaypointParentGroup parentGroup = new WaypointParentGroup() { WaypointParent = tile.transform.Find("Waypoints"), JunctionWaypointParent = tile.transform.Find("Junction Waypoints"), CrossingWaypointParent = tile.transform.Find("Crossing Waypoints") }; waypointParents.Add(parentGroup); WaypointParentGroup bicycleParentGroup = new WaypointParentGroup() { WaypointParent = tile.transform.Find("Bicycle Waypoints"), JunctionWaypointParent = tile.transform.Find("Biycle Junction Waypoints"), CrossingWaypointParent = tile.transform.Find("Bicycle Crossing Waypoints") }; bicycleWaypointParents.Add(bicycleParentGroup); } foreach (WaypointParentGroup parentGroup in waypointParents) { _waypointParent = parentGroup.WaypointParent; _junctionWaypointParent = parentGroup.JunctionWaypointParent; _crossingWaypointParent = parentGroup.CrossingWaypointParent; ConnectWaypoints(); } if (EditorGUI.EndChangeCheck()) EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene()); } private void ConnectWaypoints() { //get the tile center position Vector3 centerOffset = new Vector3(15f, 0f, -15f); _tileCenter = _waypointParent.position + centerOffset; //store all waypoints and directions in lists _allWaypoints = new List<BaseWaypoint>(); InitEntryExitWaypoints(); InitJunctionWaypoints(); InitCrossingWaypoints(); ResetWaypointConnections(); foreach (BaseWaypoint entryWaypoint in _entryWaypoints) { BranchingWaypoint entryWp; if (!(entryWaypoint is BranchingWaypoint)) { Debug.LogWarning("WaypointConnector expected BranchingWaypoint as entry waypoint, skipping waypoint link"); continue; } entryWp = entryWaypoint as BranchingWaypoint; char entryDirection = GetWaypointDirection(entryWp); foreach (BaseWaypoint exitWaypoint in _exitWaypoints) { char exitDirection = GetWaypointDirection(exitWaypoint); if (exitDirection.Equals(entryDirection)) { continue; } string combinedDirection = entryDirection.ToString() + exitDirection.ToString(); LinkWaypoints(entryWp, exitWaypoint, combinedDirection); } } _allWaypoints.Clear(); } private void LinkWaypoints(BranchingWaypoint entryWp, BaseWaypoint exitWp, string combinedDirection) { List<BaseWaypoint> connectingWaypoints = GetConnectingWaypoints(combinedDirection); //if no junction- or crossing waypoints were found, connect to the exit waypoint if (connectingWaypoints.Count == 0) { ConnectBranchingWp(entryWp, exitWp); ConnectCornerWp(null, entryWp, exitWp as CornerWaypoint, null); return; } BaseWaypoint previousPreviousWaypoint = null, nextWaypoint = null; //sort connecting Waypoints by distance //using Linq connectingWaypoints = connectingWaypoints.OrderBy(waypoint => (waypoint.transform.position - entryWp.transform.position).sqrMagnitude).ToList(); /* not using Linq connectingWaypoints.Sort(delegate (BaseWaypoint a, BaseWaypoint b) { return Vector2.Distance(entryWp.transform.position, a.transform.position).CompareTo( Vector2.Distance(entryWp.transform.position, b.transform.position)); }); */ //connect entry to the closest ConnectBranchingWp(entryWp, connectingWaypoints[0]); if (connectingWaypoints[0] is CornerWaypoint) { CornerWaypoint corner = connectingWaypoints[0] as CornerWaypoint; if (connectingWaypoints.Count > 1) nextWaypoint = connectingWaypoints[1]; ConnectCornerWp(null, entryWp, corner, nextWaypoint); } //connect to each other for (int i = 0; i < connectingWaypoints.Count - 2; i++) { previousPreviousWaypoint = nextWaypoint = null; BaseWaypoint nextWp = connectingWaypoints[i + 1]; if (nextWp is CornerWaypoint) { CornerWaypoint corner = nextWp as CornerWaypoint; if (i > 0) previousPreviousWaypoint = connectingWaypoints[i - 1]; if (i < connectingWaypoints.Count - 2) nextWaypoint = connectingWaypoints[i + 2]; ConnectCornerWp(previousPreviousWaypoint, connectingWaypoints[i], corner, nextWaypoint); } if (connectingWaypoints[i] is BranchingWaypoint) { ConnectBranchingWp((BranchingWaypoint)connectingWaypoints[i], connectingWaypoints[i + 1]); continue; } else if (!(connectingWaypoints[i] is Waypoint)) { Debug.LogWarning("Connecting waypoint is not of type Waypoint, cancelling waypoint linking"); continue; } Waypoint waypoint = connectingWaypoints[i] as Waypoint; ConnectWp(waypoint, nextWp); } //connect the last connecting waypoint to the exit waypoint if (!(connectingWaypoints[connectingWaypoints.Count - 1] is Waypoint)) { Debug.LogWarning("Last connecting waypoint is not of type Waypoint, cancelling waypoint linking"); return; } Waypoint lastWaypoint = connectingWaypoints[connectingWaypoints.Count - 1] as Waypoint; if (lastWaypoint == null) { Debug.Log("lastWaypoint is null"); } ConnectWp(lastWaypoint, exitWp); if (!(exitWp is CornerWaypoint)) { Debug.LogWarning("Last connecting waypoint is not of type CornerWaypoint, cancelling corner linking"); return; } if (exitWp is CornerWaypoint) { CornerWaypoint cornerWp = exitWp as CornerWaypoint; ConnectCornerWp(connectingWaypoints[connectingWaypoints.Count - 2], lastWaypoint, cornerWp, null); } } private List<BaseWaypoint> GetConnectingWaypoints(string combinedDirection) { List<BaseWaypoint> connectingWaypoints = new List<BaseWaypoint>(); char entryDirection = combinedDirection[0]; char exitDirection = combinedDirection[1]; if(_junctionWaypoints != null) { for (int i = 0; i < _junctionWaypoints.Length && i < _junctionDirections.Count; i++) { char junctionEntry = _junctionDirections[i][0]; char junctionExit = _junctionDirections[i][1]; if ((entryDirection.Equals(junctionEntry) || junctionEntry.Equals('X')) && (exitDirection.Equals(junctionExit) || junctionExit.Equals('X'))) connectingWaypoints.Add(_junctionWaypoints[i]); } } if(_crossingWaypoints != null) { for (int i = 0; i < _crossingWaypoints.Length && i < _crossingDirections.Count; i++) { char crossingEntry = _crossingDirections[i][0]; char crossingExit = _crossingDirections[i][1]; if ((entryDirection.Equals(crossingEntry) || crossingEntry.Equals('X')) && (exitDirection.Equals(crossingExit) || crossingExit.Equals('X'))) connectingWaypoints.Add(_crossingWaypoints[i]); } } return connectingWaypoints; } private void ConnectBranchingWp(BranchingWaypoint branchingWp, BaseWaypoint nextWp) { branchingWp.AddPossibleWaypoint(nextWp); EditorUtility.SetDirty(branchingWp); } private void ConnectCornerWp(BaseWaypoint previousPreviousWp, BaseWaypoint previousWp, CornerWaypoint cornerWp, BaseWaypoint nextWaypoint) { if (cornerWp == null) { Debug.Log("cornerWp is null: " + previousWp.name + "\t" + previousWp.transform.parent.name + "\t" + previousWp.transform.parent.parent.name); } Vector3 entryDirection = GetCornerEntryDirection(previousPreviousWp, previousWp, cornerWp); Vector3 exitDirection = GetCornerExitDirection(previousWp, cornerWp, nextWaypoint); Vector3 entryLineEnd = previousWp.transform.position + (entryDirection * 20f); Vector3 exitLineEnd = cornerWp.transform.position - (exitDirection * 20f); Vector3 intersectionPoint = CollisionUtilityHelper.lineIntersect(previousWp.transform.position, entryLineEnd, exitLineEnd, cornerWp.transform.position); if(intersectionPoint == Vector3.zero) { Debug.LogWarning("No intersection point found"); return; } BezierControlPoint controlPoint = GetControlPoint(previousWp, cornerWp); controlPoint.transform.position = intersectionPoint; EditorUtility.SetDirty(controlPoint); CurvePair curvePair = new CurvePair() { ControlPoint = controlPoint, PreviousWaypoint = previousWp }; CheckForStraight(curvePair, cornerWp); cornerWp.AddCorner(curvePair); EditorUtility.SetDirty(cornerWp); } private Vector3 GetCornerEntryDirection(BaseWaypoint previousPreviousWp, BaseWaypoint previousWp, CornerWaypoint cornerWp) { Vector3 entryDirection; if(previousPreviousWp == null) { //previous is entry return Vector3.zero; } if (previousWp is CornerWaypoint) { BezierControlPoint previousControlPoint = previousWp.GetComponentInChildren<BezierControlPoint>(); Vector3 controlPointPos; if (previousControlPoint != null) controlPointPos = previousControlPoint.transform.position; else controlPointPos = NewCalculateControllPointPosition(previousWp, cornerWp); entryDirection = (previousWp.transform.position - controlPointPos).normalized; return entryDirection; } else { if (_entryWaypoints.Contains(previousWp)) { //is first waypoint char entryDir = GetWaypointDirection(cornerWp); return GetWaypointDirection(entryDir); } entryDirection = (previousWp.transform.position - previousPreviousWp.transform.position).normalized; return entryDirection; } } private Vector3 GetCornerExitDirection(BaseWaypoint previousWp, CornerWaypoint cornerWp, BaseWaypoint nextWaypoint) { Vector3 exitDirection; if (nextWaypoint == null) { char exitDir = GetWaypointDirection(cornerWp); return GetWaypointDirection(exitDir); } else if (nextWaypoint is CornerWaypoint) { CornerWaypoint nextCornerWaypoint = nextWaypoint as CornerWaypoint; Vector3 tempControlPointPos = NewCalculateControllPointPosition(cornerWp, nextCornerWaypoint); exitDirection = (tempControlPointPos - nextCornerWaypoint.transform.position).normalized; return exitDirection; } else { exitDirection = (nextWaypoint.transform.position - cornerWp.transform.position).normalized; return exitDirection; } } private void ConnectWp(Waypoint startWp, BaseWaypoint nextWp) { startWp.SetNextWaypoint(nextWp); EditorUtility.SetDirty(startWp); } private void ResetWaypointConnections() { foreach (BaseWaypoint entryWp in _allWaypoints) { if (entryWp is BranchingWaypoint) { BranchingWaypoint branchingWp = entryWp as BranchingWaypoint; branchingWp.ResetPossibleWaypoints(); } } foreach (BaseWaypoint exitWp in _allWaypoints) { if (exitWp is CornerWaypoint) { CornerWaypoint cornerWp = exitWp as CornerWaypoint; cornerWp.ResetCorners(); if (_clearExistingControlPoints) { DestroyChildGameObjects(exitWp.transform); } } } } private void InitEntryExitWaypoints() { _entryWaypoints = new List<BaseWaypoint>(); _exitWaypoints = new List<BaseWaypoint>(); BaseWaypoint[] waypoints = _waypointParent.GetComponentsInChildren<BaseWaypoint>(); _allWaypoints.AddRange(waypoints); foreach (BaseWaypoint waypoint in waypoints) { if (waypoint.gameObject.name.StartsWith(Entry)) _entryWaypoints.Add(waypoint); else if (waypoint.gameObject.name.StartsWith(Exit)) _exitWaypoints.Add(waypoint); else Debug.LogWarning("Attempting to connect waypoint without 'Entry' or 'Exit' in its name"); } } private void InitJunctionWaypoints() { _junctionDirections = new List<string>(); if (_junctionWaypointParent == null) { return; } _junctionWaypoints = _junctionWaypointParent.GetComponentsInChildren<BaseWaypoint>(); _allWaypoints.AddRange(_junctionWaypoints); foreach (BaseWaypoint waypoint in _junctionWaypoints) { string directions = waypoint.name.Substring(waypoint.name.Length - 2); _junctionDirections.Add(directions); } } private void InitCrossingWaypoints() { _crossingDirections = new List<string>(); if (_crossingWaypointParent == null) { return; } _crossingWaypoints = _crossingWaypointParent.GetComponentsInChildren<BaseWaypoint>(); _allWaypoints.AddRange(_crossingWaypoints); foreach (BaseWaypoint waypoint in _crossingWaypoints) { string directions = waypoint.name.Substring(waypoint.name.Length - 2); _crossingDirections.Add(directions); } } private void CheckForStraight(CurvePair curvePair, CornerWaypoint cornerWaypoint) { char entryDirection = GetWaypointDirection(curvePair.PreviousWaypoint); char cornerDirection = GetWaypointDirection(cornerWaypoint); bool isStraight = false; if (entryDirection.Equals('N') && cornerDirection.Equals('S')) { isStraight = true; } if (entryDirection.Equals('E') && cornerDirection.Equals('W')) { isStraight = true; } if (entryDirection.Equals('S') && cornerDirection.Equals('N')) { isStraight = true; } if (entryDirection.Equals('W') && cornerDirection.Equals('E')) { isStraight = true; } if (curvePair.PreviousWaypoint.transform.position.x == cornerWaypoint.transform.position.x) isStraight = true; if (curvePair.PreviousWaypoint.transform.position.z == cornerWaypoint.transform.position.z) isStraight = true; if (!isStraight) { return; } curvePair.ControlPoint.IsStraight = true; curvePair.ControlPoint.transform.position = (curvePair.PreviousWaypoint.transform.position + cornerWaypoint.transform.position) / 2; } private Vector3 CalculateControllPointPosition(BaseWaypoint waypoint, CornerWaypoint cornerWaypoint) { Vector3 position1 = new Vector3(waypoint.transform.position.x, 0f, cornerWaypoint.transform.position.z); Vector3 position2 = new Vector3(cornerWaypoint.transform.position.x, 0f, waypoint.transform.position.z); if (Vector3.Distance(position1, _tileCenter) < Vector3.Distance(position2, _tileCenter)) return position1; else return position2; } private Vector3 NewCalculateControllPointPosition(BaseWaypoint previousWaypoint, CornerWaypoint cornerWaypoint) { Vector3 direction = (cornerWaypoint.transform.position - previousWaypoint.transform.position).normalized; Vector3 center = (previousWaypoint.transform.position + cornerWaypoint.transform.position) * 0.5f; Vector3 perpendicular = new Vector3(direction.z, 0f, direction.x).normalized; Vector3 position1 = center + (perpendicular * _controlPointDistance); Vector3 position2 = center - (perpendicular * _controlPointDistance); if (cornerWaypoint.transform.position.x > previousWaypoint.transform.position.x) { if (position1.x > position2.x) return position1; else return position2; } else { if (position1.x > position2.x) return position2; else return position1; } } private BezierControlPoint GetControlPoint(BaseWaypoint entryWaypoint, CornerWaypoint exitWaypoint) { Vector3 position = CalculateControllPointPosition(entryWaypoint, exitWaypoint); GameObject controllPoint = (GameObject)PrefabUtility.InstantiatePrefab(_controlPointPrefab, exitWaypoint.transform); controllPoint.transform.position = position; controllPoint.transform.rotation = Quaternion.identity; return controllPoint.GetComponent<BezierControlPoint>(); } private char GetWaypointDirection(BaseWaypoint waypoint) { return waypoint.name[waypoint.name.Length - 1]; } private Vector3 GetWaypointDirection(char direction) { switch (direction) { case 'N': return Vector3.forward; case 'E': return Vector3.right; case 'S': return Vector3.back; case 'W': return Vector3.left; default: return Vector3.zero; } } private void ConvertDirections() { BaseWaypoint[] waypoints = _waypointParent.parent.GetComponentsInChildren<BaseWaypoint>(); foreach(BaseWaypoint waypoint in waypoints) { StringBuilder stringBuilder = new StringBuilder(waypoint.name); int nameLength = waypoint.name.Length; char entryDir = waypoint.name[nameLength - 2]; char exitDir = waypoint.name[nameLength - 1]; char prevDir, newDir; //modify name for(int i = 0; i < _directionConversions.Length; i++) { prevDir = _directionConversions[i][0]; newDir = _directionConversions[i][1]; if (entryDir.Equals(prevDir)) stringBuilder[nameLength - 2] = newDir; if (exitDir.Equals(prevDir)) stringBuilder[nameLength - 1] = newDir; waypoint.name = stringBuilder.ToString(); } } } private void DestroyChildGameObjects(Transform parentTransform) { while (parentTransform.childCount > 0) { GameObject child = parentTransform.GetChild(0).gameObject; if (PrefabUtility.IsPartOfPrefabInstance(child)) { //if a part of a prefab instance then get the instance handle var prefabInstance = PrefabUtility.GetPrefabInstanceHandle(child); //destroy the handle DestroyImmediate(prefabInstance); } //the usual destroy immediate to clean up scene objects DestroyImmediate(child.gameObject, true); } } private struct WaypointParentGroup { public Transform WaypointParent { get; set; } public Transform JunctionWaypointParent { get; set; } public Transform CrossingWaypointParent { get; set; } } }