Generation

Procedural generation

How Meep's marker-and-rule model generates levels — GridData, layers, task pipeline, and where generation fits in the ECS.

Meep’s procedural-generation system lives under @woosh/meep-engine/src/generation/. The central idea is a two-phase pipeline: first, rules visit every cell of a grid and emit typed markers into a spatial index; second, a processing pass matches those markers against rules and instantiates ECS entities or paints terrain. The grid itself can carry multiple typed data layers, each independently sized and sampled.

GridData — the shared workspace

GridData is the mutable workspace that every generator reads and writes. It has three parts:

  • width / height — cell dimensions of the grid.
  • layers — an array of GridDataLayer objects, each a named typed raster.
  • markers — a QuadTreeNode spatial index of MarkerNode objects placed during generation.
import { GridData }      from "@woosh/meep-engine/src/generation/grid/GridData.js";
import { GridDataLayer } from "@woosh/meep-engine/src/generation/grid/layers/GridDataLayer.js";
import { BinaryDataType } from "@woosh/meep-engine/src/core/binary/type/BinaryDataType.js";

const grid = new GridData();
grid.resize(128, 128);
grid.computeScale(1.0);   // tileSize → world-space offset/scale transform

const obstacleLayer = GridDataLayer.from("obstacles", BinaryDataType.Uint8);
grid.addLayer(obstacleLayer);

GridDataLayer.from(id, type, resolution) creates a named layer backed by a Sampler2D. The optional resolution multiplier lets a layer carry higher-resolution data than the grid cell count — useful for splat maps that need sub-cell precision. Once added, grid.getLayerById(id) retrieves it by name, and layers resize automatically when grid.resize(w, h) is called.

grid.addMarker(node) inserts a MarkerNode into the spatial index. grid.containsMarkerInCircle(x, y, radius, matcher) and grid.countMarkerInCircle(x, y, radius, matcher) query the index spatially, using a MarkerNodeMatcher to filter by type or tag.

Markers

A MarkerNode is a lightweight record that the generation pipeline places in the grid:

FieldTypePurpose
typestringSemantic type identifier — matched by rules
tagsstring[]Arbitrary labels for matcher predicates
positionVector2Grid-space position
transformTransformWorld-space position, rotation, scale
sizenumberRadius — used for spatial queries and overlap rejection
prioritynumberHigher-priority nodes are processed first
propertiesObjectArbitrary key-value data for downstream rules

Markers are emitted by GridCellActionPlaceMarker, which is a GridCellAction that runs when a rule fires on a grid cell. Multiple related markers can be emitted atomically with GridCellActionPlaceMarkerGroup, which writes a shared groupId into all properties.

The rule-and-action loop

GridActionRuleSet is the engine of the emission phase. It iterates over every cell (or sub-sample, given a resolution parameter), selects rules by the configured policy, and for each rule:

  1. Tests the rule’s pattern (CellMatcher) against the current cell at up to four 90° rotations if allowRotation is true.
  2. Rolls against the rule’s probability (CellFilter, default 1.0).
  3. If both pass, calls the rule’s GridCellAction.execute(grid, x, y, rotation).
import { GridActionRuleSet }      from "@woosh/meep-engine/src/generation/markers/GridActionRuleSet.js";
import { GridCellPlacementRule }  from "@woosh/meep-engine/src/generation/placement/GridCellPlacementRule.js";
import { RuleSelectionPolicyType } from "@woosh/meep-engine/src/generation/markers/RuleSelectionPolicyType.js";
import { CellFilterLiteralFloat } from "@woosh/meep-engine/src/generation/filtering/numeric/CellFilterLiteralFloat.js";

const ruleSet = GridActionRuleSet.from({
    rules: [myRule],
    policy: RuleSelectionPolicyType.Sequential,  // or Random
    pattern: [0, 0],   // sub-sample offsets within each cell
});

const task = ruleSet.process(grid, seed, /* resolution= */ 1);

RuleSelectionPolicyType.Sequential tries rules in order and stops at the first match; Random shuffles before each cell, producing more varied output at higher cost. process() returns an async Task suitable for Meep’s task scheduler.

Task generators

For larger pipelines, GridTaskGenerator subclasses compose generation stages. Each generator has a build(grid, ecd, seed) method that returns a Task or TaskGroup, and dependencies can be wired between generators with addDependency / addDependencies.

GeneratorPurpose
GridTaskSequenceRun a list of child generators in order
GridTaskApplyActionToCellsApply a GridCellAction to every cell returned by a CellSupplier
GridTaskExecuteRuleTimesExecute a GridCellPlacementRule exactly N times at random cells
GridTaskDensityMarkerDistributionScatter markers according to a density CellFilter, with collision rejection
GridTaskCellularAutomata (discrete)Advance a cellular automaton over a grid layer
GridTaskConnectRooms (discrete)Corridor generation between room cells

GridTaskDensityMarkerDistribution is the primary tool for organic scatter. It tiles the grid into 16×16 blocks to approximate blue-noise distribution, samples the density filter at random positions, rolls against the density value, and rejects candidates that overlap an existing marker’s radius. The expected marker count is estimated analytically before the run.

Multi-layer grids

Layers are independent: a Uint8 obstacle layer can coexist with a Float32 heightmap layer and a Uint16 biome-index layer on the same GridData. CellFilterSampleLayerLinear and CellFilterSampleLayerCubic read back from any named layer inside a filter expression. CellMatcherLayerBitMaskTest tests a bitmask against an integer layer directly inside a matcher. This lets rules in one pass use data written by a previous pass.

Where generation fits

The procedural pipeline is separate from the ECS but operates on the same EntityComponentDataset. A typical sequence is:

  1. Allocate a GridData and add layers.
  2. Run one or more GridActionRuleSet passes to populate markers.
  3. Optionally apply cellular automata or room-connection passes.
  4. Apply a ThemeEngine — see Themes, matchers & rules — which converts markers into ECS entities and paints the terrain.
rules visit every grid cellemit typed markers into a spatial indexThemeEngine matches the markersinstantiate entities and paint terrain

The filter algebra used in every step is described in Cell filters & automata.