XNA from F# and C#

I’ve gotten one of the first XNA 1.0 tutorials going in F# called from a tiny C# stub. I got the basic approach for the 3-way integration from grammarjack’s blog. The tutorial is documented HERE; it tests for collisions in 2D between falling "anvils" and a mobile "person," reminiscent of the old Space Invaders control model. The C# stub is a Visual Studio Express project, as are all official XNA applications. Its source code follows:

namespace MyGame
{
    static class Program
    {
        static void Main(string[] args)
        {
            File1.FSharpCalledFromCSharp(); // File1 refers to File1.fs, the F# DLL          
        }
    }
}

My F# version is a VERY straightforward translation of the original C# code of the tutorial — maybe "transliteration" is a better word. It’s side-effecting EVERYWHERE because that’s object-oriented programming. Construction starts with the usual field initialization followed by a “then” clause that makes some subordinate objects, followed by an “Initialize” callback that does even more setup after graphics has been partly set up, followed by “LoadGraphicsContent” which does even MORE setup even LATER. The main loop has an Update entry point and a Draw entry point which communicate by (you guessed it) side-effected fields. It may be interesting in the future to do a "topological inversion refactoring" of this code structure to sequence the side effects more monadically.

I preserved another deficiency of the original: various rectangle and vector types seem schizophrenic about floats, ints, or float32s. I just inserted whatever coercions necessary to get it to run. Perhaps later it would be interesting to consolidate and reconcile all that noise.

Despite these issues, the code is not hard to read to my eyes anyway, and is a little prettier than the C# version since I managed to sweep some counted loops into maps and filters.

Build a DLL in Visual Studio 2008 with F# added in, and run the program from the C# executable after adding a reference to the F# DLL. Here’s the F# code:

#light

#I @"c:\Program Files\Microsoft XNA\XNA Game Studio Express\v1.0\References\Windows\x86"
#r "Microsoft.Xna.Framework.dll"
#r "Microsoft.Xna.Framework.Game.dll"

open Collections
open Compatibility
open Idioms
open Microsoft.Xna.Framework
open Microsoft.Xna.Framework.Audio
open Microsoft.Xna.Framework.Content
open Microsoft.Xna.Framework.Graphics
open Microsoft.Xna.Framework.Input
open Microsoft.Xna.Framework.Storage
open System
open System.IO

