Skip to main content

Post-processing - Walker Generator

Post-processing is a powerful system that allows you to extend and customize the walker generator's level generation process. By creating custom post-processing scripts, you can alter how a level is generated. You can add extra steps to the generator or modify how built-in steps work.

Overview

The post-processing system works by allowing you to register callbacks that execute at specific points during the generation pipeline. Each callback runs at a defined execution order, ensuring that operations happen in the correct sequence.

Post-processing scripts inherit from the WalkerGeneratorPostProcessor class and are automatically discovered and executed by the WalkerGenerator when attached to the generator object or as a child of the generator.

tip

The WalkerGeneratorPostProcessor base class inherits from MonoBehaviour, so it is expected that each post-processor is attached to a game object in a scene.

Execution Order

Use constants from WalkerGeneratorPostProcessor.ExecutionOrder when registering callbacks:

OrderOperation
100Walker simulation (movement + floor tile spawning)
200Build IntGrid from simulated floor tiles (also fills empty inner cells with walls)
300Add border (BorderSize) and paint border wall tiles
400Noise mapping (applies noise mapping into the inner simulation domain)
401 (internal)Center-grid transform adjustment (moves the generator root transform; does not modify the IntGrid)
500Place spawn/exit POIs
600Spawn structures (populates result.FixedPositions)
700Minimum wall size cleanup (removes wall islands inside the inner domain)
1000Layout generated (do not modify the IntGrid)
1100Theme engine
2000Level generated
tip

Modify tiles/biomes before ExecutionOrder.LayoutGenerated (1000). After that, the theme engine assumes the layout is stable.

Creating Custom Post-Processors

To create a custom post-processor:

  1. Create a new script that inherits from WalkerGeneratorPostProcessor
  2. Implement the required SetupCallbacks method
  3. Optionally override the core walker simulation by setting Generator.WalkerAlgorithmOverride in SetupCallbacks
  4. Attach the script to your WalkerGenerator or to a child object

Basic Structure

using Frigga;
using UnityEngine;

namespace Frigga.Extra
{
public class MyWalkerPostProcessor : WalkerGeneratorPostProcessor
{
public override void SetupCallbacks(GeneratorCallbacks callbacks)
{
callbacks.RegisterAfter(ExecutionOrder.BuildIntGrid, HandleAfterBuild);
}

private void HandleAfterBuild(GeneratorResult result)
{
// Custom processing logic here
}
}
}

Callback Registration

Use ExecutionOrder constants to register callbacks at the appropriate execution point:

// Execute after build
callbacks.RegisterAfter(ExecutionOrder.BuildIntGrid, HandleAfterBuild);

// Execute before a specific stage
callbacks.RegisterBefore(ExecutionOrder.PlaceSpawnAndExit, HandleBeforeSpawn);

// Replace a stage entirely (non-coroutine)
callbacks.RegisterInsteadOf(ExecutionOrder.MinimumWallSizeCleanup, HandleCleanupReplacement);

// Replace a stage entirely (coroutine)
callbacks.RegisterCoroutineInsteadOf(ExecutionOrder.MinimumWallSizeCleanup, HeavyCleanup);

Performance Considerations

For performance-heavy operations, use coroutine callbacks and yield return null to keep the editor/game responsive:

private IEnumerator HeavyProcessing(GeneratorResult result)
{
var positions = result.Bounds.allPositionsWithin;
var i = 0;

foreach (var position in positions)
{
// Process position

i++;
if (i % 200 == 0)
yield return null;
}
}

Examples

Checkerboard Post-processor

Checkerboard post-processor filling large squares with lava tiles
Checkerboard post-processor filling large squares with lava tiles

The WalkerCheckerboardPostProcessor demonstrates modifying the generated IntGrid directly after BuildIntGrid.

It registers with:

  • callbacks.RegisterAfter(ExecutionOrder.BuildIntGrid, ...) (so it runs at BuildIntGrid + 1, before the border expansion stage)

The post-processor:

  • iterates result.Bounds.allPositionsWithin (at that point result.Bounds is the inner simulation domain)
  • only touches cells that are already non-empty (tileData.Tile != 0)
  • writes _checkerboardTile on the checkerboard pattern

Example implementation:

using Frigga;
using UnityEngine;

