Logo Mibo

Build a Map End to End

This page builds one map from an empty project: the cell type, the domain, a KDL document, the build, the queries, and the instanced draw. Each step is one short block. The other pages add options; this one shows the whole path in order.

The example is a 3D map. A 2D map is the same path without Height, Lift, and Span, and it draws sprites instead of instances. Step 12 states the difference.

1. Define the cell type

The cell holds what the renderer and the simulation read.

type ModelInfo = {
  Name: string
  SizeX: float32
  SizeY: float32
  SizeZ: float32
}

type BlockCell = {
  Model: ModelInfo
  Height: float32
  Span: InstanceSpan
  Lift: float32
  Solid: bool
}

2. Create the grid

The grid is the same CellGrid2D used by 2D maps. A cell holds ValueSome cell or ValueNone.

open Mibo.Layout
open System.Numerics

let cellSize = 16f

let newGrid (width: int) (height: int) =
  CellGrid2D.create width height (Vector2(cellSize, cellSize)) Vector2.Zero

3. Define the domain values

A word names one cell value. A kernel is a per-cell rule for generate. An element is a named body of statements.

let model name sx sy sz = {
  Name = name
  SizeX = sx
  SizeY = sy
  SizeZ = sz
}

let grass = { Model = model "grass" 1f 1f 1f; Height = 1f; Span = One; Lift = 0f; Solid = false }
let wall = { Model = model "wall" 1f 1f 1f; Height = 2f; Span = One; Lift = 0f; Solid = true }
let tree = { Model = model "tree" 1f 1f 1f; Height = 3f; Span = One; Lift = 0f; Solid = true }
let slab = { Model = model "platform" 1f 1f 1f; Height = 0.2f; Span = Span(3, 2); Lift = 0f; Solid = false }
let chest = { Model = model "chest" 1f 1f 1f; Height = 1f; Span = One; Lift = 0f; Solid = false }

Height scales the mesh on Y. Span states how many cells the instance covers. Solid is gameplay data; the framework never reads it.

let field =
  Doc.Gen2(fun x y -> if (x + y) % 7 = 0 then tree else grass)

let hut: Doc.ElementDecl<BlockCell> = {
  Name = "hut"
  Extent = ValueSome { W = 3; H = 3 }
  Body = [|
    Doc.Op.Fill grass
    Doc.Op.Border(ValueSome { X = 0; Y = 0; W = 3; H = 3 }, wall)
    Doc.Op.Set(ValueSome { X = 1; Y = 1 }, Start, Start, chest)
  |]
}

4. Build the surface

The surface maps document names to the domain. Build it once.

open System.Collections.Frozen
open System.Collections.Generic
open Mibo.Markup

