Using FAKE to automate building MonoGame content (and draw some fractals)

3993026943_7607489eed_m

This post is part of the F# Advent Calendar in English 2017.

MonoGame is an excellent cross-platform framework for building 2D and 3D games in .NET, including F#.  Originally based on Microsoft’s now-deprecated XNA, it now supports WIndows, Linux, Mac, iOS, Android, PlayStation, Xbox and the Nintendo Switch.

As well as games, we can use MonoGame to build any application that needs high-performance graphics.  In this post, I’ll show how using FAKE makes for a much smoother development process when working with MonoGame.

Content

Almost any game will need some non-code assets, known as “content”.  These could include images, sounds, 3D models, shader definitions and fonts.

As part of the build process, all this content needs to be converted from its original formats (e.g. JPEGs or shader text files) into .xnb files for use in the final application.  Typically these .xnb files are included as part of the application project, but they will need to be updated if the content itself changes.  Of course, we would like to automate this as far as possible.

MonoGame itself includes a command-line tool for building content.  A common way of automating this is to use the MonoGame Pipeline tool, but it’s a bit clunky and adds some manual steps to the build process.  This is not much of a problem if your content is static, but if you’re changing a shader with each build, it gets tedious fairly quickly.

Using FAKE

FAKE is a tool for automating almost any kind of build task.  Although written in .NET, it can happily be used for C# projects as well.  Build pipelines are written in F# although the FAKE DSL and libraries mean you don’t need much knowledge of the language.

Here is a simple example of an F# build script:

// include Fake lib
#r "packages/FAKE/tools/FakeLib.dll"
open Fake

// Properties
let buildDir = "./build/"

// Targets
Target "Clean" (fun _ ->
	CleanDir buildDir
)

Target "BuildApp" (fun _ ->
	!! "src/app/**/*.csproj"
	  |> MSBuildRelease buildDir "Build"
	  |> Log "AppBuild-Output: "
)

// Dependencies
"Clean"
  ==> "BuildApp"

// start build
RunTargetOrDefault "BuildApp"

Different stages of the build pipeline are called “targets”, which are defined using F# functions, and the ==> operator is used to define dependencies between them.

FAKE has a huge number of built-in tasks to do everything from building projects and running tests to running code analysis and publishing NuGet packages.  As well as the built-in tasks, anything that can be run from the command line can also be turned into a task.

Automating MonoGame content

With a little work we can automate the conversion of content files to .xnb files as required by MonoGame.  Here is a function to do just that:

let MonoGameContent (setParams : MonoGameContentParams -> MonoGameContentParams) (content : string seq) =
    let details = content |> separated ", "
    let parameters = MonoGameContentDefaults |> setParams
    let tool = parameters.ToolPath
    let args = buildMonoGameContentArgs parameters content
    let result = 
        ExecProcess (fun info -> 
            info.FileName <- tool
            info.WorkingDirectory <- getWorkingDir parameters
            info.Arguments <- args) parameters.TimeOut
    let errorDescription error = 
        match error with
        | FatalError x -> sprintf "MonoGame content building failed. Process finished with exit code %s (%d)." x error
        | _ -> "OK"
    match result with
    | OK -> ()
    | _ -> raise (BuildException(errorDescription result, content |> List.ofSeq))

I haven’t included the simple helper functions that back this – the full file can be found here.  The only slight irritation is that I’ve had to include the full path of the command-line tool (MGCB.exe).

Once we have this in place, it’s simple to add a target to a FAKE build script to build our content:

#load "MonoGameContent.fsx"

let intermediateContentDir = "./intermediateContent"
let contentDir = "./Fractals"

let contentFiles =
    !! "**/*.fx"
    ++ "**/*.spritefont"

Target "BuildContent" (fun _ ->
    contentFiles
        |> MonoGameContent (fun p ->
            { p with
                OutputDir = contentDir;
                IntermediateDir = intermediateContentDir;
            }))

In this example, we’re picking up any files with .fx or .spritefont extensions to be build as content, regardless of where they are under the solution folder.  You could also ask for all files under a particular folder to be included.

The resulting .xnb files will be placed in the folder specified by contentDir, with any subfolder structure retained – for example, “Content\hello.fx” would be built to “Fractals\Content\hello.xnb” in this example.  Typically the target file will be included in the application project with a build action of “Content”.

An intermediate folder is also needed.  MonoGame will use this to cache content, so the build script will not re-build anything that hasn’t changed.  This saves time for large projects.

MonoGame in action: Fractals!

Originally inspired by this blog post (in particular, I’ve copied the colour map) my fractal viewer can be used to draw pretty pictures from the Mandelbrot set and some Julia sets.  It will happy render a full-screen fractal 60 times per second on any reasonable graphics card.

3993025815_61a8c65834_z

The full source code can be found here.  As well as scrolling around with the arrow keys, you can zoom in and out (PageUp/PageDown) to an extraordinary degree before the GPU’s single-precision floating-point arithmetic breaks down.  Holding down Shift slows down the scrolling.

Switching between the Mandelbrot and Julia sets is done with “J” and “M”, pressing “P” will show the current view parameters and PrtScrn saves a screenshot.

Additionally, when the Julia set is shown, the seed value can be changed with the A/W/S/D keys – this transforms the fractal into all sorts of different patterns.

More fractals!

As well as playing with these particular fractals, new ones can be created by changing the iterated function that is applied to calculate the colour of each pixel.  For the Mandelbrot set, the function is zz²+c where z and c are complex numbers.

The implementation of this in HLSL (the language DirectX shaders are written in) is straightforward:

float2 ComplexMultiply(float2 a, float2 b)
{
    return float2(a.x * b.x - a.y * b.y, a.x * b.y + a.y * b.x);
}

float2 ComplexSquare(float2 z)
{
    return ComplexMultiply(z, z);
}

float2 Function(float2 z, float2 offset)
{
    return ComplexSquare(z) + offset;
}

cubeIf we change this, say to cube rather than square z, we only need to run the build script again to compile the shader, which takes just a second or two.  This results in a different fractal, as seen here.

Have fun playing with fractals, and don’t forget to share your favourites!

Advertisements

, , ,

  1. F# Advent Calendar in English 2017 – Sergey Tihon's Blog
  2. F# Weekly #52 – Merry Christmas! – Sergey Tihon's Blog

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

w

Connecting to %s

%d bloggers like this: