godotengine / godotengine/godot-docs
AStar2D and AStar3D documentation says weights can be 0.0 or greater, but does not mention that weights below 1.0 may result in sub-optimal paths.
- Dominant language
- reStructuredText
- Stars
- 5.7k
- Forks
- 3.8k
- Avg merge
- 1d 20h
- Merged PRs (30d)
- 25
Description
v4.5.1 (but I think this goes back to v4.0)
To find a shortest path, the A* algorithm relies on a heuristic which never overestimates.
In Godot this heuristic is _estimate_cost(). Actual distances for connected points are calculated by _compute_cost(), but are then multiplied by weight. Unless _estimate_cost() is overridden with an implementation which in some way compensates for the existence of weights below 1.0 it will therefore be capable of over-estimating distances.
I'd consider raising this as a bug in Godot itself, but the PR that originally changed the minimum from 1.0 to 0.0 did have use cases for weights less than 1 (such as teleporters). Using these weights will sometimes result in sub-optimal paths, but some may consider sub-optimal pathing an acceptable compromise for allowing eg. teleporters to exist in the graph. Others may find clever heuristic functions to deal with the problem.
At the very least, I do think this needs addressing in the documentation. It is misleading to those without pre-existing knowledge of the algorithm, and confusing to those who have it ("why does it allow 0, are they doing something special under the hood?"). It might even be worth mentioning that very high weights can cause the entire graph to be evaluated as well. I'm not sure exactly what it should say but thought I'd open this for discussion.
Relevant pages:
https://docs.godotengine.org/en/stable/classes/class_astar2d.html
https://docs.godotengine.org/en/stable/classes/class_astar3d.html
A (slightly contrived) example:
```gdscript
class_name Test
extends Node
enum PointName {
START,
GOAL,
STRAIGHT_AHEAD,
PRE_WEIGHTLESS,
WEIGHTLESS
}
var pathfinder := AStar2D.new();
func _ready() -> void:
# start at 0 and end point at 10
pathfinder.add_point(PointName.START, Vector2.ZERO);
pathfinder.add_point(PointName.GOAL, Vector2(0, 10));
# first path
# a single point which is halfway there at 5
pathfinder.add_point(PointName.STRAIGHT_AHEAD, Vector2(0, 5));
# second path
# a point 1 away from start, in the wrong direction
pathfinder.add_point(PointName.PRE_WEIGHTLESS, Vector2(0, -1));
# and another point 4 away from goal, but with a weight of 0.0
pathfinder.add_point(PointName.WEIGHTLESS, Vector2(0, 6), 0.0);
# connect start -> straight ahead -> goal
# the cost should be 5 + 5 = 10
pathfinder.connect_points(PointName.START, PointName.STRAIGHT_AHEAD);
pathfinder.connect_points(PointName.STRAIGHT_AHEAD, PointName.GOAL);
# connect start -> pre-weightless -> weightless -> goal
# the cost should be 1 + 0 + 4 = 5
pathfinder.connect_points(PointName.START, PointName.PRE_WEIGHTLESS);
pathfinder.connect_points(PointName.PRE_WEIGHTLESS, PointName.WEIGHTLESS);
pathfinder.connect_points(PointName.WEIGHTLESS, PointName.GOAL);
print("Finding shortest route...");
__test_path(PointName.START, PointName.GOAL); # Says: START, STRAIGHT_AHEAD, GOAL
print("And on the way back...")
__test_path(PointName.GOAL, PointName.START); # Says: GOAL, WEIGHTLESS, PRE_WEIGHTLESS, START
func __test_path(a: int, b: int) -> void:
# get the path
var path := pathfinder.get_id_path(a, b);
# write it out
for point in path:
print(PointName.find_key(point));
print();
```
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.