Header menu logo Mibo

2D Layout Engine

The Layout engine provides a tile-based level design system for 2D games. It lives in Mibo.Layout.

Core Concepts

The system is built on three primitives:

CellGrid2D - The Storage

A dense 2D array that stores your tile content:

open Mibo.Layout
open Microsoft.Xna.Framework

// Create a 100x50 grid with 32x32 pixel cells
let grid = CellGrid2D.create 100 50 (Vector2(32f, 32f)) Vector2.Zero

Each cell holds 'T voption - either ValueSome content or ValueNone (empty). This struct-based option type has zero heap allocation per cell.

Basic Operations

// Set a cell
CellGrid2D.set 5 3 myTile grid

// Get a cell (returns voption)
match CellGrid2D.get 5 3 grid with
| ValueSome tile -> // use tile
| ValueNone -> // empty

// Get world position for a cell
let worldPos = CellGrid2D.getWorldPos 5 3 grid  // Vector2(160f, 96f)

Iteration

// Iterate all populated cells
grid
|> CellGrid2D.iter (fun x y tile ->
    printfn "Tile at (%d, %d)" x y
)

// Iterate only visible cells (culled to viewport)
// This is critical for performance in large levels, as it avoids
// processing tiles that aren't on screen.
let viewBounds = Rectangle(cameraX, cameraY, viewportWidth, viewportHeight)
grid
|> CellGrid2D.iterVisible viewBounds (fun x y tile ->
    // render tile at (x, y)
)

GridSection2D - The Cursor

A section is a lightweight view into a grid. It provides:

You rarely create sections directly - the Layout.run function creates the root section for you.

Layout DSL - Composing Content

The Layout module provides a fluent DSL for placing content. All functions return the section, enabling pipeline composition.

Basic Usage

open Mibo.Layout

let myGrid =
    CellGrid2D.create 20 15 (Vector2(32f, 32f)) Vector2.Zero
    |> Layout.run (fun section ->
        section
        |> Layout.fill 0 0 20 15 FloorTile      // Fill entire area
        |> Layout.border 0 0 20 15 WallTile     // Add border
        |> Layout.set 10 7 ChestTile            // Place item
    )

Scoping with Sections

Create sub-sections for relative positioning:

section
|> Layout.section 5 3 (fun inner ->
    // (0, 0) here maps to (5, 3) in the parent
    inner
    |> Layout.fill 0 0 4 4 FloorTile
)
// Returns to parent section, can continue chaining
|> Layout.section 12 3 (fun inner ->
    inner |> Layout.fill 0 0 4 4 FloorTile
)

Structural Helpers

// Padding - shrink section by N cells on all sides
section |> Layout.padding 2 (fun inner -> ...)

// PaddingEx - explicit padding for each side: left, top, right, bottom
section |> Layout.paddingEx 1 2 1 2 (fun inner -> ...)

// Center - position a fixed-size block in the center
section |> Layout.center 4 4 (fun inner -> ...)

// Flow - place stamps horizontally or vertically with spacing
section |> Layout.flowX 5 stamps
section |> Layout.flowY 5 stamps

Primitives

Layout.set x y content section         // Single cell
Layout.fill x y w h content section    // Rectangle
Layout.border x y w h content section  // Hollow rectangle
Layout.rect x y w h bContent fContent section // Filled rectangle with border
Layout.corners x y w h content section // Only the four corners
Layout.repeatX x y count content section // Horizontal line
Layout.repeatY x y count content section // Vertical line
Layout.clear x y w h section           // Clear cells to empty

Geometry

Layout.line x1 y1 x2 y2 content section        // Bresenham line
Layout.circle cx cy radius filled content      // Midpoint circle
Layout.polygon points filled content           // Arbitrary polygon

Patterns

Layout.checker oddContent evenContent section  // 3D checkerboard
Layout.checkerBorder x y w h odd even section  // Only on perimeter
Layout.scatter count seed content section      // Random placement
Layout.scatterBorder x y w h count seed content section // On perimeter
Layout.scatterLine x1 y1 x2 y2 count seed content section // Along line
Layout.generate x y w h (fun x y -> ...) section  // Procedural

Iteration / Transformation

Non-destructive operations for modifying existing content:

