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
|
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.
val string: value: 'T -> string
--------------------
type string = System.String
val float32: value: 'T -> float32 (requires member op_Explicit)
--------------------
type float32 = System.Single
--------------------
type float32<'Measure> = float32
val int: value: 'T -> int (requires member op_Explicit)
--------------------
type int = int32
--------------------
type int<'Measure> = int
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
<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>
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>
(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>
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>
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
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
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(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(xPosition: float32, yPosition: float32, zPosition: float32) : Matrix4x4
module Result from Microsoft.FSharp.Core
--------------------
type Result<'T,'TError> = | Ok of ResultValue: 'T | Error of ErrorValue: 'TError
Mibo