F# interactive for level design

If you’ve tried F#, you’re probably aware that there’s also a REPL to interactively evaluate expressions. It’s also useful for quickly modifying game levels. The normal process for game level design has been to use a level editor such as Tiled and then load it in to your game. However, we then don’t have things like our character in game to test whether a platform is reachable or whether a power up is too hidden. This meant that we would normally have to edit the level, reload the level and then get back to our original position. Instead, imagine being able to run your game and then add new level items on the fly.

Let’s walk through and get to a stage where we can dynamically add content to our screen as we go along. If you want a ready to go project, you can get it from here but I’ll walk through getting to that stage here. First we need to create a new F# project. I chose to put it all in a class library. Let’s add the required references to the Monogame framework and SDL.dll. We’ll be running this on Windows so we’ll need to add the WindowsGL libraries which are probably stored in “C:\Program Files (x86)\MonoGame\v3.0\Assemblies\WindowsGL” Add references to the following:

  • MonoGame.Framework.dll
  • Lidgren.Network.dll
  • OpenTK.dll
  • Tao.SDL.dll

We also need to add the file SDL.dll from that directory to the project with build action none and Copy to output directory copy if newer.

We won’t be making a complex game here, we’ll just be making something which we can quickly demonstrate the REPL functionality. Let’s add a new file called REPLActor.fs, this will essentially allow us to represent our textures with a position as well and make it easier to load in certain content. So add the following to the file.

module REPLActor

open Microsoft.Xna.Framework
open Microsoft.Xna.Framework.Content
open Microsoft.Xna.Framework.Graphics

type Actor =
 Texture : Texture2D;
 Position : Vector2

let CreateActor (content:ContentManager) fileName position =
 let t = content.Load<Texture2D> fileName
 { Texture = t; Position = position }

Now beneath that file, add a file called REPLGame.fs and add the following code.

module REPLGame

open Microsoft.Xna.Framework
open Microsoft.Xna.Framework.Graphics
open REPLActor

type Game1 () as this =
 inherit Game ()

do this.Content.RootDirectory <- "Content"
 let graphics = new GraphicsDeviceManager(this)
 let mutable spriteBatch = Unchecked.defaultof<SpriteBatch>

let mutable (actors:Actor list) = []

let DrawActor (sb:SpriteBatch) actor =
 do sb.Draw(actor.Texture, actor.Position, Color.White)

member this.Actors
 with get() = actors
 and set(v) = actors <- v

override this.Initialize() =
 do spriteBatch <- new SpriteBatch(this.GraphicsDevice)
 do base.Initialize ()

override this.Draw gameTime =
 do this.GraphicsDevice.Clear Color.CornflowerBlue
 let DrawActor' = DrawActor spriteBatch
 do spriteBatch.Begin()
 |> List.iter DrawActor'
 do spriteBatch.End()

Now build the project and it should hopefully compile. That’s all the compilation we’ll be needing to do now.

Next we’ll need to create an F# script file. This is where we’ll keep all of the helper functions which we’ll need for when we run the game.

The first step is to load in all of the required DLLs, this is done as follows.

#I @"bin\Debug"
#r @"REPLGame.dll"
#r @"C:\Program Files (x86)\MonoGame\v3.0\Assemblies\WindowsGL\MonoGame.Framework.dll"
#r "WindowsBase"

The #I tells the interactive window to look in that specified directory for files and #r tells it to load in the specified DLL.

Normally when we run our game with game.Run(), it blocks the thread that called it. This is no good for us as it means that we can’t then use the REPL, so what we need to do is run the game on a background thread. Normally we might wrap our call to game.Run() in an async block. We can’t do that though as our draw needs to be called from the UI thread. If it isn’t then we’ll immediately get an exception thrown from OpenGL about the context. So we’ll create a helper function which is capable of running the game off in it’s own thread, this is done as follows.

open REPLActor
open REPLGame
open Microsoft.Xna.Framework
open Microsoft.Xna.Framework.Graphics
open System.Threading

let RunInBackground() =
 let (game:REPLGame.Game1 ref) = ref Unchecked.defaultof<REPLGame.Game1>
 let start() =
 game := new REPLGame.Game1()
 let g = !game
 let thread = Thread start
 thread.IsBackground <- true
 thread.SetApartmentState ApartmentState.STA
 !game, thread

Here we create a new thread which runs as an STA in the background and we then start it. I found that I needed the Thread.Sleep in order to prevent it from immediately returning null due to a race condition. So we pause for a while until it has successfully created our game. Then we return it and the thread it’s running on. We also need to call our function which creates everything so let’s add another function as follows.

let game, thread = RunInBackground()

Now you should see a window appear with the familiar cornflower blue background.

We’re nearly ready, now we just need to make a couple of other changes in our script file. First is the content root directory. This is usually \bin\Debug\Content but taken from the current working directory which when using FSI is a temp directory. So what we’ll do is use a constant we are given called __SOURCE_DIRECTORY__ we’ll then append the usual \bin\Debug\Content. So we set it as follows.

do game.Content.RootDirectory <- __SOURCE_DIRECTORY__ + @"bin\Debug\Content"

I’ll also create another function which just wraps our normal CreateActor method with the ContentManager from the game as follows.

let Create = CreateActor game.Content

Now we can easily add more actors into our scene from within the F# REPL as follows.

let p = Vector2(10.0f,10.0f);;
let actor = Create "player.png" p;;
do game.Actors <- actor :: game.Actors;;

One comment

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 )

Connecting to %s