gai-godot-games/pathfinding-algorithms/scenes/mario-party/MarioPartyManagement.gd

236 lines
9.4 KiB
GDScript

extends Node2D
@onready var ground_layer: TileMapLayer = $GroundLayer
@onready var path_layer: TileMapLayer = $PathLayer
@onready var data_layer: TileMapLayer = $DataLayer
@onready var navigation_graph: MPNavigationGraph = $NavigationGraph
#
@onready var player: CharacterBody2D = $Player
var last_dice_result: int = 0
#
var global_transform_size: Vector2 = Vector2(0, 0)
#
const DIRECTIONS: Dictionary = {
"up": Vector2(0, -1),
"down": Vector2(0, 1),
"left": Vector2(-1, 0),
"right": Vector2(1, 0)
}
var node_positions: Dictionary = {}
func _ready() -> void:
global_transform_size = Vector2(16, 16) * data_layer.transform.get_scale()
build_graph()
# place the player at the first node
var current_player_position: Vector2 = node_positions.keys()[0]
player.global_position = local_to_world(current_player_position)
# bring player to top
player.z_index = 1
roll_dice()
player.finished_movement.connect(roll_dice)
func _physics_process(delta: float) -> void:
if Input.is_action_just_pressed("draw_toggle_nodes") or Input.is_action_just_pressed("draw_toggle_edges"):
navigation_graph.draw_nodes = !navigation_graph.draw_nodes
navigation_graph.draw_edges = !navigation_graph.draw_edges
navigation_graph.queue_redraw()
var dice_controls_min: int = 2
var dice_controls_max: int = 8
func roll_dice() -> void:
last_dice_result = randi() % (dice_controls_max - dice_controls_min + 1) + dice_controls_min
print("Dice result: ", last_dice_result)
$ResultLabel.text = "Rolled a " + str(last_dice_result)
# Find possible movement options
var movement_options: Array[MPNavigationNode] = find_nodes_with_distance(world_to_local(player.global_position), last_dice_result)
# visualize by spawning an indicator at each possible node, with a click event to move the player
for node in movement_options:
# res://scenes/mario-party/TileIndicator.tscn
var indicator_scene: Node2D = preload("res://scenes/mario-party/TileIndicator.tscn").instantiate()
indicator_scene.scale = Vector2(2, 2)
var area_node: Area2D = indicator_scene.get_node("area")
area_node.set_data(node.position, "player_movement")
area_node.indicator_clicked.connect(_on_indicator_clicked)
indicator_scene.global_position = local_to_world(node.get_position())
add_child(indicator_scene)
func _on_indicator_clicked(pos: Vector2, type: String) -> void:
print("Indicator clicked: ", type, " ", pos)
if type == "player_movement":
player.move_to(pos)
# delete all indicators
for child in get_children():
if child is Node2D:
var area_node: Area2D = child.get_node("area")
if area_node != null:
if area_node.get_type() == "player_movement":
child.queue_free()
func local_to_world(location: Vector2) -> Vector2:
return location * global_transform_size + global_transform_size / 2
func world_to_local(location: Vector2) -> Vector2:
return (location - global_transform_size / 2) / global_transform_size
func build_graph() -> void:
print("Identifying nodes")
# Step 1: Place nodes at positions where is_tile is true
for position in data_layer.get_used_cells():
var tile_data: TileData = data_layer.get_cell_tile_data(position)
var is_tile: bool = tile_data.get_custom_data("is_tile")
if is_tile:
var node: MPNavigationNode = navigation_graph.add_node(position.x, position.y)
node_positions[position] = node
var indicator_scene: Node2D = preload("res://scenes/mario-party/TileIndicator.tscn").instantiate()
var area_node: Area2D = indicator_scene.get_node("area")
area_node.set_display_type("node")
indicator_scene.global_position = local_to_world(position)
add_child(indicator_scene)
# Step 2: Connect nodes using flood-fill based on walkable tiles
print("Connecting nodes")
for position in node_positions.keys():
connect_node(position)
func connect_node(start_position: Vector2) -> void:
var start_node = node_positions.get(Vector2i(start_position))
var visited: Dictionary = {}
visited[start_position] = true
# print("Connecting node at ", start_position)
# For each direction, perform flood-fill
for dir_name in DIRECTIONS.keys():
var direction = DIRECTIONS[dir_name]
var next_position = start_position + direction
# print("Checking direction ", dir_name, " from ", start_position, " to ", next_position)
# Ensure the first tile respects the direction
if not is_valid_direction(next_position, dir_name):
continue
# print("Flood-fill in direction ", dir_name, " from ", next_position)
# Perform flood-fill from the valid tile
var connected_nodes: Array = flood_fill(next_position, visited)
# Add connections between the start node and found nodes
for target_position in connected_nodes:
if target_position != start_position:
if node_positions.has(Vector2i(target_position)):
var target_node = node_positions.get(Vector2i(target_position))
navigation_graph.add_connection(start_node, target_node)
print(start_position, " --> ", target_position)
func flood_fill(start_position: Vector2, visited: Dictionary) -> Array:
var stack: Array[Vector2] = [start_position]
var connected_nodes: Array[Vector2] = []
while stack.size() > 0:
var current_position = stack.pop_back()
# print(" - Visiting ", current_position)
# Skip if already visited
if visited.has(current_position):
continue
visited[current_position] = true
# Skip if not walkable
var tile_data: TileData = data_layer.get_cell_tile_data(current_position)
if tile_data == null:
continue
var is_walkable: bool = tile_data.get_custom_data("is_walkable")
var is_tile: bool = tile_data.get_custom_data("is_tile")
if (not is_walkable) and (not is_tile):
continue
# If this position is a node, add it to the result
if is_tile:
# print(" - Found node tile at ", current_position)
connected_nodes.append(current_position)
# Add neighboring tiles to the stack if they respect the direction
for dir_name in DIRECTIONS.keys():
var direction = DIRECTIONS[dir_name]
var neighbor_position = current_position + direction
if not visited.has(neighbor_position):
if is_valid_direction(current_position, dir_name):
stack.append(neighbor_position)
return connected_nodes
func is_valid_direction(position: Vector2, required_dir: String) -> bool:
var tile_data: TileData = data_layer.get_cell_tile_data(position)
if tile_data == null:
return false
var is_walkable: bool = tile_data.get_custom_data("is_walkable")
var walk_dir: String = tile_data.get_custom_data("walk_dir")
if walk_dir == "":
walk_dir = "any"
# print(" L ", position, " ", is_walkable, " ", walk_dir, " ", required_dir)
# Check if the tile is walkable and allows movement in the required direction
return is_walkable and (walk_dir == required_dir or walk_dir == "any")
# the first function that evaluates the finished graph
# it is given a starting position and a distance in integers, which represent the amount of nodes to travel to reach the target node(s)
# it will find any nodes that are the given distance away from the starting node and return them in an array
# distance here is the amount of nodes to travel, not the actual distance in pixels
# must use the MPNavigationNode class and the navigation_graph.get_connections(node: MPNavigationNode) function
func find_nodes_with_distance(start_position: Vector2, distance: int) -> Array[MPNavigationNode]:
var start_node = node_positions.get(Vector2i(start_position))
if start_node == null:
print("Error: No node found at starting position ", start_position)
return []
# Initialize BFS
var queue: Array = [[start_node, 0]] # Each element is [node, current_distance]
var visited: Dictionary = {start_node: true}
var result_nodes: Array[MPNavigationNode] = []
while queue.size() > 0:
var current = queue.pop_front()
var current_node: MPNavigationNode = current[0]
var current_distance: int = current[1]
# If the target distance is reached, add the node to the result
if current_distance == distance:
result_nodes.append(current_node)
continue # Do not explore further from this node
# If the current distance exceeds the target, stop exploring
if current_distance > distance:
break
# Explore neighbors of the current node
var neighbors: Array[MPNavigationNode] = navigation_graph.get_connections(current_node)
for neighbor in neighbors:
if not visited.has(neighbor):
visited[neighbor] = true
queue.append([neighbor, current_distance + 1])
return result_nodes