Layout.iter x y w h action section    // Read access to volume
Layout.map x y w h mapping section    // Transform existing content
Layout.replace oldContent newContent section  // Find and replace
Layout.replaceScatter old new prob seed section // Probabilistic replace
Layout.scatterStamp count seed stamp section  // Place complex components
Layout.setIfEmpty x y content section  // Conditional set

Layered Composition

For multi-layer content (background, foreground, decorations), use LayeredGrid2D. This manages a collection of grids sharing the same dimensions, keyed by an integer index (usually representing depth).

let level =
    LayeredGrid2D.create 100 50 (Vector2(32f, 32f)) Vector2.Zero
    |> LayeredLayout.layer 0 (fun section ->
        // Layer 0: Ground/Collision
        section |> Layout.fill 0 45 100 5 GroundTile
    )
    |> LayeredLayout.layer 1 (fun section ->
        // Layer 1: Foliage
        section |> Layout.scatter 50 42 GrassDecoration
    )

Rendering Layers

When rendering a layered grid, you don't need to manually sort the layers. Instead, you can map the grid's layer index to Mibo's RenderLayer measure. The engine's deferred rendering system will handle the sorting for you:

// Render each layer into the buffer
for KeyValue(layerIndex, layerGrid) in level.Layers do
    layerGrid
    |> CellGrid2D.iterVisible viewBounds (fun x y tile ->
        let pos = CellGrid2D.getWorldPos x y layerGrid

        buffer.Sprite(sprite {
            texture myTexture
            at pos.X pos.Y
            // Tag with the layer index using the RenderLayer measure
            layer (layerIndex<RenderLayer>)
        })
    )

This approach is efficient because Mibo's RenderBuffer performs a single, optimized CPU-side sort of all collected draw commands before sending them to the GPU. This ensures your layout layers are drawn in the correct back-to-front order and allows them to interact correctly with other game entities (like players or particles) that are also tagged with RenderLayer values.

Layers are created on-demand, so only layers you've painted into will consume memory.

Creating Your Own Stamps

A stamp is simply a function GridSection2D<'T> -> GridSection2D<'T>. You can create reusable stamps just like HTML custom elements:

Simple Stamp

/// A treasure chest on a pedestal
let treasureChest (section: GridSection2D<Tile>) =
    section
    |> Layout.fill 0 1 3 1 PedestalTile   // Base
    |> Layout.set 1 0 ChestTile           // Chest on top

Parameterized Stamp

/// A configurable room with walls and floor
let room width height floor wall (section: GridSection2D<Tile>) =
    section
    |> Layout.fill 0 0 width height floor
    |> Layout.border 0 0 width height wall

Composing Stamps

Stamps compose with >> (function composition):

let guardPost =
    room 8 6 FloorTile WallTile
    >> Layout.center 2 1 (treasureChest)
    >> Layout.section 6 2 (torchStand)

Building a Component Library

Organize stamps into domain modules:

module Dungeon =
    let cell = room 5 5 StoneFloor StoneWall

    let corridor length =
        Layout.fill 0 0 length 3 StoneFloor
        >> Layout.repeatX 0 0 length StoneWall
        >> Layout.repeatX 0 2 length StoneWall

    let intersection =
        cell >> Layout.clear 2 0 1 1  // North door
            >> Layout.clear 2 4 1 1   // South door
            >> Layout.clear 0 2 1 1   // West door
            >> Layout.clear 4 2 1 1   // East door

Use them:

level
|> LayeredLayout.layer 0 (fun section ->
    section
    |> Layout.section 0 0 Dungeon.cell
    |> Layout.section 5 1 (Dungeon.corridor 10)
    |> Layout.section 15 0 Dungeon.intersection
)

The Stamp Pattern

Think about stamps like Lego pieces, you can use a few blocks to build a bigger thing.

The key insight: stamps are just functions. You can store them, pass them around, compose them, and build complex structures from simple pieces.

Domain Modules

Mibo includes pre-built stamps for common game types:

These serve as examples and starting points. Copy and modify them for your game's needs.

namespace Microsoft
val grid: obj
union case ValueOption.ValueSome: 'T -> ValueOption<'T>
val tile: obj
union case ValueOption.ValueNone: ValueOption<'T>
val printfn: format: Printf.TextWriterFormat<'T> -> 'T
active recognizer KeyValue: System.Collections.Generic.KeyValuePair<'Key,'Value> -> 'Key * 'Value
val floor: value: 'T -> 'T (requires member Floor)

Type something to start searching.