public class WalkerCheckerboardPostProcessor : WalkerGeneratorPostProcessor
{
[IntGridTilePicker] [SerializeField] private int _checkerboardTile;
[SerializeField] [Min(1)] private int _squareSize = 1;

public override void SetupCallbacks(GeneratorCallbacks callbacks)
{
callbacks.RegisterAfter(ExecutionOrder.BuildIntGrid, HandleCheckerboard);
}

private void HandleCheckerboard(GeneratorResult result)
{
foreach (var position in result.Bounds.allPositionsWithin)
{
var size = Mathf.Max(1, _squareSize);
var isCheckerboard =
(Mathf.FloorToInt((float) position.x / size) + Mathf.FloorToInt((float) position.y / size)) % 2 == 0;

if (!isCheckerboard)
{
continue;
}

var tileData = result.IntGrid.GetTileData(position);
if (tileData.Tile == 0)
{
continue;
}

tileData.Tile = _checkerboardTile;
result.IntGrid.SetTileData(position, tileData);
}
}
}

Source:

  • com.ondrejnepozitek.frigga.extra/Extra/WalkerPostProcessing/WalkerCheckerboardPostProcessor.cs

Custom Walker Algorithm

Custom walker algorithm
Custom walker algorithm

You can override the core Walker simulation (movement + floor tile spawning) by setting Generator.WalkerAlgorithmOverride in SetupCallbacks().

using Frigga;
using UnityEngine;

public class SimpleWalkerPostProcessor : WalkerGeneratorPostProcessor, IWalkerAlgorithm
{
[SerializeField] [Range(0, 1)] private float _rotationChance = 0.5f;
[SerializeField] [Min(1)] private int _maxStepsWithoutRotation = 10;

private System.Random _random;

public override void SetupCallbacks(GeneratorCallbacks callbacks)
{
Generator.WalkerAlgorithmOverride = this;
}

public void Initialize(WalkerGenerator generator, WalkerGeneratorState state)
{
_random = new System.Random(generator.Seed);

var walker = new WalkerAgent(Vector3Int.zero, WalkerDirection.Up, state.Bounds);
state.Walkers.Add(walker);
}

public void DoOneIteration(WalkerGeneratorState state)
{
foreach (var walker in state.Walkers)
{
if (!walker.MoveForward())
{
walker.TurnRight();
walker.TurnRight();
continue;
}

state.FloorTiles.Add(walker.Position);

if (walker.StepsForward >= _maxStepsWithoutRotation || _random.NextDouble() < _rotationChance)
{
var turnRight = _random.NextDouble() < 0.5;
walker.TurnRight();

if (!turnRight)
{
walker.TurnRight();
walker.TurnRight();
}
}
}
}

public void OnSimulationComplete(WalkerGeneratorState state)
{
}
}

IWalkerAlgorithm is run-scoped:

  • Initialize(generator, state) is called once per generation run
  • DoOneIteration(state) is called repeatedly until the generator stops
  • OnSimulationComplete(state) finalizes any simulation-specific data (notably state.Bounds when applicable)

Useful runtime types for custom core logic:

  • IWalkerAlgorithm
  • WalkerGeneratorState (walkers, floor tiles, and bounds)
  • WalkerAgent (per-walker state and movement helpers)
  • WalkerDirection and WalkerSquareGrid helpers
  • IWalkerBounds / RectWalkerBounds for custom movement constraints

Advanced post-processors can also read/mutate generator internals through Generator.State.

Generator.State.PostProcessProtectedPositions is the shared contract used by built-in Walker stages to protect POIs and structure-used cells from later cleanup passes.

Source:

  • com.ondrejnepozitek.frigga.extra/Extra/WalkerPostProcessing/SimpleWalkerPostProcessor.cs

Advanced Usage

GeneratorCallbacks exposes several registration methods (all based on execution order values):

  • Register(order, callback) for exact order
  • RegisterAfter(order, callback) / RegisterBefore(order, callback) for nearby hooks
  • RegisterInsteadOf(order, callback) to replace all callbacks at that exact order
  • Coroutine variants for heavy work (use yield return null;):
    • RegisterCoroutine(order, callback)
    • RegisterCoroutineAfter(order, callback)
    • RegisterCoroutineBefore(order, callback)
    • RegisterCoroutineInsteadOf(order, callback)
tip

Prefer using WalkerGeneratorPostProcessor.ExecutionOrder constants over hardcoded numbers.