2024-11-19 11:51:36 +01:00
|
|
|
class_name MPNavigationGraph
|
|
|
|
extends Node2D
|
|
|
|
|
2024-11-19 13:52:51 +01:00
|
|
|
# godot does not support types on dictionaries, actual type is Dictionary[MPNavigationNode, Array[MPNavigationNode]]
|
|
|
|
var navigation_nodes: Dictionary = {}
|
|
|
|
var latest_navigation_result: MPPathfindingResult = null
|
|
|
|
var draw_nodes: bool = false
|
|
|
|
var draw_edges: bool = false
|
2024-11-19 11:51:36 +01:00
|
|
|
|
|
|
|
|
2024-11-19 13:52:51 +01:00
|
|
|
func all_nodes() -> Array[MPNavigationNode]:
|
2024-11-19 11:51:36 +01:00
|
|
|
# i've had a problem where godot would not allow me to directly return navigation_nodes.keys()
|
2024-11-19 13:52:51 +01:00
|
|
|
# because it wasn't able to cast the keys to Array[MPNavigationNode] directly because the type is not explicit
|
2024-11-19 11:51:36 +01:00
|
|
|
# on the dictionary, so i had to do this workaround.
|
2024-11-19 13:52:51 +01:00
|
|
|
var keys: Array = navigation_nodes.keys()
|
|
|
|
var nodes: Array[MPNavigationNode] = []
|
2024-11-19 11:51:36 +01:00
|
|
|
for key in keys:
|
2024-11-19 13:52:51 +01:00
|
|
|
if key is MPNavigationNode:
|
2024-11-19 11:51:36 +01:00
|
|
|
nodes.append(key)
|
|
|
|
else:
|
2024-11-19 13:52:51 +01:00
|
|
|
push_error("Key is not a MPNavigationNode: %s" % key)
|
2024-11-19 11:51:36 +01:00
|
|
|
return nodes
|
|
|
|
|
|
|
|
|
2024-11-19 13:52:51 +01:00
|
|
|
func get_connections(from: MPNavigationNode) -> Array[MPNavigationNode]:
|
2024-11-19 11:51:36 +01:00
|
|
|
# the same problem as the all_nodes() function
|
2024-11-19 13:52:51 +01:00
|
|
|
var connections: Array = navigation_nodes[from]
|
|
|
|
var nodes: Array[MPNavigationNode] = []
|
2024-11-19 11:51:36 +01:00
|
|
|
for connection in connections:
|
2024-11-19 13:52:51 +01:00
|
|
|
if connection is MPNavigationNode:
|
2024-11-19 11:51:36 +01:00
|
|
|
nodes.append(connection)
|
|
|
|
else:
|
2024-11-19 13:52:51 +01:00
|
|
|
push_error("Connection is not a MPNavigationNode: %s" % connection)
|
2024-11-19 11:51:36 +01:00
|
|
|
return nodes
|
|
|
|
|
|
|
|
|
2024-11-19 13:52:51 +01:00
|
|
|
func add_connection(from: MPNavigationNode, to: MPNavigationNode) -> void:
|
2024-11-19 11:51:36 +01:00
|
|
|
if all_nodes().has(from):
|
|
|
|
navigation_nodes[from].append(to)
|
|
|
|
else:
|
|
|
|
navigation_nodes[from] = [to]
|
|
|
|
|
|
|
|
|
2024-11-19 13:52:51 +01:00
|
|
|
func add_node(x: float, y: float, merge_threshold: float = -1.0) -> MPNavigationNode:
|
2024-11-19 11:51:36 +01:00
|
|
|
if merge_threshold > 0:
|
2024-11-19 13:52:51 +01:00
|
|
|
var closest_node: MPNavigationNode = find_closest_node_with_threshold(Vector2(x, y), merge_threshold)
|
2024-11-19 11:51:36 +01:00
|
|
|
if closest_node:
|
|
|
|
closest_node.was_merged = true
|
|
|
|
return closest_node
|
2024-11-19 13:52:51 +01:00
|
|
|
var node: MPNavigationNode = MPNavigationNode.new()
|
2024-11-19 11:51:36 +01:00
|
|
|
node.set_position(Vector2(x, y))
|
|
|
|
navigation_nodes[node] = []
|
|
|
|
return node
|
|
|
|
|
|
|
|
|
2024-11-19 13:52:51 +01:00
|
|
|
func find_closest_node_with_threshold(position: Vector2, threshold: float) -> MPNavigationNode:
|
|
|
|
var closest_node: MPNavigationNode = null
|
|
|
|
var closest_distance: float = threshold
|
2024-11-19 11:51:36 +01:00
|
|
|
for node in all_nodes():
|
|
|
|
var distance: float = position.distance_to(node.position)
|
|
|
|
if distance < closest_distance:
|
|
|
|
closest_node = node
|
|
|
|
closest_distance = distance
|
|
|
|
return closest_node
|
|
|
|
|
|
|
|
|
2024-11-19 13:52:51 +01:00
|
|
|
func remove_connection(from: MPNavigationNode, to: MPNavigationNode) -> void:
|
2024-11-19 11:51:36 +01:00
|
|
|
if all_nodes().has(from):
|
|
|
|
navigation_nodes[from].erase(to)
|
|
|
|
|
|
|
|
|
2024-11-19 13:52:51 +01:00
|
|
|
func remove_node(node: MPNavigationNode) -> void:
|
2024-11-19 11:51:36 +01:00
|
|
|
navigation_nodes.erase(node)
|
|
|
|
for other_node in all_nodes():
|
|
|
|
if other_node != node:
|
|
|
|
remove_connection(other_node, node)
|
|
|
|
|
|
|
|
|
|
|
|
func _draw() -> void:
|
|
|
|
if draw_nodes or draw_edges:
|
|
|
|
for from in all_nodes():
|
|
|
|
if draw_edges:
|
|
|
|
for to in get_connections(from):
|
|
|
|
draw_line(local_to_world(from.position), local_to_world(to.position), Color.RED, 1, false)
|
|
|
|
if draw_nodes:
|
|
|
|
draw_circle(local_to_world(from.position), 5, Color.RED)
|
|
|
|
|
|
|
|
if latest_navigation_result != null:
|
|
|
|
if latest_navigation_result.path.size() > 1:
|
|
|
|
for i in range(latest_navigation_result.path.size() - 1):
|
|
|
|
draw_line(local_to_world(latest_navigation_result.path[i].position), local_to_world(latest_navigation_result.path[i + 1].position), Color.GREEN, 1, false)
|
2024-11-19 13:52:51 +01:00
|
|
|
for node in latest_navigation_result.path:
|
|
|
|
draw_circle(local_to_world(node.position), 5, Color.GREEN)
|
2024-11-19 11:51:36 +01:00
|
|
|
|
|
|
|
|
|
|
|
func local_to_world(location: Vector2) -> Vector2:
|
|
|
|
return location * 32 + Vector2(16, 16)
|
|
|
|
|
2024-11-19 13:52:51 +01:00
|
|
|
|
|
|
|
func draw_pathfinding_result(result: MPPathfindingResult) -> void:
|
2024-11-19 11:51:36 +01:00
|
|
|
if latest_navigation_result and latest_navigation_result.is_identical_to(result):
|
|
|
|
return
|
|
|
|
latest_navigation_result = result
|
|
|
|
queue_redraw()
|
|
|
|
|
|
|
|
|
2024-11-19 13:52:51 +01:00
|
|
|
func clear_pathfinding_result() -> void:
|
|
|
|
latest_navigation_result = null
|
|
|
|
queue_redraw()
|
|
|
|
|
|
|
|
|
|
|
|
func determine_next_position(current_position: Vector2, target_position: Vector2) -> MPPathfindingResult:
|
|
|
|
var result: MPPathfindingResult = MPPathfindingResult.new()
|
2024-11-19 11:51:36 +01:00
|
|
|
|
|
|
|
# Find the closest node to the current position
|
2024-11-19 13:52:51 +01:00
|
|
|
var start_node: MPNavigationNode = find_closest_node_with_threshold(current_position, INF)
|
2024-11-19 11:51:36 +01:00
|
|
|
if start_node == null:
|
|
|
|
# No nodes exist; cannot navigate
|
|
|
|
result.is_next_target = true
|
|
|
|
result.next_position = current_position
|
|
|
|
return result
|
|
|
|
|
|
|
|
# Find the closest node to the target position
|
2024-11-19 13:52:51 +01:00
|
|
|
var end_node: MPNavigationNode = find_closest_node_with_threshold(target_position, INF)
|
2024-11-19 11:51:36 +01:00
|
|
|
if end_node == null:
|
|
|
|
# No nodes exist; cannot navigate
|
|
|
|
result.is_next_target = true
|
|
|
|
result.next_position = current_position
|
|
|
|
return result
|
|
|
|
|
|
|
|
# If the start and end nodes are the same, go directly to the target position
|
|
|
|
if start_node == end_node:
|
|
|
|
result.is_next_target = true
|
|
|
|
result.next_position = target_position
|
|
|
|
return result
|
|
|
|
|
|
|
|
# Run Dijkstra's algorithm to find the path
|
2024-11-19 13:52:51 +01:00
|
|
|
var path: Array[MPNavigationNode] = dijkstra(start_node, end_node)
|
2024-11-19 11:51:36 +01:00
|
|
|
result.path = path
|
|
|
|
|
|
|
|
# If a path is found, return the position of the next node in the path
|
|
|
|
if path.size() > 1:
|
|
|
|
# Next node in the path
|
|
|
|
result.next_position = path[1].position
|
|
|
|
return result
|
|
|
|
elif path.size() == 1:
|
|
|
|
# Directly reachable
|
|
|
|
result.is_next_target = true
|
|
|
|
result.next_position = target_position
|
|
|
|
else:
|
|
|
|
# No path found; return current position
|
|
|
|
result.is_next_target = true
|
|
|
|
result.next_position = current_position
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
2024-11-19 13:52:51 +01:00
|
|
|
func array_contains_node(arr: Array, node: MPNavigationNode) -> bool:
|
2024-11-19 11:51:36 +01:00
|
|
|
for item in arr:
|
|
|
|
if item == node:
|
|
|
|
return true
|
|
|
|
return false
|
|
|
|
|
|
|
|
|
2024-11-19 13:52:51 +01:00
|
|
|
func dijkstra(start_node: MPNavigationNode, end_node: MPNavigationNode) -> Array[MPNavigationNode]:
|
|
|
|
var open_set: Array[MPNavigationNode] = []
|
|
|
|
var closed_set: Array[MPNavigationNode] = []
|
|
|
|
var distances: Dictionary = {}
|
|
|
|
var previous_nodes: Dictionary = {}
|
2024-11-19 11:51:36 +01:00
|
|
|
|
|
|
|
distances[start_node] = 0
|
|
|
|
open_set.append(start_node)
|
|
|
|
|
|
|
|
while open_set.size() > 0:
|
|
|
|
# Find the node with the smallest tentative distance
|
2024-11-19 13:52:51 +01:00
|
|
|
var current_node: MPNavigationNode = open_set[0]
|
|
|
|
var current_distance: float = distances[current_node]
|
2024-11-19 11:51:36 +01:00
|
|
|
for node in open_set:
|
|
|
|
if distances[node] < current_distance:
|
|
|
|
current_node = node
|
|
|
|
current_distance = distances[node]
|
|
|
|
|
|
|
|
# If the end node is reached, reconstruct the path
|
|
|
|
if current_node == end_node:
|
2024-11-19 13:52:51 +01:00
|
|
|
var path: Array[MPNavigationNode] = []
|
|
|
|
var node: MPNavigationNode = end_node
|
2024-11-19 11:51:36 +01:00
|
|
|
while node != null:
|
|
|
|
path.insert(0, node)
|
|
|
|
node = previous_nodes.get(node, null)
|
|
|
|
return path
|
|
|
|
|
|
|
|
open_set.erase(current_node)
|
|
|
|
closed_set.append(current_node)
|
|
|
|
|
|
|
|
# Examine neighbors of the current node
|
|
|
|
var neighbors = get_connections(current_node)
|
|
|
|
for neighbor in neighbors:
|
|
|
|
if array_contains_node(closed_set, neighbor):
|
|
|
|
continue
|
|
|
|
|
|
|
|
var tentative_distance: float = distances[current_node] + current_node.position.distance_to(neighbor.position)
|
|
|
|
|
|
|
|
if not array_contains_node(open_set, neighbor) or tentative_distance < distances.get(neighbor, INF):
|
|
|
|
distances[neighbor] = tentative_distance
|
|
|
|
previous_nodes[neighbor] = current_node
|
|
|
|
if not array_contains_node(open_set, neighbor):
|
|
|
|
open_set.append(neighbor)
|
|
|
|
|
|
|
|
# No path found; return an empty array
|
|
|
|
return []
|