protected void PathTo(Vector3 endPosition) { // Snap start/end positions to road lattice nodes RoadLatticeNode startNode = BaseMapLoader.MapsService.RoadLattice.SnapToNode(transform.position); RoadLatticeNode endNode = BaseMapLoader.MapsService.RoadLattice.SnapToNode(endPosition); // Find path between start/end nodes PathToDestination = RoadLattice.UncheckedFindPath(startNode, endNode, PathSearchLimit); // Reset our pathfinding instructions - waypoint at index 0 is our start position WaypointIndex = 0; if (PathToDestination != null && PathToDestination.Count > 0) { // Init navigation details - our next target is the waypoint after the start position CurrentTargetPosition = new Vector3( PathToDestination[WaypointIndex].Location.x, transform.position.y, PathToDestination[WaypointIndex].Location.y); IsMoving = true; } else { IsMoving = false; } DisplayRoute(PathToDestination); }
/// <summary> /// Receives two game objects, snaps them to the current road lattice, and displays the shortest /// path between them. /// </summary> /// <remarks> /// Can be attached as a Unity event handler to the /// <see cref="RoadLatticePathFindingObjectMover.UpdatedPositions"/> UnityEvent. /// </remarks> /// <param name="start">The path start object.</param> /// <param name="end">The path end object.</param> public void CreatePathBetweenObjects(GameObject start, GameObject end) { if (PathDisplay != null) { Destroy(PathDisplay); } RoadLatticeNode startNode = MapsService.RoadLattice.SnapToNode(start.transform.position); RoadLatticeNode endNode = MapsService.RoadLattice.SnapToNode(end.transform.position); start.transform.position = new Vector3(startNode.Location.x, 0, startNode.Location.y); end.transform.position = new Vector3(endNode.Location.x, 0, endNode.Location.y); List <RoadLatticeNode> path = RoadLattice.UncheckedFindPath(startNode, endNode, PathSearchLimit); if (path == null) { Debug.LogFormat("No path found!"); } else { PathDisplay = MakeRouteDisplay(path); PathDisplay.transform.Translate(Vector3.up * PathDisplayY); } }
/// <summary> /// Creates a maker (a ground plane penetrating sphere of unit radius) at the location of the /// supplied <see cref="RoadLatticeNode"/> /// </summary> /// <param name="node">The node to indicate</param> /// <param name="parent">The parent GameObject under which to create the indicator object</param> static void MakeRoadLatticeNodeIndicator(RoadLatticeNode node, GameObject parent) { GameObject sphere = GameObject.CreatePrimitive(PrimitiveType.Sphere); sphere.transform.position = new Vector3(node.Location.x, 0, node.Location.y); sphere.name = "RoadLatticeNode"; sphere.transform.parent = parent.transform; }
/// <summary> /// Request a ticket for a particular path. /// </summary> /// <param name="from">The start location.</param> /// <param name="to">The target location.</param> /// <param name="vehicle">The requesting vehicle.</param> /// <returns>A ticket for the path, or null if one could not be given.</returns> public PathTicket Request(ulong from, ulong to, Vehicle vehicle) { string key = GetPathKey(from, to); Path path; // Check if this is the first time this path has been requested. if (!Paths.TryGetValue(key, out path)) { // Generate the path. RoadLatticeNode fromNode = RoadLattice.FindNodeAt(from); RoadLatticeNode toNode = RoadLattice.FindNodeAt(to); RoadLatticeEdge edge = fromNode.EdgeTo(toNode); // Check if the edge for this path is flagged as entering an intersection. if ((edge.EdgeFlags & RoadLatticeEdge.Flags.Intersection) != 0) { // Generate a path that opens and closes, like a traffic light. Randomize the values of // this behaviour. int closeDurationMs = Random.Range(3000, 8000); int closeFrequencyMs = Random.Range(10000, 30000); path = new IntersectionPath(fromNode.Location, toNode.Location, closeFrequencyMs, closeDurationMs); } else { // Not an intersection edge. Generate a regular path. path = new Path(fromNode.Location, toNode.Location); } // Store the path. Paths.Add(key, path); } if (path.IsClosed()) { // Path is closed, don't return a ticket. The requesting vehicle should try again later. return(null); } // Check if the vehicle can fit on the path given its available length, or if the path is // currently empty. if (path.Length - path.OccupiedLength >= vehicle.Clearance || path.Tickets.Count == 0) { // Add this vehicle's clearance to the path to update its available length. path.OccupiedLength += vehicle.Clearance; // Generate and return a ticket for the path. LinkedListNode <PathTicket> node = new LinkedListNode <PathTicket>(null); PathTicket ticket = new PathTicket(path, vehicle.Clearance, node); node.Value = ticket; path.Tickets.AddLast(node); return(ticket); } return(null); }
/// <summary> /// Once the map is loaded, setup main character and AI agents. /// Start activating the search. /// </summary> private void OnMapLoaded(MapLoadedArgs args) { if (CharactersContainer != null && BaseMapLoader != null) { GameObject avatarGO = GetRandomCharacter(AvatarPrefab); RoadLatticeCharacterController cc = avatarGO.GetComponent <RoadLatticeCharacterController>(); if (cc != null) { cc.BaseMapLoader = BaseMapLoader; cc.ShowPath = true; } // Start following our Avatar on the map if (SmoothFollowCamera != null) { SmoothFollowCamera.target = avatarGO.transform; SmoothFollowCamera.transform.LookAt(SmoothFollowCamera.target); } // Add random npc characters on map (red) for (int i = 0; i < NumberOfOpponents; i++) { GameObject npcGO = GetRandomCharacter(NPCPrefab); RoadLatticeAIController aic = npcGO.GetComponent <RoadLatticeAIController>(); if (aic != null) { aic.BaseMapLoader = BaseMapLoader; aic.Target = avatarGO; aic.enabled = true; NPCs.Add(aic); if (cc != null) { // Move distant NPCs closer to the avatar so that they don't take too long to reach // their target. Vector3 diff = aic.transform.position - cc.transform.position; Vector3 desiredPosition = Vector3.MoveTowards(cc.transform.position, aic.transform.position, Mathf.Min(diff.magnitude, MaxAIStartDistanceFromAvatar)); RoadLatticeNode node = BaseMapLoader.MapsService.RoadLattice.SnapToNode(desiredPosition); aic.transform.position = new Vector3(node.Location.x, 0, node.Location.y); } } } } ActiveAllNPCs(IsAISearchActive); ShowAIPaths(IsDebugPathOn); }
/// <summary> /// Returns the next target node after traversing the edge between two nodes. /// </summary> /// <param name="endNode">The previous end node.</param> /// <param name="startNode"> /// The previous start node. This node won't be returned unless it's the only neighbor of /// <see cref="endNode"/>. /// </param> /// <returns>The next target node to path to.</returns> private RoadLatticeNode FindNextTarget(RoadLatticeNode endNode, RoadLatticeNode startNode) { List <RoadLatticeNode> neighbors = new List <RoadLatticeNode>(endNode.Neighbors); if (neighbors.Count > 1) { // Remove the start node so we don't return it. neighbors.Remove(startNode); } // Return a random neighbor node. return(neighbors[Random.Range(0, neighbors.Count)]); }
/// <summary> /// Initializes the vehicle. /// </summary> /// <param name="trafficSystem">The traffic system.</param> /// <param name="startNode">The node the vehicle should start at.</param> public void Initialize(TrafficSystem trafficSystem, RoadLatticeNode startNode) { TrafficSystem = trafficSystem; // Find a target node from the start node. RoadLatticeNode targetNode = FindNextTarget(startNode, null); CurrentNodeLocationUID = startNode.LocationUID; TargetNodeLocationUID = targetNode.LocationUID; // Set the vehicle's position and rotation. Vector2 heading = (targetNode.Location - startNode.Location).normalized; transform.position = new Vector3(startNode.Location.x, 0, startNode.Location.y) + new Vector3(-heading.y, 0, heading.x) * DistanceFromRoadCenter; transform.forward = new Vector3(heading.x, 0, heading.y); }
/// <summary> /// Spawns as many vehicles as specified by <see cref="VehiclesToSpawn"/>. /// </summary> private void OnRoadLatticeModified(DidModifyRoadLatticeArgs args) { // Get the nodes in the road lattice. List <RoadLatticeNode> nodes = new List <RoadLatticeNode>(args.RoadLattice.Nodes); for (; VehiclesToSpawn > 0; VehiclesToSpawn--) { // Get a random node to spawn a vehicle on. RoadLatticeNode spawnNode = nodes[Random.Range(0, nodes.Count)]; // Instantiate a random vehicle. Vehicle vehiclePrefab = Vehicles[Random.Range(0, Vehicles.Length)]; Vehicle vehicle = Instantiate(vehiclePrefab); // Initialize it at the spawn node. vehicle.Initialize(TrafficSystem, spawnNode); } }
/// <summary> /// Returns the next target node after traversing the edge between two nodes. /// </summary> /// <param name="endNode">The previous end node.</param> /// <param name="startNode"> /// The previous start node. This node won't be returned unless it's the only neighbor of /// <see cref="endNode"/>. /// </param> /// <returns>The next target node to path to.</returns> private RoadLatticeNode FindNextTarget(RoadLatticeNode endNode, RoadLatticeNode startNode) { // Find all traversable neighbor nodes. List <RoadLatticeNode> neighbors = new List <RoadLatticeNode>(); foreach (RoadLatticeEdge edge in endNode.Edges) { if (IsTraversableRoad(edge.Segment)) { neighbors.Add(edge.Target); } } if (neighbors.Count > 1) { // Remove the start node so we don't return it. neighbors.Remove(startNode); } // Return a random neighbor node. return(neighbors[Random.Range(0, neighbors.Count)]); }
/// <summary> /// Creates characters and places them randomly on the map. /// </summary> private GameObject GetRandomCharacter(GameObject prefab) { GameObject character = Instantiate(prefab, CharactersContainer, false); List <RoadLatticeNode> nodes = BaseMapLoader.MapsService.RoadLattice.GetNodes(); if (nodes.Count == 0) { Debug.LogError("Could not find any nodes to spawn a character at."); } else { RoadLatticeNode firstRandomNode = nodes.ElementAt(Random.Range(0, nodes.Count)); Debug.LogFormat("Character spawned at {0}", firstRandomNode.Location); Vector3 newPosition = new Vector3(firstRandomNode.Location.x, 0f, firstRandomNode.Location.y); character.transform.position = newPosition; } return(character); }
/// <summary> /// Spawns as many vehicles as specified by <see cref="VehiclesToSpawn"/>. /// </summary> private void OnRoadLatticeModified(DidModifyRoadLatticeArgs args) { // Get all traversable nodes in the road lattice. List <RoadLatticeNode> nodes = new List <RoadLatticeNode>(); foreach (RoadLatticeNode node in args.RoadLattice.Nodes) { foreach (RoadLatticeEdge edge in node.Edges) { if (!Vehicle.IsTraversableRoad(edge.Segment)) { continue; } nodes.Add(node); break; } } // Don't spawn any vehicles if we have no traversable nodes to spawn them on. if (nodes.Count == 0) { return; } for (; VehiclesToSpawn > 0; VehiclesToSpawn--) { // Get a random node to spawn a vehicle on. RoadLatticeNode spawnNode = nodes[Random.Range(0, nodes.Count)]; // Instantiate a random vehicle. Vehicle vehiclePrefab = Vehicles[Random.Range(0, Vehicles.Length)]; Vehicle vehicle = Instantiate(vehiclePrefab); // Initialize it at the spawn node. vehicle.Initialize(TrafficSystem, spawnNode); } }
/// <summary> /// Queries <see cref="TrafficSystem"/> and moves the vehicle. /// </summary> void Update() { // Get the current and target node objects from the road lattice. RoadLatticeNode currentNode = TrafficSystem.RoadLattice.FindNodeAt(CurrentNodeLocationUID); RoadLatticeNode targetNode = TrafficSystem.RoadLattice.FindNodeAt(TargetNodeLocationUID); // Check that our current and target nodes exist in the road lattice, and that an edge // exists between them. if (targetNode == null || currentNode == null || !currentNode.HasEdgeTo(targetNode)) { // Release our (invalid) ticket. if (Ticket != null) { Ticket.Release(); Ticket = null; } if (currentNode != null && currentNode.NeighborCount > 0) { // Current node exists and has neighbors; try to find a new target. TargetNodeLocationUID = FindNextTarget(currentNode, null).LocationUID; } else { // Current node does not exist or has no neighbors. Destroy the vehicle. Destroy(gameObject); } return; } // Check if we have a ticket to the target node. if (Ticket == null) { // Don't have a ticket. Request one. Ticket = TrafficSystem.Request(CurrentNodeLocationUID, TargetNodeLocationUID, this); if (Ticket == null) { // Didn't get a ticket. Either the path is at capacity or is closed. Try again later. return; } } // Get the target position of the vehicle. This is the position offset from the centreline // by `DistanceFromRoadCenter`. Vector3 perpendicular = new Vector3(-Ticket.Heading.z, 0, Ticket.Heading.x); Vector3 targetPosition = Ticket.CurrentPosition + perpendicular * DistanceFromRoadCenter; // Face the target position. Vector3 heading = (targetPosition - transform.position).normalized; if (heading.sqrMagnitude > 0) { transform.forward = heading; } // Move the vehicle. transform.position = Vector3.MoveTowards(transform.position, targetPosition, Speed * Time.deltaTime); // Advance the ticket position if we've reached `targetPosition`. if ((transform.position - targetPosition).sqrMagnitude < 0.1f) { Ticket.Move(Speed * Time.deltaTime); } // Check if we've reached the end of the path. if (Ticket.DistanceToTarget < MaxDistanceUntilNextTarget) { // Find the next target node. RoadLatticeNode nextTargetNode = FindNextTarget(targetNode, currentNode); // Try acquiring a ticket to the new node. TrafficSystem.PathTicket nextTicket = TrafficSystem.Request(TargetNodeLocationUID, nextTargetNode.LocationUID, this); if (nextTicket == null) { // Couldn't acquire a ticket, try again next frame. Don't release the current ticket // until we can acquire the next one. return; } // Acquired a ticket. Release the old one and set the new current and target nodes. Ticket.Release(); Ticket = nextTicket; CurrentNodeLocationUID = TargetNodeLocationUID; TargetNodeLocationUID = nextTargetNode.LocationUID; } }
/// <summary> /// Instantiates a GameObject showing connections between <see cref="RoadLatticeNode"/>s using /// materials to distinguish disjoint partitions of the road graph. /// </summary> /// <param name="lattice">The lattice from which to generate a GameObject</param> /// <param name="indicateNodes">Whether to add markers at each node</param> /// <param name="materials">The materials to apply to the disjoint sub-lattices</param> /// <returns>The GameObject representation of the supplied lattice</returns> private static GameObject MakePartitionedRoadLatticeDebugGameObject( RoadLattice lattice, bool indicateNodes, Material[] materials) { // A list for collecting road segment end point locations. List <Vector2> vertices = new List <Vector2>(); // A set of nodes with connections to other nodes that have all been converted into geometry. HashSet <RoadLatticeNode> processed = new HashSet <RoadLatticeNode>(); int materialIndex = 0; GameObject latticeParent = new GameObject(); latticeParent.name = "Road Lattice Parent"; List <RoadLatticeNode> nodes = lattice.GetNodes(); foreach (RoadLatticeNode node in nodes) { // Skip this node if it has already had all its connections turned into geometry, otherwise // use it as the starting node for a new disjoint sub-lattice representation. if (processed.Contains(node)) { continue; } vertices.Clear(); // A set of nodes reachable from the set of nodes that have been processed so far. Note: this // list may contain nodes that have already been processed if such ndoes are reachable from // more than one neighbouring node. Stack <RoadLatticeNode> reachableNodes = new Stack <RoadLatticeNode>(); reachableNodes.Push(node); // Keep processing nodes til we have visited all reachable nodes. while (reachableNodes.Count > 0) { RoadLatticeNode source = reachableNodes.Pop(); // Skip nodes we have previously processed by reaching them through an alternative path. if (processed.Contains(source)) { continue; } if (indicateNodes) { MakeRoadLatticeNodeIndicator(source, latticeParent); } // Mark this node as processed, add segments for all its connections, and add all its // neighbours to the reachable set. processed.Add(source); foreach (RoadLatticeNode neighbor in source.Neighbors) { if (!processed.Contains(neighbor)) { vertices.Add(source.Location); vertices.Add(neighbor.Location); reachableNodes.Push(neighbor); } } } GameObject go = new GameObject(); go.name = "Road Lattice"; MakeSublatticeMeshes(go, vertices, materials[materialIndex]); materialIndex = (materialIndex + 1) % materials.Length; go.transform.parent = latticeParent.transform; } return(latticeParent); }