type MyGame = class
    inherit Game as base
    val mutable graphics        : GraphicsDeviceManager
    val mutable content         : ContentManager
    val mutable personTexture   : Texture2D
    val mutable blockTexture    : Texture2D
    val mutable spriteBatch     : SpriteBatch
    val mutable personPosition  : Vector2
    val         PersonMoveSpeed : float32
    val mutable blockPositions  : Vector2 list
    val         BlockSpawnProb  : float
    val         BlockFallSpeed  : int
    val         random          : Random
    val mutable personHit       : bool (* set in "Update", tested in "Draw" *)
    val mutable safeBounds      : Rectangle (* would like this to be constant, but must be
                                               set in "Initialize" after graphics set up *)
    val         SafeAreaPortion : float
    
    new () as this = 
      { graphics        = null
        content         = null 
        personTexture   = null 
        blockTexture    = null 
        spriteBatch     = null 
        personPosition  = Vector2(0.0f)
        PersonMoveSpeed = 5.0f
        blockPositions  = []
        BlockSpawnProb  = 0.02
        BlockFallSpeed  = 2
        random          = Random ()
        personHit       = false
        safeBounds      = Rectangle(0,0,0,0)
        SafeAreaPortion = 0.05
        }
      then
        this.graphics <- new GraphicsDeviceManager(this)
        this.content  <- new ContentManager(this.Services)
        this.graphics.PreferredBackBufferWidth  <- 853
        this.graphics.PreferredBackBufferHeight <- 480
        
    override this.Initialize() =
        base.Initialize ()
        let vp = this.graphics.GraphicsDevice.Viewport
        this.safeBounds <- Rectangle 
            (int (float vp.Width * this.SafeAreaPortion), 
             int (float vp.Height * this.SafeAreaPortion),
             int (float vp.Width * (1.0 - 2.0 * this.SafeAreaPortion)), 
             int (float vp.Height * (1.0 - 2.0 * this.SafeAreaPortion)))
        this.personPosition.X <- 
            float32 (this.safeBounds.Width - this.personTexture.Width) / 2.0f
        this.personPosition.Y <-
            float32 (this.safeBounds.Height - this.personTexture.Height)
       
    override this.LoadGraphicsContent (loadAllContent : bool) = 
        if loadAllContent then
            this.blockTexture  <- this.content.Load<Texture2D>("Content/Block")
            this.personTexture <- this.content.Load<Texture2D>("Content/Person")
            this.spriteBatch   <- new SpriteBatch(this.graphics.GraphicsDevice)
            
    override this.UnloadGraphicsContent (unloadAllContent : bool) =
        if unloadAllContent then
            this.content.Unload ()

    override this.Draw(gameTime) = 
        let gd = this.graphics.GraphicsDevice
        gd.Clear(if this.personHit then Color.Orange else Color.CornflowerBlue)
        this.spriteBatch.Begin()
        this.spriteBatch.Draw(this.personTexture, this.personPosition, Color.White)
        List.iter
            (fun (v : Vector2) -> this.spriteBatch.Draw(this.blockTexture, v, Color.White))
            this.blockPositions
        this.spriteBatch.End()
        base.Draw (gameTime)
        
    override this.Update(gameTime) =
        let kb = Keyboard.GetState()
        let gp = GamePad.GetState(PlayerIndex.One)
        if (gp.Buttons.Back = ButtonState.Pressed || kb.IsKeyDown(Keys.Escape)) then
            this.Exit()
        if (kb.IsKeyDown(Keys.Left) || gp.DPad.Left = ButtonState.Pressed) then
            this.personPosition.X <- this.personPosition.X - this.PersonMoveSpeed
        if (kb.IsKeyDown(Keys.Right) || gp.DPad.Right = ButtonState.Pressed) then
            this.personPosition.X <- this.personPosition.X + this.PersonMoveSpeed
        this.personPosition.X <- 
            MathHelper.Clamp 
            (this.personPosition.X, 
             float32 this.safeBounds.Left, 
             float32 this.safeBounds.Right - float32 this.personTexture.Width)
        if this.random.NextDouble () < this.BlockSpawnProb then
            let x = this.random.NextDouble() * 
                    float (this.Window.ClientBounds.Width - this.blockTexture.Width)
            this.blockPositions <- 
                Vector2 (float32 x, - float32 this.blockTexture.Height)
                :: this.blockPositions
        this.blockPositions <-
            this.blockPositions.Map 
            (fun v -> Vector2 (v.X, v.Y + float32 this.BlockFallSpeed))
        let personRectangle = 
            Rectangle (int this.personPosition.X, int this.personPosition.Y, 
                       this.personTexture.Width, this.personTexture.Height)
        this.personHit <- 
            List.exists (fun (v : Vector2) -> 
                personRectangle.Intersects 
                    (Rectangle 
                        (int v.X, int v.Y, 
                         this.blockTexture.Width, 
                         this.blockTexture.Height))) 
                this.blockPositions
        this.blockPositions <-
            this.blockPositions.Filter (fun v -> 
            v.Y <= float32 this.Window.ClientBounds.Height)
        
    end (*class*)
     
let FSharpCalledFromCSharp () =
    let game = new MyGame()
    game.Run()
    

Advertisements

~ by rebcabin on December 13, 2007.

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 )

Twitter picture

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

Facebook photo

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

Google+ photo

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

Connecting to %s

 
%d bloggers like this: