Bezier Map curves

This commit is contained in:
2ManyProjects 2025-03-04 11:08:01 -06:00
parent e1178d5f5c
commit b4db4afd5f
2 changed files with 33 additions and 568 deletions

View file

@ -169,63 +169,7 @@ func generate_map():
if from_valid and to_valid: if from_valid and to_valid:
valid_connections.append(conn) valid_connections.append(conn)
valid_connections = ensure_path_exists(valid_nodes, valid_connections)
# Then filter out overlapping connections while maintaining connectivity
valid_connections = filter_overlapping_connections(valid_nodes, valid_connections)
# Perform a final check to make sure all nodes are connected
var reachable_nodes = {}
reachable_nodes[0] = true # Start node id is always 0
# Build the graph again
var graph = {}
for node in valid_nodes:
graph[node.id] = []
for conn in valid_connections:
if not graph.has(conn.from):
graph[conn.from] = []
graph[conn.from].append(conn.to)
# Do a BFS from the start node
var queue = [0] # Starting node ID
while queue.size() > 0:
var current = queue.pop_front()
if graph.has(current):
for neighbor in graph[current]:
if not reachable_nodes.has(neighbor):
reachable_nodes[neighbor] = true
queue.append(neighbor)
# Check if all nodes are reachable
var all_reachable = true
for node in valid_nodes:
if not reachable_nodes.has(node.id):
all_reachable = false
print("Node not reachable: ", node.id, " - Type: ", node.type)
break
# If not all nodes are reachable, only keep the reachable ones
if not all_reachable:
var truly_valid_nodes = []
for node in valid_nodes:
if reachable_nodes.has(node.id):
truly_valid_nodes.append(node)
# Update valid_nodes to only include reachable nodes
valid_nodes = truly_valid_nodes
# Also update connections to only include those between reachable nodes
var truly_valid_connections = []
for conn in valid_connections:
if reachable_nodes.has(conn.from) and reachable_nodes.has(conn.to):
truly_valid_connections.append(conn)
valid_connections = truly_valid_connections
space_nodes_horizontally(valid_nodes, valid_connections)
# Return the generated map # Return the generated map
return { return {
"nodes": valid_nodes, "nodes": valid_nodes,
@ -268,363 +212,3 @@ func _get_next_id():
var id = _next_id var id = _next_id
_next_id += 1 _next_id += 1
return id return id
func filter_overlapping_connections(nodes, connections):
var valid_connections = []
var node_positions = {}
# Create a lookup for node positions
for node in nodes:
node_positions[node.id] = Vector2(node.position.x, node.position.y)
# Create a graph representation to check connectivity
var graph = {}
for node in nodes:
graph[node.id] = []
for connection in connections:
if not graph.has(connection.from):
graph[connection.from] = []
if not graph.has(connection.to):
graph[connection.to] = []
graph[connection.from].append(connection.to)
# Find start and end nodes
var start_node_id = -1
var end_node_id = -1
for node in nodes:
if node.type == RoomType.STARTING:
start_node_id = node.id
elif node.type == RoomType.FINAL:
end_node_id = node.id
# Sort connections by priority, giving preference to:
# 1. Critical path connections
# 2. More vertical connections (to reduce crossing)
# 3. Shorter connections
var sorted_connections = connections.duplicate()
sorted_connections.sort_custom(func(a, b):
var a_from_pos = node_positions[a.from]
var a_to_pos = node_positions[a.to]
var b_from_pos = node_positions[b.from]
var b_to_pos = node_positions[b.to]
# Critical path connections (ones needed to reach the final boss) get highest priority
var a_critical = is_on_critical_path(a.from, a.to, start_node_id, end_node_id, graph)
var b_critical = is_on_critical_path(b.from, b.to, start_node_id, end_node_id, graph)
if a_critical and not b_critical:
return true
elif not a_critical and b_critical:
return false
# Connections from the same node should be spread out by x-position
if a.from == b.from:
var a_x_diff = abs(a_from_pos.x - a_to_pos.x)
var b_x_diff = abs(b_from_pos.x - b_to_pos.x)
# Prefer more aligned connections (less horizontal distance)
return a_x_diff < b_x_diff
# Calculate how vertical each connection is
var a_dx = abs(a_to_pos.x - a_from_pos.x)
var a_dy = abs(a_to_pos.y - a_from_pos.y)
var b_dx = abs(b_to_pos.x - b_from_pos.x)
var b_dy = abs(b_to_pos.y - b_from_pos.y)
var a_vertical_ratio = a_dy / max(a_dx, 0.1) # Avoid division by zero
var b_vertical_ratio = b_dy / max(b_dx, 0.1)
# Prefer more vertical connections
if a_vertical_ratio > b_vertical_ratio * 1.5: # Threshold to make the difference significant
return true
elif b_vertical_ratio > a_vertical_ratio * 1.5:
return false
# Then prioritize by level progression (forward progress)
var a_forward = a_from_pos.y < a_to_pos.y
var b_forward = b_from_pos.y < b_to_pos.y
if a_forward and not b_forward:
return true
elif not a_forward and b_forward:
return false
# Finally by length
var a_dist = (a_to_pos - a_from_pos).length_squared()
var b_dist = (b_to_pos - b_from_pos).length_squared()
return a_dist < b_dist
)
# First pass: Add all critical path connections
for connection in sorted_connections:
if is_on_critical_path(connection.from, connection.to, start_node_id, end_node_id, graph):
valid_connections.append(connection)
# Create a set of connected nodes from the critical path
var connected_nodes = {}
connected_nodes[start_node_id] = true
for connection in valid_connections:
connected_nodes[connection.from] = true
connected_nodes[connection.to] = true
# Second pass: Add remaining connections, avoiding overlaps
for connection in sorted_connections:
# Skip if already added
if connection in valid_connections:
continue
# Only consider connections that link to already connected nodes
if not connected_nodes.has(connection.from) and not connected_nodes.has(connection.to):
continue
var from_pos = node_positions[connection.from]
var to_pos = node_positions[connection.to]
# Check if this connection would intersect with any valid connection
var has_intersection = false
for valid_conn in valid_connections:
var valid_from_pos = node_positions[valid_conn.from]
var valid_to_pos = node_positions[valid_conn.to]
# Skip if they share a node
if connection.from == valid_conn.from or connection.from == valid_conn.to or \
connection.to == valid_conn.from or connection.to == valid_conn.to:
continue
# Check intersection
if do_lines_intersect(from_pos, to_pos, valid_from_pos, valid_to_pos):
has_intersection = true
break
# If no intersections, add to valid connections
if not has_intersection:
valid_connections.append(connection)
connected_nodes[connection.from] = true
connected_nodes[connection.to] = true
# Final pass: Ensure all nodes are reachable
# This part can remain the same as your current implementation
return valid_connections
# Helper function to check if a connection is on a critical path
func is_on_critical_path(from_id, to_id, start_id, end_id, graph):
# Skip the check if we're directly connecting start to end
if (from_id == start_id and to_id == end_id):
return true
# Use breadth-first search to find paths
var visited = {from_id: true}
var queue = [from_id]
# First check if we can reach end_id from to_id
while queue.size() > 0:
var current = queue.pop_front()
if current == end_id:
return true
for neighbor in graph[current]:
if not visited.has(neighbor):
visited[neighbor] = true
queue.append(neighbor)
# If we're not on a path to the end, this isn't critical
return false
# Helper function to ensure at least one valid path exists from start to end
func ensure_path_exists(nodes, connections):
# Find start and end node IDs
var start_id = -1
var end_id = -1
for node in nodes:
if node.type == RoomType.STARTING:
start_id = node.id
elif node.type == RoomType.FINAL:
end_id = node.id
# Build a graph representation
var graph = {}
for node in nodes:
graph[node.id] = []
for conn in connections:
if not graph.has(conn.from):
graph[conn.from] = []
graph[conn.from].append(conn.to)
# Use BFS to check if end is reachable from start
var visited = {start_id: true}
var queue = [start_id]
var path_exists = false
while queue.size() > 0:
var current = queue.pop_front()
if current == end_id:
path_exists = true # Path exists
break
for neighbor in graph[current]:
if not visited.has(neighbor):
visited[neighbor] = true
queue.append(neighbor)
# If path exists, return the existing connections
if path_exists:
return connections # Return the unmodified connections array
# No path exists, need to add one
# Find nodes by level
var nodes_by_level = {}
for node in nodes:
if not nodes_by_level.has(node.level):
nodes_by_level[node.level] = []
nodes_by_level[node.level].append(node)
# Create a direct path from start to end through each level
var current_id = start_id
var levels = nodes_by_level.keys()
levels.sort()
# Copy connections array to avoid modifying the original
var new_connections = connections.duplicate()
for i in range(1, levels.size()):
var level = levels[i]
if nodes_by_level.has(level) and nodes_by_level[level].size() > 0:
# Pick the middle node in this level
var target_node = nodes_by_level[level][nodes_by_level[level].size() / 2]
# Add a connection from current to this node
new_connections.append({
"from": current_id,
"to": target_node.id
})
current_id = target_node.id
# If we've reached the final level, connect to end
if level == levels[-1]:
new_connections.append({
"from": current_id,
"to": end_id
})
return new_connections
func do_lines_intersect(a_start: Vector2, a_end: Vector2, b_start: Vector2, b_end: Vector2) -> bool:
# Line segment intersection using the parametric equation approach
var s1_x = a_end.x - a_start.x
var s1_y = a_end.y - a_start.y
var s2_x = b_end.x - b_start.x
var s2_y = b_end.y - b_start.y
var denominator = (-s2_x * s1_y + s1_x * s2_y)
if denominator == 0:
return false # Collinear or parallel lines
var s = (-s1_y * (a_start.x - b_start.x) + s1_x * (a_start.y - b_start.y)) / denominator
var t = (s2_x * (a_start.y - b_start.y) - s2_y * (a_start.x - b_start.x)) / denominator
# If s and t are between 0-1, the lines intersect
return (s >= 0 && s <= 1 && t >= 0 && t <= 1)
# Fixed version of space_nodes_horizontally with proper connection tracking
func space_nodes_horizontally(nodes, connections):
# Group nodes by level
var nodes_by_level = {}
for node in nodes:
if not nodes_by_level.has(node.level):
nodes_by_level[node.level] = []
nodes_by_level[node.level].append(node)
# Process each level to spread out nodes
for level in nodes_by_level.keys():
var level_nodes = nodes_by_level[level]
# Skip levels with just one node
if level_nodes.size() <= 1:
continue
# Sort nodes by their current horizontal position
level_nodes.sort_custom(func(a, b): return a.position.x < b.position.x)
# First pass: calculate connections for each node
var node_connections = {}
for node in level_nodes:
node_connections[node.id] = {
"above": [],
"below": []
}
# Find connections to this node
for conn in connections:
# Connection from above
if conn.to == node.id:
var from_node = null
for n in nodes:
if n.id == conn.from and n.level < node.level:
from_node = n
break
if from_node:
node_connections[node.id].above.append(from_node)
# Connection to below
elif conn.from == node.id:
var to_node = null
for n in nodes:
if n.id == conn.to and n.level > node.level:
to_node = n
break
if to_node:
node_connections[node.id].below.append(to_node)
# Second pass: adjust node positions based on connections
for node in level_nodes:
# Get this node's connections
var connections_above = node_connections[node.id].above
var connections_below = node_connections[node.id].below
# Calculate "pull" forces from connected nodes
var pull_x = 0.0
var total_pull = 0.0
# Look at connections to nodes above
for conn_node in connections_above:
pull_x += conn_node.position.x
total_pull += 1.0
# Look at connections to nodes below
for conn_node in connections_below:
pull_x += conn_node.position.x
total_pull += 1.0
# Adjust position based on pull if there are connections
if total_pull > 0:
# Calculate ideal x position (average of connected nodes)
var ideal_x = pull_x / total_pull
# Move slightly toward ideal position (don't move fully to maintain ordering)
node.position.x = node.position.x * 0.7 + ideal_x * 0.3
# Ensure minimum spacing between nodes
var min_spacing = 1.0 # Minimum horizontal distance between nodes
for i in range(1, level_nodes.size()):
var prev_node = level_nodes[i-1]
var curr_node = level_nodes[i]
# Check if nodes are too close
if curr_node.position.x - prev_node.position.x < min_spacing:
# Shift this node and all subsequent nodes to the right
var shift = min_spacing - (curr_node.position.x - prev_node.position.x)
for j in range(i, level_nodes.size()):
level_nodes[j].position.x += shift
return nodes

View file

@ -21,7 +21,6 @@ const NODE_SPACING_X = 150
const NODE_SPACING_Y = 120 const NODE_SPACING_Y = 120
const LINE_WIDTH = 3 const LINE_WIDTH = 3
const LINE_COLOR = Color(0.2, 0.2, 0.2) const LINE_COLOR = Color(0.2, 0.2, 0.2)
const LINE_COLOR_CRIT = Color(0.9, 0.8, 0.2, 0.8)
const LINE_COLOR_SELECTED = Color(0.2, 0.6, 0.2) const LINE_COLOR_SELECTED = Color(0.2, 0.6, 0.2)
const LINE_COLOR_ACCESSIBLE = Color(0.6, 0.6, 0.2) const LINE_COLOR_ACCESSIBLE = Color(0.6, 0.6, 0.2)
@ -189,7 +188,6 @@ func display_map():
var map_height = max_y - min_y + NODE_SIZE.y * 2 + SCROLL_PADDING_TOP + SCROLL_PADDING_BOTTOM var map_height = max_y - min_y + NODE_SIZE.y * 2 + SCROLL_PADDING_TOP + SCROLL_PADDING_BOTTOM
map_container.custom_minimum_size = Vector2(map_width, map_height) map_container.custom_minimum_size = Vector2(map_width, map_height)
# Create padding nodes
# Create a padding node at the top # Create a padding node at the top
var top_padding = Control.new() var top_padding = Control.new()
top_padding.custom_minimum_size = Vector2(map_width, SCROLL_PADDING_TOP) top_padding.custom_minimum_size = Vector2(map_width, SCROLL_PADDING_TOP)
@ -213,46 +211,24 @@ func display_map():
right_padding.position = Vector2(map_width - SCROLL_PADDING_RIGHT, 0) right_padding.position = Vector2(map_width - SCROLL_PADDING_RIGHT, 0)
map_container.add_child(right_padding) map_container.add_child(right_padding)
# Find the optimal path from start to end for highlighting # Draw connections first (so they're behind nodes)
var critical_path = find_unique_path_to_end()
var critical_connections = []
# Convert path nodes to connections # Modify your display_map function to use the curved connections
if critical_path.size() > 1: # Replace your loop that draws connections with this:
for i in range(critical_path.size() - 1):
critical_connections.append({
"from": critical_path[i],
"to": critical_path[i + 1]
})
# First draw non-critical connections (so critical ones are on top) # Draw connections first (so they're behind nodes)
for connection in map_connections: for connection in map_connections:
var is_critical = false
for crit_conn in critical_connections:
if connection.from == crit_conn.from and connection.to == crit_conn.to:
is_critical = true
break
if not is_critical:
var from_node = get_node_by_id(connection.from) var from_node = get_node_by_id(connection.from)
var to_node = get_node_by_id(connection.to) var to_node = get_node_by_id(connection.to)
if from_node and to_node: if from_node and to_node:
draw_curved_connection(from_node, to_node, false) draw_curved_connection(from_node, to_node)
# Then draw critical connections on top
for crit_conn in critical_connections:
var from_node = get_node_by_id(crit_conn.from)
var to_node = get_node_by_id(crit_conn.to)
if from_node and to_node:
draw_curved_connection(from_node, to_node, true)
# Draw nodes # Draw nodes
for node_data in map_nodes: for node_data in map_nodes:
draw_node(node_data) draw_node(node_data)
func get_node_by_id(id): func get_node_by_id(id):
for node in map_nodes: for node in map_nodes:
if node.id == id: if node.id == id:
@ -365,12 +341,11 @@ func _on_back_button_pressed():
emit_signal("back_pressed") emit_signal("back_pressed")
visible = false visible = false
func draw_curved_connection(from_node, to_node, isCritPath):
func draw_curved_connection(from_node, to_node):
var line = Line2D.new() var line = Line2D.new()
line.width = LINE_WIDTH line.width = LINE_WIDTH
line.default_color = LINE_COLOR line.default_color = LINE_COLOR
# if isCritPath:
# line.default_color = LINE_COLOR_CRIT
if from_node.id == current_node.id: if from_node.id == current_node.id:
line.default_color = LINE_COLOR_ACCESSIBLE line.default_color = LINE_COLOR_ACCESSIBLE
elif traversed_map.has(to_node.id) and (traversed_map.has(from_node.id) || from_node.id == 0): elif traversed_map.has(to_node.id) and (traversed_map.has(from_node.id) || from_node.id == 0):
@ -380,92 +355,42 @@ func draw_curved_connection(from_node, to_node, isCritPath):
var start_pos = from_node.position * Vector2(NODE_SPACING_X, NODE_SPACING_Y) + Vector2(SCROLL_PADDING_LEFT, SCROLL_PADDING_TOP) var start_pos = from_node.position * Vector2(NODE_SPACING_X, NODE_SPACING_Y) + Vector2(SCROLL_PADDING_LEFT, SCROLL_PADDING_TOP)
var end_pos = to_node.position * Vector2(NODE_SPACING_X, NODE_SPACING_Y) + Vector2(SCROLL_PADDING_LEFT, SCROLL_PADDING_TOP) var end_pos = to_node.position * Vector2(NODE_SPACING_X, NODE_SPACING_Y) + Vector2(SCROLL_PADDING_LEFT, SCROLL_PADDING_TOP)
# Find all connections from the same starting node to help with path separation
var sibling_connections = []
for conn in map_connections:
if conn.from == from_node.id and conn.to != to_node.id:
var other_node = get_node_by_id(conn.to)
if other_node:
sibling_connections.append(other_node)
# Count how many connections this source node has
var connection_count = sibling_connections.size() + 1 # +1 for this connection
# Determine position of this connection among siblings (for consistent curve offsets)
var connection_index = 0
for i in range(sibling_connections.size()):
if sibling_connections[i].id < to_node.id:
connection_index += 1
# Create a bezier curve path # Create a bezier curve path
var points = [] var points = []
# Calculate direction vector between nodes # If nodes are on different levels (y positions)
var direction = (end_pos - start_pos).normalized()
var perp_direction = Vector2(-direction.y, direction.x) # Perpendicular to direction
# Different curve handling based on node positions
if from_node.position.y != to_node.position.y: if from_node.position.y != to_node.position.y:
# Vertical/diagonal connection between different levels # Calculate control points for a curve
var mid_y = (start_pos.y + end_pos.y) / 2
var control_offset = NODE_SPACING_Y * 0.5 * (1 + abs(from_node.position.x - to_node.position.x) * 0.2)
# Calculate curve offset based on connection count and index var control1 = Vector2(start_pos.x, start_pos.y + control_offset)
var curve_offset = 0.0 var control2 = Vector2(end_pos.x, end_pos.y - control_offset)
if connection_count > 1:
# Calculate spacing between -0.5 and 0.5
var spacing = 1.0 / (connection_count + 1)
curve_offset = (connection_index + 1) * spacing - 0.5
curve_offset *= NODE_SPACING_X * 0.8 # Scale by node spacing
# Generate control points with horizontal offset # Add more points for a smoother curve
var dist = (end_pos - start_pos).length() for i in range(11): # 0.0, 0.1, 0.2, ..., 1.0
var mid_point = start_pos + (end_pos - start_pos) * 0.5 var t = i / 10.0
# Apply the offset perpendicular to the path direction
mid_point += perp_direction * curve_offset
# Calculate control points with vertical offsets
var control1 = start_pos + direction * (dist * 0.25) + perp_direction * curve_offset * 1.5
var control2 = end_pos - direction * (dist * 0.25) + perp_direction * curve_offset * 1.5
# Generate points for a cubic bezier curve
for i in range(21): # More points for a smoother curve
var t = i / 20.0
var point = cubic_bezier(start_pos, control1, control2, end_pos, t) var point = cubic_bezier(start_pos, control1, control2, end_pos, t)
points.append(point) points.append(point)
else: else:
# Horizontal connection (nodes at same level) # For nodes on the same level, use a simple curved path
var curve_height = NODE_SPACING_Y * 0.4 # Base curve height
# Adjust curve height based on connection position
if connection_count > 1:
curve_height *= (1.0 + 0.3 * connection_index)
# Determine curve direction (up or down)
var curve_up = true
# If nodes are further apart horizontally, make a higher curve
var x_distance = abs(from_node.position.x - to_node.position.x)
if x_distance > 1:
curve_height *= (1.0 + 0.2 * x_distance)
# Calculate mid point with vertical offset
var mid_x = (start_pos.x + end_pos.x) / 2 var mid_x = (start_pos.x + end_pos.x) / 2
var mid_y = null var mid_y = start_pos.y
if curve_up: var curve_height = NODE_SPACING_Y * 0.3 * (1 + abs(from_node.position.x - to_node.position.x) * 0.1)
mid_y = start_pos - curve_height
else : if to_node.position.x > from_node.position.x:
mid_y = start_pos - (-curve_height) mid_y -= curve_height
else:
mid_y += curve_height
var mid_point = Vector2(mid_x, mid_y) var mid_point = Vector2(mid_x, mid_y)
# Generate points for a quadratic bezier curve # Create a quadratic bezier curve with just 3 points
for i in range(21): for i in range(11):
var t = i / 20.0 var t = i / 10.0
var point = quadratic_bezier(start_pos, mid_point, end_pos, t) var point = quadratic_bezier(start_pos, mid_point, end_pos, t)
points.append(point) points.append(point)
# Apply anti-aliasing
line.antialiased = true
# Add the points to the line # Add the points to the line
for point in points: for point in points:
line.add_point(point) line.add_point(point)
@ -474,50 +399,6 @@ func draw_curved_connection(from_node, to_node, isCritPath):
map_container.add_child(line) map_container.add_child(line)
connection_lines.append(line) connection_lines.append(line)
func find_unique_path_to_end():
# Find start and end nodes
var start_node = null
var end_node = null
for node in map_nodes:
if node.type == RoomType.STARTING:
start_node = node
elif node.type == RoomType.FINAL:
end_node = node
if not start_node or not end_node:
return []
# Build a graph representation
var graph = {}
for node in map_nodes:
graph[node.id] = []
for conn in map_connections:
if graph.has(conn.from):
graph[conn.from].append(conn.to)
# Use BFS to find the shortest path
var queue = [[start_node.id]] # Queue of paths
var visited = {start_node.id: true}
while queue.size() > 0:
var path = queue.pop_front()
var node_id = path[path.size() - 1]
if node_id == end_node.id:
return path # Return the full path
if graph.has(node_id):
for neighbor in graph[node_id]:
if not visited.has(neighbor):
visited[neighbor] = true
var new_path = path.duplicate()
new_path.append(neighbor)
queue.append(new_path)
return [] # No path found
# Helper function for cubic bezier curve calculation # Helper function for cubic bezier curve calculation
func cubic_bezier(p0: Vector2, p1: Vector2, p2: Vector2, p3: Vector2, t: float) -> Vector2: func cubic_bezier(p0: Vector2, p1: Vector2, p2: Vector2, p3: Vector2, t: float) -> Vector2:
var q0 = p0.lerp(p1, t) var q0 = p0.lerp(p1, t)