let frozen (pairs: (string * 'T)[]) =
  let table = Dictionary<string, 'T>()

  for name, value in pairs do
    table[name] <- value

  table.ToFrozenDictionary()

let spanOf (cell: BlockCell) = cell.Span
let heightOf (cell: BlockCell) = cell.Height
let withSpan (cell: BlockCell) (span: InstanceSpan) = { cell with Span = span }

let surface: Doc.Surface<BlockCell> = {
  Words =
    frozen [|
      "grass", grass
      "wall", wall
      "tree", tree
      "slab", slab
    |]
  Kernels = frozen [| "field", field |]
  Elements = frozen [| "hut", hut |]
  Span = ValueSome spanOf
  WithSpan = ValueSome withSpan
}

The three tables are the game's. The framework reads them and the two span projections. Your names and cell type are yours.

5. Write the document

map 24 16 {
  layer ground {
    fill grass
    generate 0 0 24 4 field
    set 4 6 slab spanX=3 spanZ=2
  }

  layer decor {
    hut x=10 y=8
    set 20 12 tree
  }
}

fill paints the whole layer. generate runs the kernel over the top four rows. set places one cell, and spanX/spanZ size the slab to 3x2. hut places the element from the surface. XML spells the same document; see Authored Maps.

6. Build the layers

One call parses, resolves, emits, paints, and scans every layer.

let layers =
  match DocFlow.buildLayers (surface, src) with
  | Ok layers -> layers
  | Error reason -> failwith reason

Each layer is a BuiltLayer<'T>: Name, Grid, Landmarks, Occupancy.

let ground = layers[0]
let decor = layers[1]

7. Lift the layers

A decoration above the ground needs the height below it, or it replaces the ground. Stack.feet derives the lift for the whole stack.

let grids = layers |> Array.map(fun layer -> layer.Grid)
let occupancies = layers |> Array.map(fun layer -> layer.Occupancy)
let feet = Stack.feet occupancies grids heightOf

let drawn =
  Array.init layers.Length (fun i ->
    let source = layers[i].Grid
    let target = newGrid source.Width source.Height

    CellGrid2D.iter
      (fun x y cell ->
        CellGrid2D.set x y { cell with Lift = feet[i][x + y * source.Width] } target)
      source

    target)

Skip this step for a flat, single-layer map.

8. Query

One query serves a hover, a collision test, and a spawn: which instance owns the cell.

let ownerAt (layer: BuiltLayer<BlockCell>) (x: int) (y: int) =
  Occupancy.owner x y layer.Occupancy
  |> ValueOption.bind(fun at ->
    CellGrid2D.get at.X at.Y layer.Grid
    |> ValueOption.map(fun cell -> struct (at, cell)))

let rectAt (layer: BuiltLayer<BlockCell>) (x: int) (y: int) =
  Occupancy.owner x y layer.Occupancy
  |> ValueOption.bind(fun at -> Occupancy.rectOf at layer.Occupancy)

A covered cell answers with the instance that covers it. Element names ride the tag channel in an authored map:

Flow.taggedRects "hut" decor.Landmarks   // every hut, newest first
Flow.isTag "hut" { X = 10; Y = 8 } decor.Landmarks

9. Draw

One context per map, with a rectangle transform. The transform receives the rectangle the instance covers and the anchor position.

let context =
  InstancedRenderContext<BlockCell, string>.Rect(
    getKey = (fun cell -> cell.Model.Name),
    getMeshesAndMaterial = meshesOf,
    getTransform =
      fun (rect: CellRect) (basePos: Vector3) (cell: BlockCell) ->
        let boxW = float32 rect.W * cellSize
        let boxD = float32 rect.H * cellSize
        let boxH = cell.Height * cellSize

        Matrix4x4.CreateScale(
          boxW / cell.Model.SizeX,
          boxH / cell.Model.SizeY,
          boxD / cell.Model.SizeZ)
        * Matrix4x4.CreateTranslation(
          basePos.X + boxW * 0.5f,
          basePos.Y + cell.Lift,
          basePos.Z + boxD * 0.5f)
  )

Draw each layer through its own occupancy:

let view (_ctx: GameContext) (_model: Model) (buffer: RenderBuffer3D) =
  context.ResetFrameBuffers()
  buffer.beginCamera(camera).drop()

  for i in 0 .. drawn.Length - 1 do
    context.RenderInstanced(buffer, drawn[i], layers[i].Occupancy)

  buffer.endCamera().drop()

meshesOf, camera, and buffer are game values. For a large map, replace RenderInstanced with RenderWindowInstanced and pass the camera window. See 3D from 2D and GPU Instancing.

10. Wire it into the program

Steps 1 to 7 run once, in init. Cache layers, drawn, and context. Step 9 runs in view each frame. Step 8 runs on demand. The frame order is in 3D from 2D.

11. The same map in F#

A code-first map skips the surface. Compose Stamp values and run them.

let hutStamp =
  Stamp.named "hut" (Stamp.box 3 3 [ Flow.fill grass; Flow.border wall; Flow.cell { X = 1; Y = 1 } chest ])

let map =
  Flow.overlay [
    Flow.canvas [ Flow.fill grass; Flow.cell { X = 4; Y = 6 } slab ]
    Flow.at 10 8 hutStamp
  ]

let struct (grid, marks) = newGrid 24 16 |> Flow.run map

let occupancy =
  Occupancy.scan spanOf grid
  |> Result.defaultWith failwith

A named stamp answers Flow.tryPosition, not Flow.taggedRects:

Flow.tryPosition "hut" marks

Steps 7 to 10 are unchanged. See Code-First Maps for the container and style vocabulary.

12. 2D maps

Drop Height, Span, and Lift from the cell type. The document drops spanX=/spanZ=. The grid, the surface, the build, the landmarks, and the queries stay the same. Draw with the sprite loop in Code-First Maps.

type ModelInfo = { Name: string SizeX: float32 SizeY: float32 SizeZ: float32 }
Multiple items
val string: value: 'T -> string

--------------------
type string = System.String
Multiple items
val float32: value: 'T -> float32 (requires member op_Explicit)

--------------------
type float32 = System.Single

--------------------
type float32<'Measure> = float32
type BlockCell = { Model: ModelInfo Height: float32 Span: obj Lift: float32 Solid: bool }
type bool = System.Boolean
namespace System
namespace System.Numerics
val cellSize: float32
val newGrid: width: int -> height: int -> 'a
val width: int
Multiple items
val int: value: 'T -> int (requires member op_Explicit)

--------------------
type int = int32

--------------------
type int<'Measure> = int
val height: int
Multiple items
type Vector2 = new: value: float32 -> unit + 2 overloads member CopyTo: array: float32 array -> unit + 2 overloads member Equals: other: Vector2 -> bool + 2 overloads member GetHashCode: unit -> int member Length: unit -> float32 member LengthSquared: unit -> float32 member ToString: unit -> string + 2 overloads member TryCopyTo: destination: Span<float32> -> bool static member (&&&) : left: Vector2 * right: Vector2 -> Vector2 static member ( * ) : left: Vector2 * right: Vector2 -> Vector2 + 2 overloads ...
<summary>Represents a vector with two single-precision floating-point values.</summary>

--------------------
Vector2 ()
Vector2(value: float32) : Vector2
Vector2(values: System.ReadOnlySpan<float32>) : Vector2
Vector2(x: float32, y: float32) : Vector2
property Vector2.Zero: Vector2 with get
<summary>Returns a vector whose 2 elements are equal to zero.</summary>
<returns>A vector whose two elements are equal to zero (that is, it returns the vector <code data-dev-comment-type="c">(0,0)</code>).</returns>
val model: name: string -> sx: float32 -> sy: float32 -> sz: float32 -> ModelInfo
val name: string
val sx: float32
val sy: float32
val sz: float32
val grass: BlockCell
val wall: BlockCell
val tree: BlockCell
val slab: BlockCell
val chest: BlockCell
val field: obj
val hut: ModelInfo
union case ValueOption.ValueSome: 'T -> ValueOption<'T>
namespace System.Collections
namespace System.Collections.Frozen
namespace System.Collections.Generic
val frozen: pairs: (string * 'T) array -> FrozenDictionary<string,'T>
val pairs: (string * 'T) array
'T
val table: Dictionary<string,'T>
Multiple items
type Dictionary<'TKey,'TValue> = interface ICollection<KeyValuePair<'TKey,'TValue>> interface seq<KeyValuePair<'TKey,'TValue>> interface IEnumerable interface IDictionary<'TKey,'TValue> interface IReadOnlyCollection<KeyValuePair<'TKey,'TValue>> interface IReadOnlyDictionary<'TKey,'TValue> interface ICollection interface IDictionary interface IDeserializationCallback interface ISerializable ...
<summary>Represents a collection of keys and values.</summary>
<typeparam name="TKey">The type of the keys in the dictionary.</typeparam>
<typeparam name="TValue">The type of the values in the dictionary.</typeparam>


--------------------
Dictionary() : Dictionary<'TKey,'TValue>
Dictionary(dictionary: IDictionary<'TKey,'TValue>) : Dictionary<'TKey,'TValue>
Dictionary(collection: KeyValuePair<'TKey,'TValue> seq) : Dictionary<'TKey,'TValue>
Dictionary(comparer: IEqualityComparer<'TKey>) : Dictionary<'TKey,'TValue>
Dictionary(capacity: int) : Dictionary<'TKey,'TValue>
Dictionary(dictionary: IDictionary<'TKey,'TValue>, comparer: IEqualityComparer<'TKey>) : Dictionary<'TKey,'TValue>
Dictionary(collection: KeyValuePair<'TKey,'TValue> seq, comparer: IEqualityComparer<'TKey>) : Dictionary<'TKey,'TValue>
Dictionary(capacity: int, comparer: IEqualityComparer<'TKey>) : Dictionary<'TKey,'TValue>
val value: 'T
(extension) IEnumerable.ToFrozenDictionary<'TKey,'TValue>(?comparer: IEqualityComparer<'TKey>) : FrozenDictionary<'TKey,'TValue>
(extension) IEnumerable.ToFrozenDictionary<'TSource,'TKey>(keySelector: System.Func<'TSource,'TKey>, ?comparer: IEqualityComparer<'TKey>) : FrozenDictionary<'TKey,'TSource>
(extension) IEnumerable.ToFrozenDictionary<'TSource,'TKey,'TElement>(keySelector: System.Func<'TSource,'TKey>, elementSelector: System.Func<'TSource,'TElement>, ?comparer: IEqualityComparer<'TKey>) : FrozenDictionary<'TKey,'TElement>
val spanOf: cell: BlockCell -> obj
val cell: BlockCell
BlockCell.Span: obj
val heightOf: cell: BlockCell -> float32
BlockCell.Height: float32
val withSpan: cell: BlockCell -> span: 'a -> BlockCell
val span: 'a
val surface: BlockCell
union case Result.Ok: ResultValue: 'T -> Result<'T,'TError>
union case Result.Error: ErrorValue: 'TError -> Result<'T,'TError>
val failwith: message: string -> 'T
module Array from Microsoft.FSharp.Collections
val map: mapping: ('T -> 'U) -> array: 'T array -> 'U array
Multiple items
type Stack<'T> = interface seq<'T> interface IEnumerable interface IReadOnlyCollection<'T> interface ICollection new: unit -> unit + 2 overloads member Clear: unit -> unit member Contains: item: 'T -> bool member CopyTo: array: 'T array * arrayIndex: int -> unit member EnsureCapacity: capacity: int -> int member GetEnumerator: unit -> Enumerator<'T> ...
<summary>Represents a variable size last-in-first-out (LIFO) collection of instances of the same specified type.</summary>
<typeparam name="T">Specifies the type of elements in the stack.</typeparam>


--------------------
Stack() : Stack<'T>
Stack(collection: 'T seq) : Stack<'T>
Stack(capacity: int) : Stack<'T>
val init: count: int -> initializer: (int -> 'T) -> 'T array
Multiple items
module ValueOption from Microsoft.FSharp.Core

--------------------
type ValueOption<'T> = | ValueNone | ValueSome of 'T static member Some: value: 'T -> 'T voption static member op_Implicit: value: 'T -> 'T voption member IsNone: bool member IsSome: bool member Value: 'T static member None: 'T voption
val bind: binder: ('T -> 'U voption) -> voption: 'T voption -> 'U voption
val map: mapping: ('T -> 'U) -> voption: 'T voption -> 'U voption
Multiple items
type Vector3 = new: value: Vector2 * z: float32 -> unit + 3 overloads member CopyTo: array: float32 array -> unit + 2 overloads member Equals: other: Vector3 -> bool + 2 overloads member GetHashCode: unit -> int member Length: unit -> float32 member LengthSquared: unit -> float32 member ToString: unit -> string + 2 overloads member TryCopyTo: destination: Span<float32> -> bool static member (&&&) : left: Vector3 * right: Vector3 -> Vector3 static member ( * ) : left: Vector3 * right: Vector3 -> Vector3 + 2 overloads ...
<summary>Represents a vector with three single-precision floating-point values.</summary>

--------------------
Vector3 ()
Vector3(value: float32) : Vector3
Vector3(values: System.ReadOnlySpan<float32>) : Vector3
Vector3(value: Vector2, z: float32) : Vector3
Vector3(x: float32, y: float32, z: float32) : Vector3
Multiple items
type Matrix4x4 = new: value: Matrix3x2 -> unit + 1 overload member Equals: other: Matrix4x4 -> bool + 1 overload member GetDeterminant: unit -> float32 member GetElement: row: int * column: int -> float32 member GetHashCode: unit -> int member GetRow: index: int -> Vector4 member ToString: unit -> string member WithElement: row: int * column: int * value: float32 -> Matrix4x4 member WithRow: index: int * value: Vector4 -> Matrix4x4 static member ( * ) : value1: Matrix4x4 * value2: Matrix4x4 -> Matrix4x4 + 1 overload ...
<summary>Represents a 4x4 matrix.</summary>

--------------------
Matrix4x4 ()
Matrix4x4(value: Matrix3x2) : Matrix4x4
Matrix4x4(m11: float32, m12: float32, m13: float32, m14: float32, m21: float32, m22: float32, m23: float32, m24: float32, m31: float32, m32: float32, m33: float32, m34: float32, m41: float32, m42: float32, m43: float32, m44: float32) : Matrix4x4
Matrix4x4.CreateScale(scale: float32) : Matrix4x4
Matrix4x4.CreateScale(scales: Vector3) : Matrix4x4
Matrix4x4.CreateScale(scale: float32, centerPoint: Vector3) : Matrix4x4
Matrix4x4.CreateScale(scales: Vector3, centerPoint: Vector3) : Matrix4x4
Matrix4x4.CreateScale(xScale: float32, yScale: float32, zScale: float32) : Matrix4x4
Matrix4x4.CreateScale(xScale: float32, yScale: float32, zScale: float32, centerPoint: Vector3) : Matrix4x4
Matrix4x4.CreateTranslation(position: Vector3) : Matrix4x4
Matrix4x4.CreateTranslation(xPosition: float32, yPosition: float32, zPosition: float32) : Matrix4x4
Multiple items
module Result from Microsoft.FSharp.Core

--------------------
type Result<'T,'TError> = | Ok of ResultValue: 'T | Error of ErrorValue: 'TError
val defaultWith: defThunk: ('Error -> 'T) -> result: Result<'T,'Error> -> 'T

Type something to start searching.