perf: weight-aware A* heuristic + tamed road repulsion (route-preserving speed fix)
A* was searching almost the whole island for every road. Godot's stock estimate-to-goal is plain straight-line distance, which assumes every step costs 1 — but a step through mountains costs up to 401 and a step near an existing road cost 10,001. With the estimate that far below reality, A* cannot rule anything out, so it degenerates toward Dijkstra. Two changes, both aimed at that: 1. RoadPathGrid overrides the estimate to scale it by HEURISTIC_WEIGHT (3.0), i.e. weighted A*. Paths may be up to 3x costlier than the theoretical best in exchange for exploring far less. This does NOT push roads over mountains: a ridge costs hundreds of times more than going around, which a 3x bias nowhere near pays for. 2. ROAD_REPULSION_PENALTY replaces the hardcoded +10000 with 250. The old value made ground near a road effectively infinite, and it compounded — each road drawn made the next search slower. 250 still strongly discourages roads from running alongside each other. Terrain weights are untouched: the mountain curve (1 + elevation^3 * 400), beach and wasteland costs are exactly as before, so mountains remain expensive and roads still avoid them. Routing, tiers, smoothing and grid resolution are also unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
42f04139a2
commit
1f7613439a
1 changed files with 59 additions and 3 deletions
|
|
@ -625,7 +625,9 @@ public partial class MapGenerator : TextureRect
|
||||||
GD.Print($"[A*] Starting Logistics Pathfinding for {MapSize}x{MapSize} world...");
|
GD.Print($"[A*] Starting Logistics Pathfinding for {MapSize}x{MapSize} world...");
|
||||||
_highwayPaths.Clear(); _branchPaths.Clear(); _ruggedPaths.Clear(); _trailPaths.Clear();
|
_highwayPaths.Clear(); _branchPaths.Clear(); _ruggedPaths.Clear(); _trailPaths.Clear();
|
||||||
|
|
||||||
AStarGrid2D astar = new AStarGrid2D();
|
// RoadPathGrid is a stock AStarGrid2D with a scaled-up distance estimate, so the
|
||||||
|
// search stops flood-filling the island for every road. See the class for why.
|
||||||
|
AStarGrid2D astar = new RoadPathGrid();
|
||||||
astar.Region = new Rect2I(0, 0, MapSize, MapSize);
|
astar.Region = new Rect2I(0, 0, MapSize, MapSize);
|
||||||
astar.CellSize = new Vector2I(1, 1);
|
astar.CellSize = new Vector2I(1, 1);
|
||||||
astar.DiagonalMode = AStarGrid2D.DiagonalModeEnum.OnlyIfNoObstacles;
|
astar.DiagonalMode = AStarGrid2D.DiagonalModeEnum.OnlyIfNoObstacles;
|
||||||
|
|
@ -836,6 +838,23 @@ public partial class MapGenerator : TextureRect
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How much extra a step costs near an already-drawn road. This is what stops new
|
||||||
|
/// roads from lying on top of old ones — it is SEPARATION, not terrain avoidance,
|
||||||
|
/// so it is unrelated to the mountain/beach/wasteland weights.
|
||||||
|
///
|
||||||
|
/// It used to be 10,000, which was far past "discourage" — a normal step costs 1,
|
||||||
|
/// so it made the ground near a road effectively infinite. That wrecked the search:
|
||||||
|
/// every later road had to weigh routes costing tens of thousands, which is exactly
|
||||||
|
/// the situation A* handles by giving up on being clever and searching everything.
|
||||||
|
/// Worse, it compounded — each road drawn made the next one slower.
|
||||||
|
///
|
||||||
|
/// 250 still means "strongly prefer not to run alongside an existing road" (250x a
|
||||||
|
/// normal step) without dwarfing the search. Raise it if parallel roads crowd
|
||||||
|
/// together; lower it if roads take silly detours to avoid each other.
|
||||||
|
/// </summary>
|
||||||
|
private const float ROAD_REPULSION_PENALTY = 250f;
|
||||||
|
|
||||||
private void PenalizePath(AStarGrid2D astar, Vector2[] path, int radius)
|
private void PenalizePath(AStarGrid2D astar, Vector2[] path, int radius)
|
||||||
{
|
{
|
||||||
int step = Mathf.Max(1, radius / 2);
|
int step = Mathf.Max(1, radius / 2);
|
||||||
|
|
@ -847,8 +866,7 @@ public partial class MapGenerator : TextureRect
|
||||||
int ny = Mathf.Clamp((int)p.Y + dy, 0, MapSize - 1);
|
int ny = Mathf.Clamp((int)p.Y + dy, 0, MapSize - 1);
|
||||||
var cell = new Vector2I(nx, ny);
|
var cell = new Vector2I(nx, ny);
|
||||||
if (!astar.IsPointSolid(cell)) {
|
if (!astar.IsPointSolid(cell)) {
|
||||||
// The true Iron Curtain
|
astar.SetPointWeightScale(cell, astar.GetPointWeightScale(cell) + ROAD_REPULSION_PENALTY);
|
||||||
astar.SetPointWeightScale(cell, astar.GetPointWeightScale(cell) + 10000f);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -950,6 +968,44 @@ public partial class MapGenerator : TextureRect
|
||||||
}
|
}
|
||||||
|
|
||||||
// Place this at the very bottom of the file!
|
// Place this at the very bottom of the file!
|
||||||
|
/// <summary>
|
||||||
|
/// The road pathfinder's grid, with one change from stock: a scaled-up estimate.
|
||||||
|
///
|
||||||
|
/// WHY THIS EXISTS. A* explores outward from the start until the cheapest route it
|
||||||
|
/// has found is provably the best. It decides how far to keep looking by comparing
|
||||||
|
/// what a route has cost so far against an ESTIMATE of what remains. Godot's stock
|
||||||
|
/// estimate is plain straight-line distance — it assumes every step costs 1.
|
||||||
|
///
|
||||||
|
/// Our steps don't cost 1. A step through mountains costs up to 401, and a step near
|
||||||
|
/// an existing road used to cost 10,001. So the estimate was wildly optimistic about
|
||||||
|
/// the remaining journey, and A* responded the only way it can: by refusing to rule
|
||||||
|
/// anything out and searching almost the whole island for every single road.
|
||||||
|
///
|
||||||
|
/// Multiplying the estimate makes the search commit to a direction sooner. It's a
|
||||||
|
/// well-known trade ("weighted A*"): paths are allowed to be up to HEURISTIC_WEIGHT
|
||||||
|
/// times more expensive than the theoretical best, in exchange for exploring far less.
|
||||||
|
/// We want believable roads, not provably-optimal ones, so that's a good trade.
|
||||||
|
///
|
||||||
|
/// It does NOT make roads climb mountains. Crossing a ridge costs hundreds of times
|
||||||
|
/// more than walking around it — a 3x bias nowhere near pays for that.
|
||||||
|
/// </summary>
|
||||||
|
public partial class RoadPathGrid : AStarGrid2D
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// How strongly the search commits toward the goal.
|
||||||
|
/// 1.0 = stock behaviour: guaranteed-shortest, explores enormously.
|
||||||
|
/// 3.0 = default: still routes sensibly around terrain, explores far less.
|
||||||
|
/// higher = faster still, but paths get progressively less considered.
|
||||||
|
/// Raise it if generation is still slow; lower it if roads start looking careless.
|
||||||
|
/// </summary>
|
||||||
|
public const float HEURISTIC_WEIGHT = 3.0f;
|
||||||
|
|
||||||
|
public override float _EstimateCost(Vector2I fromId, Vector2I toId)
|
||||||
|
{
|
||||||
|
return fromId.DistanceTo(toId) * HEURISTIC_WEIGHT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public partial class MapDrawProxy : Control
|
public partial class MapDrawProxy : Control
|
||||||
{
|
{
|
||||||
public MapGenerator Source;
|
public MapGenerator Source;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue