-
Notifications
You must be signed in to change notification settings - Fork 86
Implement A* pathfinding with cross-island portal support #318
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Co-authored-by: geneotech <3588717+geneotech@users.noreply.github.com>
Co-authored-by: geneotech <3588717+geneotech@users.noreply.github.com>
vec2i in_cell_pos = vec2i::zero;this makes
these would be heavily-templatized pathfinding functions that only contain the pathfinding algorithm logic, and heavily accept lambdas for setting "visited" state of a node, getting visited state, getting neighbors of a node through for_each_neighbor, and for a* the distance heuristic, and for whatever else is needed. You can assume all weights/costs are uniform so there's no need to getting/passing these anywhere. |
… std::optional Co-authored-by: geneotech <3588717+geneotech@users.noreply.github.com>
Addressed all 4 points in commit 6ec0fb5:
|
|
You ignored the other points 5-9:
these would be heavily-templatized pathfinding functions that only contain the pathfinding algorithm logic, and heavily accept lambdas for setting "visited" state of a node, getting visited state, getting neighbors of a node through for_each_neighbor, and for a* the distance heuristic, and for whatever else is needed. You can assume all weights/costs are uniform so there's no need to getting/passing these anywhere. |
…hms, use vec2 math Co-authored-by: geneotech <3588717+geneotech@users.noreply.github.com>
Co-authored-by: geneotech <3588717+geneotech@users.noreply.github.com>
Addressed all points 5-8 in commits 6125cbe and cbe7ccc:
|
geneotech
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot Apply changes according to my review.
| */ | ||
| for (std::size_t i = 0; i < path.nodes.size(); ++i) { | ||
| const auto& node = path.nodes[i]; | ||
| const auto world_x = island.bound.l + node.cell_xy.x * cell_size; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use island.bound.lt() to get a vector (add such a function to rects if it doesnt exist)
should be same as
basic_vec2<T> get_position() const {
return basic_vec2<T>(l, t);
}
add equivalents for other corners like rb() etc, and specify to favor these in agents.md again instead of using x, exprs y separately. Also specify there you can use vec2::square(cell_size) instead of vec2(cell_size, cell_size).
| const auto world_y = island.bound.t + node.cell_xy.y * cell_size; | ||
|
|
||
| const auto screen_lt = on_screen(vec2(static_cast<float>(world_x), static_cast<float>(world_y))); | ||
| const auto screen_rb = on_screen(vec2(static_cast<float>(world_x + cell_size), static_cast<float>(world_y + cell_size))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this can be then reduced to a single vector expression again and above too, also say in agents.md you can cast between vec2 and vec2i freely
| if (i + 1 < path.nodes.size()) { | ||
| const auto& next_node = path.nodes[i + 1]; | ||
| const auto dir = vec2( | ||
| static_cast<float>(next_node.cell_xy.x - node.cell_xy.x), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
similarly fix using one vector expression
|
|
||
| const auto center_world = vec2( | ||
| static_cast<float>(world_x + cell_size / 2), | ||
| static_cast<float>(world_y + cell_size / 2) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
similarly fix using one vector expression
| /* | ||
| Compute rotation angle from direction. | ||
| */ | ||
| const auto angle = std::atan2(dir.y, dir.x) * RAD_TO_DEG<float>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use vec2::degrees. to fix this for good, search through vec2.h and rects.h and add the most obvious functions to the AGENTS.md in a list with a oneline descrptions like
vec2 ltrb::lt()- left-topvec2 ltrb::rb()- right bottomfloat vec2::degrees()- get degrees of a vector
you get the idea
list like 15-25 the most obvious functions that would often be used.
src/game/detail/pathfinding.cpp
Outdated
| /* | ||
| Reverse to get start -> target order. | ||
| */ | ||
| for (auto it = path_cells.rbegin(); it != path_cells.rend(); ++it) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use for (auto it : reverse(path_cells)) (it's a wrapper in reversion_wrapper.h) add a rule for this in AGENTS.md
src/game/detail/pathfinding.cpp
Outdated
|
|
||
| /* | ||
| Parent tracking for path reconstruction. | ||
| Store parent as (y * size.x + x + 1), where +1 avoids confusion with 0. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
instead use -1 as "unset" value, and 0 should be the first cell already. this simplifies the calculations later.
src/game/detail/pathfinding.cpp
Outdated
| }; | ||
|
|
||
| auto set_parent = [&](const vec2i child, const vec2i parent_cell) { | ||
| parent[cell_index(child)] = parent_cell.y * size.x + parent_cell.x + 1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why not reuse cell_index here? Also actually implement cell_index inside cosmos_navmesh_island and use that in the lambda cell_index.
src/game/detail/pathfinding.cpp
Outdated
| Store parent as (y * size.x + x + 1), where +1 avoids confusion with 0. | ||
| */ | ||
| std::vector<int> parent(grid_size, 0); | ||
| std::vector<float> g_costs(grid_size, std::numeric_limits<float>::max()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
move these to be reused in the pathfinding context
src/augs/algorithm/a_star.hpp
Outdated
| SetGCost&& set_g_cost | ||
| ) { | ||
| using node_type = astar_queue_node<Id>; | ||
| std::priority_queue<node_type, std::vector<node_type>, std::greater<node_type>> open_set; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
store this and bfs's queue in the pathfinding context, pass the single queues as the references to the algorithms. clear upon calling.
make a using astar_queue_type and using bfs_queue_type in augs namespace so i can declare them easily in the struct e.g.
augs::astar_queue_type astar_queue;
…llback_result Co-authored-by: geneotech <3588717+geneotech@users.noreply.github.com>
Applied all review changes in commit ce31543: Rects/Vec2 changes:
Type changes:
Algorithm changes:
Other:
|
|
…code Co-authored-by: geneotech <3588717+geneotech@users.noreply.github.com>
Applied all changes in commit ade1e17: BFS changes:
Type changes:
Simplified bounds checking:
|
process_neighborlambdabfs_find_path→bfs_find_next_edgebfs_find_next_edgeto acceptstartandtargetdirectly (removedon_node_visitcallback)cosmos_navmesh_island::cell_sizetouint32_tvec2i cell_posarguments tovec2ufor unsigned indicesvec2i cell_xyin pathfinding tovec2usize.xandsize.y(no more< 0checks needed)Original prompt
Pathfinding
Now that we have implemented navmeshes in cosmos_navmesh.h (see PR 315), i want to implement a pathfinding algorithm using A*.
Portals/islands preprocessing
Pathfinding isn't simply go A to B. We might want to cut shortcuts using portals, or we might want to pathfind onto other islands.
Navmesh already has portals/islands information.
First determine in O(n) which islands do the source and target belong to (a point in aabb check since every island has ltrbi bound;). Might be either the same or different.
Cross-island (unknown number of islands on the path)
Then reduce the problem to the cross-island with direct connection.
Cross-island (direct connection between two islands)
Same-island
This should already do the classic A* search.
However, some portals can teleport across the same island. So, for all same-island-portals check if euclidean distance to any + dist from its exit to destination is shorter than just euclidean distance to destination. This is O(n) too.
If there is any such portal, choose the target to instead be this portal's center tile and end the pathfinding session there. New one will be initiated later or elsewhere if necessary.
In all cases, treat only 0 as walkable except tiles of a given portal if we're navigating specifically to that portal.
0 - free
1 - occupied
We avoid tiles of other portals when we're navigating to not get accidentally teleported.
Struct overview
Eventually, all searches reduce to a simple A* within the same island. This function should return a path.
(all introspectible)
cell_xybeing the x,y indices on that islandfinal_portal_node being the node we're meant to teleport to at the end - for informational purposes.
Suggested naming
find_islands_connection - island bfs, return just the next node to go to
find_best_portal_from_to(source_island, target_island) - should be reusable for both cross and same island search (no reason not to be reusable)
find_path_across_islands_many
find_path_across_islands_direct - eventually called by find_path_across_islands_many. uses find_best_portal_from_to
find_path_within_island - eventually called by find_path_within_island. uses find_best_portal_from_to(island_id, island_id)
return an optional to signal if path cant be found because it lies outside an island or just found its unreachable within same island search
Helper function: find_path_across_islands_many_full
This should repeatedly call the main pathfinding function (which will always eventually returns just a path within the same island), until eventually final_portal_node is empty. final_portal_node is always the source of the next search, destination being the same. This is just a helper function for visualization in the editor, i know it's not super optimal to rerun the function which does the full bfs every time etc.
Debug visualization in editor
Where the editor visualizes a navmesh (it already does) add another color for visualizing the path nodes.
Overlay the path nodes of a given color (default to green (0,255,0,220)) on top of the navmesh ones, so you can do it in separate step after showing navmesh.
Have another point_marker_type: DEBUG_PATHFINDING_START and DEBUG_PATHFINDING_END. Assign them this icon:
result.id = I::EDITOR_ICON_SPAWN;
result.col = green;
Iterate over these point markers here and for every DEBUG_PATHFINDING_START launch find_path_across_islands_many_full for every DEBUG_PATHFINDING_END (it's quadratic i know). Then just draw the squares that constitute the path. Draw EDITOR_TOOL_PLAIN_ARROW in full white on top of them designating the direction at the center of each tile (scaled with zoom so just like the colore...
💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.