""" A converter between hex and Godot-space coordinate systems. The hex grid uses +x => NE and +y => N, whereas the projection to Godot-space uses +x => E, +y => S. We map hex coordinates to Godot-space with +y flipped to be the down vector so that it maps neatly to both Godot's 2D coordinate system, and also to x,z planes in 3D space. ## Usage: #### var hex_scale = Vector2(...) If you want your hexes to display larger than the default 1 x 0.866 units, then you can customise the scale of the hexes using this property. #### func get_hex_center(hex) Returns the Godot-space Vector2 of the center of the given hex. The coordinates can be given as either a HexCell instance; a Vector3 cube coordinate, or a Vector2 axial coordinate. #### func get_hex_center3(hex [, y]) Returns the Godot-space Vector3 of the center of the given hex. The coordinates can be given as either a HexCell instance; a Vector3 cube coordinate, or a Vector2 axial coordinate. If a second parameter is given, it will be used for the y value in the returned Vector3. Otherwise, the y value will be 0. #### func get_hex_at(coords) Returns HexCell whose grid position contains the given Godot-space coordinates. The given value can either be a Vector2 on the grid's plane or a Vector3, in which case its (x, z) coordinates will be used. ### Path-finding HexGrid also includes an implementation of the A* pathfinding algorithm. The class can be used to populate an internal representation of a game grid with obstacles to traverse. #### func set_bounds(min_coords, max_coords) Sets the hard outer limits of the path-finding grid. The coordinates given are the min and max corners *inside* a bounding square (diamond in hex visualisation) region. Any hex outside that area is considered an impassable obstacle. The default bounds consider only the origin to be inside, so you're probably going to want to do something about that. #### func get_obstacles() Returns a dict of all obstacles and their costs The keys are Vector2s of the axial coordinates, the values will be the cost value. Zero cost means an impassable obstacle. #### func add_obstacles(coords, cost=0) Adds one or more obstacles to the path-finding grid The given coordinates (axial or cube), HexCell instance, or array thereof, will be added as path-finding obstacles with the given cost. A zero cost indicates an impassable obstacle. #### func remove_obstacles(coords) Removes one or more obstacles from the path-finding grid The given coordinates (axial or cube), HexCell instance, or array thereof, will be removed as obstacles from the path-finding grid. #### func get_barriers() Returns a dict of all barriers in the grid. A barrier is an edge of a hex which is either impassable, or has a non-zero cost to traverse. If two adjacent hexes both have barriers on their shared edge, their costs are summed. Barrier costs are in addition to the obstacle (or default) cost of moving to a hex. The outer dict is a mapping of axial coords to an inner barrier dict. The inner dict maps between HexCell.DIR_* directions and the cost of travel in that direction. A cost of zero indicates an impassable barrier. #### func add_barriers(coords, dirs, cost=0) Adds one or more barriers to locations on the grid. The given coordinates (axial or cube), HexCell instance, or array thereof, will have path-finding barriers added in the given HexCell.DIR_* directions with the given cost. A zero cost indicates an impassable obstacle. Existing barriers at given coordinates will not be removed, but will be overridden if the direction is specified. #### func remove_barriers(coords, dirs=null) Remove one or more barriers from the path-finding grid. The given coordinates (axial or cube), HexCell instance, or array thereof, will have the path-finding barriers in the supplied HexCell.DIR_* directions removed. If no direction is specified, all barriers for the given coordinates will be removed. #### func get_hex_cost(coords) Returns the cost of moving into the specified grid position. Will return 0 if the given grid position is inaccessible. #### func get_move_cost(coords, direction) Returns the cost of moving from one hex to an adjacent one. This method takes into account any barriers defined between the two hexes, as well as the cost of the target hex. Will return 0 if the target hex is inaccessible, or if there is an impassable barrier between the hexes. The direction should be provided as one of the HexCell.DIR_* values. #### func find_path(start, goal, exceptions=[]) Calculates an A* path from the start to the goal. Returns a list of HexCell instances charting the path from the given start coordinates to the goal, including both ends of the journey. Exceptions can be specified as the third parameter, and will act as impassable obstacles for the purposes of this call of the function. This can be used for pathing around obstacles which may change position (eg. enemy playing pieces), without having to update the grid's list of obstacles every time something moves. If the goal is an impassable location, the path will terminate at the nearest adjacent coordinate. In this instance, the goal hex will not be included in the returned array. If there is no path possible to the goal, or any hex adjacent to it, an empty array is returned. But the algorithm will only know that once it's visited every tile it can reach, so try not to path to the impossible. """ extends Reference var HexCell = preload("./HexCell.gd") # Duplicate these from HexCell for ease of access const DIR_N = Vector3(0, 1, -1) const DIR_NE = Vector3(1, 0, -1) const DIR_SE = Vector3(1, -1, 0) const DIR_S = Vector3(0, -1, 1) const DIR_SW = Vector3(-1, 0, 1) const DIR_NW = Vector3(-1, 1, 0) const DIR_ALL = [DIR_N, DIR_NE, DIR_SE, DIR_S, DIR_SW, DIR_NW] # Allow the user to scale the hex for fake perspective or somesuch export(Vector2) var hex_scale = Vector2(1, 1) setget set_hex_scale var base_hex_size = Vector2(1, sqrt(3)/2) var hex_size var hex_transform var hex_transform_inv # Pathfinding obstacles {Vector2: cost} # A zero cost means impassable var path_obstacles = {} # Barriers restrict traversing between edges (in either direction) # costs for barriers and obstacles are cumulative, but impassable is impassable # {Vector2: {DIR_VECTOR2: cost, ...}} var path_barriers = {} var path_bounds = Rect2() var path_cost_default = 1.0 func _init(): set_hex_scale(hex_scale) func set_hex_scale(scale): # We need to recalculate some stuff when projection scale changes hex_scale = scale hex_size = base_hex_size * hex_scale hex_transform = Transform2D( Vector2(hex_size.x * 3/4, -hex_size.y / 2), Vector2(0, -hex_size.y), Vector2(0, 0) ) hex_transform_inv = hex_transform.affine_inverse() """ Converting between hex-grid and 2D spatial coordinates """ func get_hex_center(hex): # Returns hex's centre position on the projection plane hex = HexCell.new(hex) return hex_transform * hex.axial_coords func get_hex_at(coords): # Returns a HexCell at the given Vector2/3 on the projection plane # If the given value is a Vector3, its x,z coords will be used if typeof(coords) == TYPE_VECTOR3: coords = Vector2(coords.x, coords.z) return HexCell.new(hex_transform_inv * coords) func get_hex_center3(hex, y=0): # Returns hex's centre position as a Vector3 var coords = get_hex_center(hex) return Vector3(coords.x, y, coords.y) """ Pathfinding Ref: https://www.redblobgames.com/pathfinding/a-star/introduction.html We use axial coords for everything internally (to use Rect2.has_point), but the methods accept cube or axial coords, or HexCell instances. """ func set_bounds(min_coords, max_coords): # Set the absolute bounds of the pathfinding area in grid coords # The given coords will be inside the boundary (hence the extra (1, 1)) min_coords = HexCell.new(min_coords).axial_coords max_coords = HexCell.new(max_coords).axial_coords path_bounds = Rect2(min_coords, (max_coords - min_coords) + Vector2(1, 1)) func get_obstacles(): return path_obstacles func add_obstacles(vals, cost=0): # Store the given coordinate/s as obstacles if not typeof(vals) == TYPE_ARRAY: vals = [vals] for coords in vals: coords = HexCell.new(coords).axial_coords path_obstacles[coords] = cost func remove_obstacles(vals): # Remove the given obstacle/s from the grid if not typeof(vals) == TYPE_ARRAY: vals = [vals] for coords in vals: coords = HexCell.new(coords).axial_coords path_obstacles.erase(coords) func get_barriers(): return path_barriers func add_barriers(vals, dirs, cost=0): # Store the given directions of the given locations as barriers if not typeof(vals) == TYPE_ARRAY: vals = [vals] if not typeof(dirs) == TYPE_ARRAY: dirs = [dirs] for coords in vals: coords = HexCell.new(coords).axial_coords var barriers = {} if coords in path_barriers: # Already something there barriers = path_barriers[coords] else: path_barriers[coords] = barriers # Set or override the given dirs for dir in dirs: barriers[dir] = cost path_barriers[coords] = barriers func remove_barriers(vals, dirs=null): if not typeof(vals) == TYPE_ARRAY: vals = [vals] if dirs != null and not typeof(dirs) == TYPE_ARRAY: dirs = [dirs] for coords in vals: coords = HexCell.new(coords).axial_coords if dirs == null: path_barriers.erase(coords) else: for dir in dirs: path_barriers[coords].erase(dir) func get_hex_cost(coords): # Returns the cost of moving to the given hex coords = HexCell.new(coords).axial_coords if coords in path_obstacles: return path_obstacles[coords] if not path_bounds.has_point(coords): # Out of bounds return 0 return path_cost_default func get_move_cost(coords, direction): # Returns the cost of moving from one hex to a neighbour direction = HexCell.new(direction).cube_coords var start_hex = HexCell.new(coords) var target_hex = HexCell.new(start_hex.cube_coords + direction) coords = start_hex.axial_coords # First check if either end is completely impassable var cost = get_hex_cost(start_hex) if cost == 0: return 0 cost = get_hex_cost(target_hex) if cost == 0: return 0 # Check for barriers var barrier_cost if coords in path_barriers and direction in path_barriers[coords]: barrier_cost = path_barriers[coords][direction] if barrier_cost == 0: return 0 cost += barrier_cost var target_coords = target_hex.axial_coords if target_coords in path_barriers and -direction in path_barriers[target_coords]: barrier_cost = path_barriers[target_coords][-direction] if barrier_cost == 0: return 0 cost += barrier_cost return cost func get_path(start, goal, exceptions=[]): # DEPRECATED! # The function `get_path` is used by Godot for something completely different, # so we renamed it here to `find_path`. push_warning("HexGrid.get_path has been deprecated, use find_path instead.") return find_path(start, goal, exceptions) func find_path(start, goal, exceptions=[]): # Light a starry path from the start to the goal, inclusive start = HexCell.new(start).axial_coords goal = HexCell.new(goal).axial_coords # Make sure all the exceptions are axial coords var exc = [] for ex in exceptions: exc.append(HexCell.new(ex).axial_coords) exceptions = exc # Now we begin the A* search var frontier = [make_priority_item(start, 0)] var came_from = {start: null} var cost_so_far = {start: 0} while not frontier.empty(): var current = frontier.pop_front().v if current == goal: break for next_hex in HexCell.new(current).get_all_adjacent(): var next = next_hex.axial_coords var next_cost = get_move_cost(current, next - current) if next == goal and (next in exceptions or get_hex_cost(next) == 0): # Our goal is an obstacle, but we're next to it # so our work here is done came_from[next] = current frontier.clear() break if not next_cost or next in exceptions: # We shall not pass continue next_cost += cost_so_far[current] if not next in cost_so_far or next_cost < cost_so_far[next]: # New shortest path to that node cost_so_far[next] = next_cost var priority = next_cost + next_hex.distance_to(goal) # Insert into the frontier var item = make_priority_item(next, priority) var idx = frontier.bsearch_custom(item, self, "comp_priority_item") frontier.insert(idx, item) came_from[next] = current if not goal in came_from: # Not found return [] # Follow the path back where we came_from var path = [] if not (get_hex_cost(goal) == 0 or goal in exceptions): # We only include the goal if it's traversable path.append(HexCell.new(goal)) var current = goal while current != start: current = came_from[current] path.push_front(HexCell.new(current)) return path # Used to make a priority queue out of an array func make_priority_item(val, priority): return {"v": val, "p": priority} func comp_priority_item(a, b): return a.p < b.p