Compare commits

..

No commits in common. "783ee8b016d267779ba17ab2b5caa684a0bd2196" and "d72ffc0183c63dfa5973488aa52dfcb98b6b00d4" have entirely different histories.

2 changed files with 13 additions and 83 deletions

View file

@ -45,17 +45,16 @@ Instead of writing 256 `if/else` statements, the algorithm uses a hardcoded **Lo
### 4. Linear Interpolation (Why it looks Smooth) ### 4. Linear Interpolation (Why it looks Smooth)
If the algorithm just drew triangles exactly halfway between the inside and outside corners, the terrain would still look a bit jagged (like a low-poly PS1 game). If the algorithm just drew triangles exactly halfway between the inside and outside corners, the terrain would still look a bit jagged (like a low-poly PS1 game).
To make it perfectly smooth, we **Interpolate**. Remember the sign convention from §1 — To make it perfectly smooth, we **Interpolate**:
negative is underground, positive is air: * Corner A has a density of `1.0` (Very solid).
* Corner A has a density of `-1.0` (deep underground, very solid). * Corner B has a density of `-0.1` (Just barely in the air).
* Corner B has a density of `0.1` (just barely up in the air). * Because `-0.1` is much closer to our `0.0` Iso-Level than `1.0` is, the algorithm slides the triangle vertex much closer to Corner B. This creates gentle slopes and sharp cliffs dynamically.
* Because `0.1` is much closer to our `0.0` Iso-Level than `-1.0` is, the algorithm slides the triangle vertex much closer to Corner B. This creates gentle slopes and sharp cliffs dynamically.
--- ---
## ⚙️ The Execution Loop (What our C# Script will do) ## ⚙️ The Execution Loop (What our C# Script will do)
When the Server tells the Client to render a 24x24x256 chunk, the mesher does this: When the Server tells the Client to render a 16x16x64 chunk, the mesher does this:
1. Loops through every X, Y, Z coordinate in the chunk. 1. Loops through every X, Y, Z coordinate in the chunk.
2. Checks the 8 corners of the current voxel. 2. Checks the 8 corners of the current voxel.
3. Creates an `8-bit integer` (a byte) based on which corners are solid (e.g., `00001111`). 3. Creates an `8-bit integer` (a byte) based on which corners are solid (e.g., `00001111`).

View file

@ -625,9 +625,7 @@ 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();
// RoadPathGrid is a stock AStarGrid2D with a scaled-up distance estimate, so the AStarGrid2D astar = new AStarGrid2D();
// 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;
@ -655,7 +653,7 @@ public partial class MapGenerator : TextureRect
Vector2 startPos = highwayNodes[i].Position; Vector2 startPos = highwayNodes[i].Position;
Vector2 endPos = highwayNodes[(i + 1) % highwayNodes.Count].Position; Vector2 endPos = highwayNodes[(i + 1) % highwayNodes.Count].Position;
Vector2[] rawPath = FindPath(astar, Vector2[] rawPath = astar.GetPointPath(
new Vector2I((int)startPos.X, (int)startPos.Y), new Vector2I((int)startPos.X, (int)startPos.Y),
new Vector2I((int)endPos.X, (int)endPos.Y) new Vector2I((int)endPos.X, (int)endPos.Y)
); );
@ -690,7 +688,7 @@ public partial class MapGenerator : TextureRect
} }
} }
Vector2[] bossPath = FindPath(astar, Vector2[] bossPath = astar.GetPointPath(
new Vector2I((int)snowBoss.Position.X, (int)snowBoss.Position.Y), bestPixel new Vector2I((int)snowBoss.Position.X, (int)snowBoss.Position.Y), bestPixel
); );
@ -746,7 +744,7 @@ public partial class MapGenerator : TextureRect
else else
{ {
// Attempt the connection. (If it freezes here, we know exactly which town caused it!) // Attempt the connection. (If it freezes here, we know exactly which town caused it!)
countyPath = FindPath(astar, countyPath = astar.GetPointPath(
new Vector2I((int)bestUnconnected.Position.X, (int)bestUnconnected.Position.Y), new Vector2I((int)bestUnconnected.Position.X, (int)bestUnconnected.Position.Y),
new Vector2I((int)bestConnected.Position.X, (int)bestConnected.Position.Y) new Vector2I((int)bestConnected.Position.X, (int)bestConnected.Position.Y)
); );
@ -771,19 +769,6 @@ public partial class MapGenerator : TextureRect
GD.Print("[A*] Logistics Network Complete!"); GD.Print("[A*] Logistics Network Complete!");
} }
/// <summary>
/// The one place a road path is actually searched for.
///
/// Every road — highway, branch, county — comes through here, so this is the single
/// seam where pathfinding can be changed without touching any routing decision
/// (which towns connect, in what order, at which tier). Those all live above and
/// only ever deal in town positions.
/// </summary>
private Vector2[] FindPath(AStarGrid2D astar, Vector2I from, Vector2I to)
{
return astar.GetPointPath(from, to);
}
private float GetSeaLevel(float t) => Mathf.Lerp(0.26f, 0.15f, Mathf.Clamp(t, 0f, 1f)); private float GetSeaLevel(float t) => Mathf.Lerp(0.26f, 0.15f, Mathf.Clamp(t, 0f, 1f));
private Vector2I FindClosestPixel(Vector2 pos, HashSet<Vector2I> set) { private Vector2I FindClosestPixel(Vector2 pos, HashSet<Vector2I> set) {
@ -838,23 +823,6 @@ 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);
@ -866,7 +834,8 @@ 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)) {
astar.SetPointWeightScale(cell, astar.GetPointWeightScale(cell) + ROAD_REPULSION_PENALTY); // The true Iron Curtain
astar.SetPointWeightScale(cell, astar.GetPointWeightScale(cell) + 10000f);
} }
} }
} }
@ -968,44 +937,6 @@ 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;