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> |
||
|---|---|---|
| .. | ||
| MapGenerator.cs | ||
| MapGenerator.cs.uid | ||
| MapGenerator.md | ||
| README.md | ||
/Tools/Scripts — generation logic
C# backend for the offline developer tools. Decoupled from the live server and client.
MapGenerator.cs
Generates the entire 2D blueprint. Roughly in order:
- Config — reads
ServerConfig.json, which setsMapSize(8192 by default) and the crater radius. TheMapSize = 4096field initialiser is a fallback that is immediately overwritten. - Topography — FastNoiseLite base height plus a mountain spine, minus a squircle distance
falloff, giving a guaranteed island. Noise frequency is divided by
scaleFactor(MapSize / 1024f) so terrain features stay the same real-world size at any map profile. - Sea level and water — temperature-driven sea level; flood fill separates true ocean from inland lakes; a mainland fill guarantees one contiguous landmass.
- The crater — placed along the northern coast and carved to below sea level, but only out to 80 % of its radius, which guarantees a landbridge rather than severing the island.
- Biomes and towns — biome zoning by height and temperature; tiered town placement (Capitol, Hubs, Villages, Outposts, POIs) filtered by slope, water proximity and spacing.
- Roads — see below.
- Export — writes the
.datblueprint, then renders the PNG snapshot.
The road network
AStarGrid2D over the whole map at one node per pixel. Mountains are made expensive rather than
impassable (1 + elevation³ × 400), water and the crater are marked solid.
- Continental loop — highway nodes sorted by radial angle and connected in a ring.
- Mountain branch — a spur from the loop to the snow hub.
- County roads — Prim's algorithm daisy-chains remaining towns onto the network. Villages become Rugged roads, everything else Trails.
- Abandon protocol — a town further than 8 % of the map from the network is skipped rather than pathed to, so one unreachable outpost cannot hang generation.
- Smoothing — every path is decimated with Ramer–Douglas–Peucker (tolerance 4.0) then smoothed with 4 Chaikin passes.
⚠ This is the project's dominant performance problem. At 8K the grid is 67 million nodes, and the
weight-setup pass touches every one before the first path is requested. The await yields between
stages cannot interrupt a single engine-side GetPointPath call, so a long path still blocks. Full
regenerations frequently get abandoned.
The PNG snapshot
SaveMapSnapshot() builds an offscreen SubViewport in code rather than relying on the scene's
layout, because Godot's UI layout engine will otherwise crush a 4096+ render down to the editor
window size. A MapDrawProxy control re-issues the _Draw() calls into that viewport — GetImage()
captures only the base texture and would otherwise miss the roads and towns entirely — and the code
awaits two RenderingServer.FramePostDraw signals so the GPU has actually painted before the image
is read back.
Road colours on the snapshot, useful for identifying a road: red = Highway, black = Branch, dark brown = Rugged, light brown = Trail.
Outputs
MapData_Seed_<seed>.dat and Map_Seed_<seed>.png, both to user://.
Rules
- No magic numbers. Distances, radii and thresholds derive from
MapSizeorscaleFactor, so the generator behaves identically at 4K and 10K. - Respect the GPU. Anything exporting visual data must
awaitthe appropriate frame signals before reading pixels back.