8.1 KiB
WorldGraph
A node-based procedural tile generation system. Graphs are composed of typed nodes wired together; evaluating the graph at a given world cell produces a tile ID.
Overview
World generation is expressed as a directed acyclic graph (DAG) of computation nodes. Each node takes zero or more typed inputs, performs some computation, and produces a single typed output. The graph is evaluated independently per cell — there is no shared mutable state between cells.
The final result is an integer tile ID, obtained by calling AsInt() on whatever value the output node produces.
Core Types (WorldGraphTypes.h)
Value
A tagged union that can hold an Int, Float, or Bool. Values are coerced automatically when a node requests a different type (e.g. AsFloat() on an Int just casts it). This means connections between different types are permissive at runtime, though the editor can flag them.
EvalContext
The per-cell context forwarded to every node during evaluation:
| Field | Description |
|---|---|
worldX, worldY |
World-space tile coordinates of the cell being generated |
seed |
World seed for deterministic noise |
prevTiles |
Flat row-major array of tile IDs from the previous generation pass |
prevOriginX/Y, prevWidth/Height |
Position and size of the previous pass buffer |
GetPrevTile(x, y) safely reads the previous pass at any absolute world coordinate, returning 0 (AIR) for out-of-bounds or when no previous pass exists.
Graph (WorldGraph.h)
Graph owns a set of nodes and the connections between them.
NodeID AddNode(unique_ptr<Node>) — add a node, returns its ID
bool Connect(from, to, inputSlot) — wire an output to an input
Value Evaluate(outputNode, ctx) — evaluate for one cell
bool IsValid(outputNode) — check all inputs are wired
Cycle detection runs at Connect() time. Any connection that would form a cycle is rejected and returns false.
Evaluation is recursive and on-demand. Starting from the output node, the graph walks upstream, evaluating each dependency before the node that depends on it. Unconnected inputs receive a zero value of their declared type.
Nodes (WorldGraphNode.h)
All nodes derive from the abstract Node base class and implement:
GetOutputType()— the type this node producesGetInputTypes()— the types of each input slotEvaluate(ctx, inputs)— computes and returns the output value
Source Nodes (no inputs)
| Node | Output | Description |
|---|---|---|
ConstantNode |
Float | A fixed float value |
IDNode |
Int | A fixed tile ID integer |
PositionXNode |
Int | World X coordinate of the current cell |
PositionYNode |
Int | World Y coordinate of the current cell |
Math Nodes
| Node | Inputs | Output | Description |
|---|---|---|---|
AddNode |
Float, Float | Float | a + b |
SubtractNode |
Float, Float | Float | a − b |
MultiplyNode |
Float, Float | Float | a × b |
DivideNode |
Float, Float | Float | a / b (0 on divide-by-zero) |
ModuloNode |
Float, Float | Float | fmod(a, b) |
SinNode |
Float | Float | sin(a) in radians |
CosNode |
Float | Float | cos(a) in radians |
Comparison Nodes
All produce Bool. Inputs are Float.
LessNode, GreaterNode, LessEqualNode, GreaterEqualNode, EqualNode
Note:
EqualNodecompares floats directly — use only with integer-valued inputs.
Boolean Logic Nodes
| Node | Inputs | Output |
|---|---|---|
AndNode |
Bool, Bool | Bool |
OrNode |
Bool, Bool | Bool |
NotNode |
Bool | Bool |
Control Flow
BranchNode — selects between two values based on a condition:
inputs[0] Bool — condition
inputs[1] Float — value if true
inputs[2] Float — value if false
The concrete type of the chosen branch is passed through, so an IDNode connected to the true/false slot will produce an Int even though GetOutputType() reports Float.
Previous-Pass Query Nodes
These nodes read from the previous generation pass via EvalContext. They have no inputs — their parameters are baked into the node at construction time. The chunk generator uses these baked offsets to pre-compute how much border padding the previous pass must produce.
| Node | Output | Description |
|---|---|---|
QueryTileNode(offsetX, offsetY, expectedID) |
Bool | true if the tile at (worldX+offsetX, worldY+offsetY) equals expectedID |
QueryRangeNode(minX, minY, maxX, maxY, tileID) |
Int | Count of tiles matching tileID within the relative rectangle |
QueryDistanceNode(tileID, maxDistance) |
Int | Chebyshev distance to the nearest tile matching tileID; returns maxDistance + 1 if none found. The current cell is excluded. |
Multi-Pass Chunk Generation (WorldGraphChunk.h)
Complex terrain is built in ordered passes, where each pass can read the output of the previous one. For example: pass 1 places stone, pass 2 adds ores based on nearby stone.
GenerationPass
A thin struct pairing a Graph with the NodeID of its output node.
Zero = "No Change"
When a pass outputs 0 for a cell, it means keep the previous pass's value (or AIR if there was no previous pass). This lets later passes act as selective overrides rather than full rewrites.
GenerateRegion
Generates a single rectangular region using one graph. Walks every cell, builds an EvalContext pointing at prevPassData, evaluates the graph, and writes the result.
GenerateChunk
Runs a full sequence of passes over a chunk, automatically computing the padding each pass needs.
Algorithm:
1. The last pass generates exactly [chunkOriginX, chunkOriginY, W × H].
2. Working backwards from the last pass, ComputeRequiredPadding inspects
every QueryTile / QueryRange / QueryDistance node in each pass's graph
to determine how much the previous pass's output region must expand.
This expansion accumulates from last pass to first.
3. Passes execute in forward order. Each feeds its TileGrid output to the
next pass as prevPassData.
4. The final TileGrid covers the original chunk region only.
PaddingBounds
Records how many extra tiles are needed beyond the output region in each direction (negX, posX, negY, posY). Computed automatically by ComputeRequiredPadding, which walks the graph and unions the baked offsets from all query nodes.
Serialization (WorldGraphSerializer.h)
GraphSerializer converts a Graph to/from JSON (nlohmann/json).
nlohmann::json j = GraphSerializer::ToJson(graph);
optional<Graph> g = GraphSerializer::FromJson(j);
bool ok = GraphSerializer::Save(graph, "path/to/file.json");
optional<Graph> g2 = GraphSerializer::Load("path/to/file.json");
JSON structure:
{
"nextId": 5,
"nodes": [
{ "id": 1, "type": "Constant", "value": 0.5 },
{ "id": 2, "type": "TileID", "tileId": 3 },
{ "id": 3, "type": "Branch" }
],
"connections": [
{ "from": 1, "to": 3, "slot": 0 },
{ "from": 2, "to": 3, "slot": 1 }
]
}
Only Constant and TileID nodes carry extra fields. All query nodes (QueryTile, QueryRange, QueryDistance) serialize their baked offset/ID parameters.
Minimal Usage Example
using namespace WorldGraph;
// Build a graph that places tile 2 when Y < -10, otherwise tile 1
Graph g;
auto posY = g.AddNode(std::make_unique<PositionYNode>());
auto thresh = g.AddNode(std::make_unique<ConstantNode>(-10.0f));
auto cond = g.AddNode(std::make_unique<LessNode>());
auto tileA = g.AddNode(std::make_unique<IDNode>(2)); // underground tile
auto tileB = g.AddNode(std::make_unique<IDNode>(1)); // surface tile
auto branch = g.AddNode(std::make_unique<BranchNode>());
g.Connect(posY, cond, 0); // posY → Less.a
g.Connect(thresh, cond, 1); // -10 → Less.b
g.Connect(cond, branch, 0); // bool → Branch.condition
g.Connect(tileA, branch, 1); // tile2 → Branch.true
g.Connect(tileB, branch, 2); // tile1 → Branch.false
// Generate a 64×64 chunk at world position (0, -32)
GenerationPass pass { g, branch };
TileGrid chunk = GenerateChunk({ pass }, 0, -32, 64, 64, /*seed=*/12345);