Files
2026-02-24 17:22:41 +09:00
..
2026-02-22 15:19:54 +09:00
2026-02-22 11:57:39 +09:00
2026-02-22 11:57:39 +09:00
2026-02-24 17:22:41 +09:00
2026-02-23 23:07:10 +09:00

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 produces
  • GetInputTypes() — the types of each input slot
  • Evaluate(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: EqualNode compares 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);