Well, it's been a while but I have finally had a chance to have a proper play around with XNA ... and it's great fun! To start with I have ported the
Primitives Sample across to F#. The complete project, which will compile for PC and XBox 360, is
here. It was an interesting job and a couple of things came up;
* The new class syntax is really nice, but it can't always, it seems, be used with XNA. It seems to be a requiment imposed by the framework that you construct objects within a class constructor which must refer to the class itself - I can't see how to do that. I think you'd need a syntax like
type PrimitiveBatch(device : GraphicsDevice) = class as xto do this, but I am not sure what the consequences would be. Either way the old syntax with "new(..) = {...} then" works fine.
* I ported a class that needs to be constructed after the creation of the main game object, but also needs to be a member variable of said game. This worked OK from C# but from F# the magic of null needed for initialisation disappeared. To get round it I made the object an Option type - works fine, and I am happy with the results, but did take a bit of thinking about.
* The sample manages an array of VertexPositionColor structs. In C# the code started out as
VertexPositionColor[] vertices = new VertexPositionColor[DefaultBufferSize]; and was used like so;
vertices[positionInBuffer].Position = new Vector3(vertex, 0);
vertices[positionInBuffer].Color = color;
In F# what I came up with is;
Array.create DefaultBufferSize
(new VertexPositionColor(new Vector3(0.0f,0.0f,0.0f), new Color(0uy,0uy,0uy)))
and;
let set_vertex idx =
let mutable t = vertices.[idx]
t.Position <- new Vector3(vertex, 0.0f)
t.Color <- color
vertices.[idx] <- t
set_vertex positionInBuffer
I am wondering whether these two bits of code really do the same things under the hood. My impression is the C# version creates a flat array and copys data into it. It's not clear to me that the F# version is doing the same thing. I tried playing around with struct types in fsi.exe, but kept coming up errors like;
error: forceM: envGetConsB: failed: x = {mrefParent = {trefScope = ScopeRef_local;
trefNested = ["FSI_0003"];
trefName = "ts";};
mrefCallconv = Callconv (CC_instance,CC_default);
mrefArity = 0;
mrefName = ".ctor";
In particular, I cant seem to update a mutable value in a struct. It's quite probable I am doing something silly, but I cant seem to find much in the way of an example for using them.
Anyhow, here's a version of the code which will compile and run directly - it's pretty much the same as the full XNA Game Studio example except the one dependacy on the content pipeline is commented out.
Cheers,
Andy.
(**************************************************************************)
#light
#if XBOX360
#I @"C:\Program Files\Microsoft XNA\XNA Game Studio Express\v1.0\References\Xbox360"
#else
#I @"C:\Program Files\Microsoft XNA\XNA Game Studio Express\v1.0\References\Windows\x86"
#endif
#r "Microsoft.Xna.Framework.dll"
#r "Microsoft.Xna.Framework.Game.dll"
open System
open System.Collections.Generic
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
(**************************************************************************)
type PrimitiveBatch(device : GraphicsDevice) = class
let DefaultBufferSize = 500
let vertices =
Array.create DefaultBufferSize
(new VertexPositionColor(new Vector3(0.0f,0.0f,0.0f), new Color(0uy,0uy,0uy)))
let mutable positionInBuffer = 0
let vertexDeclaration = new VertexDeclaration(device, VertexPositionColor.VertexElements);
let basicEffect =
let b = new BasicEffect(device, null)
b.Projection <-
Matrix.CreateOrthographicOffCenter(0.0f,
float32 device.Viewport.Width,
float32 device.Viewport.Height,
0.0f, 0.0f, 1.0f)
b
let mutable primitiveType = PrimitiveType.TriangleList
let mutable numVertsPerPrimitive = 3
let mutable hasBegun = false
let mutable isDisposed = false
let NumVertsPerPrimitive(primitive) =
match primitive with
| PrimitiveType.PointList -> 1
| PrimitiveType.LineList -> 2
| PrimitiveType.TriangleList -> 3
| _ -> raise (new InvalidOperationException("primitive is not valid"))
let Dispose' disposing =
if (disposing && not isDisposed) then
if (vertexDeclaration <> null) then
vertexDeclaration.Dispose()
if (basicEffect <> null) then
basicEffect.Dispose()
isDisposed <- true
interface IDisposable with
member x.Dispose () =
Dispose' true
GC.SuppressFinalize(x);
end
member x.Begin primType =
if hasBegun then
raise (new InvalidOperationException ("End must be called before Begin can be called again."))
if (primitiveType = PrimitiveType.LineStrip ||
primitiveType = PrimitiveType.TriangleFan ||
primitiveType = PrimitiveType.TriangleStrip) then
raise (new NotSupportedException
("The specified primitiveType is not supported by PrimitiveBatch."))
primitiveType <- primType
numVertsPerPrimitive <- NumVertsPerPrimitive(primitiveType)
device.VertexDeclaration <- vertexDeclaration
basicEffect.Begin()
basicEffect.CurrentTechnique.Passes.get_Item(0).Begin() (* ??? get_Item instead of .[0] ??? *)
hasBegun <- true
member x.AddVertex(vertex:Vector2, color:Color) =
if not hasBegun then
raise (new InvalidOperationException
("Begin must be called before AddVertex can be called."))
let newPrimitive = (positionInBuffer % numVertsPerPrimitive) = 0
if (newPrimitive &&
(positionInBuffer + numVertsPerPrimitive) >= vertices.Length) then
x.Flush()
let set_vertex idx =
let mutable t = vertices.[idx]
t.Position <- new Vector3(vertex, 0.0f)
t.Color <- color
vertices.[idx] <- t
set_vertex positionInBuffer
positionInBuffer <- positionInBuffer + 1
member x.End() =
if (not hasBegun) then
raise (new InvalidOperationException
("Begin must be called before End can be called."))
x.Flush()
basicEffect.CurrentTechnique.Passes.get_Item(0).End()
basicEffect.End()
hasBegun <- false
member x.Flush() =
if (not hasBegun) then
raise (new InvalidOperationException
("Begin must be called before Flush can be called."))
if (positionInBuffer <> 0) then
let primitiveCount = positionInBuffer / numVertsPerPrimitive
device.DrawUserPrimitives<VertexPositionColor>(primitiveType, vertices, 0, primitiveCount)
positionInBuffer <- 0
end
(**************************************************************************)
let (|Opt|) = function
| Some x -> x
| None -> failwith "Expecting something"
type MyGame = class
inherit Game as base
val NumStars : int;
val PercentBigStars : float;
val MinimumStarBrightness : int;
val MaximumStarBrightness : int;
val ShipSizeX : float32;
val ShipSizeY : float32;
val ShipCutoutSize : float32;
val SunSize : float32;
val mutable graphics : GraphicsDeviceManager;
val mutable content : ContentManager;
val mutable primitiveBatch : PrimitiveBatch option;
val stars : List<Vector2>;
val starColors : List<Color>;
val mutable courierNew : SpriteFont
val mutable fgBatch : SpriteBatch
new () as x =
{
NumStars = 500
PercentBigStars = 0.2
MinimumStarBrightness = 56
MaximumStarBrightness = 255
ShipSizeX = 10.0f
ShipSizeY = 15.0f
ShipCutoutSize = 5.0f
SunSize = 30.0f
primitiveBatch = None
graphics = null
content = null
stars = new List<Vector2>();
starColors = new List<Color>();
courierNew = null
fgBatch = null
}
then
x.graphics <- new GraphicsDeviceManager(x)
x.graphics.PreferredBackBufferWidth <- 853
x.graphics.PreferredBackBufferHeight <- 480
x.content <- new ContentManager(x.Services);
override x.Initialize() =
base.Initialize();
x.CreateStars();
member x.CreateStars() =
let random = new Random()
let screenWidth = x.graphics.GraphicsDevice.Viewport.Width
let screenHeight = x.graphics.GraphicsDevice.Viewport.Height
for i=0 to x.NumStars-1 do
let where = new Vector2(float32 (random.Next(0, screenWidth)), float32 (random.Next(0, screenHeight)));
let greyValue = byte (random.Next(x.MinimumStarBrightness, x.MaximumStarBrightness))
let color = new Color(greyValue, greyValue, greyValue);
if (((random.NextDouble())) > x.PercentBigStars) then
x.starColors.Add(color);
x.stars.Add(where);
else
for j=0 to 3 do
x.starColors.Add(color);
x.stars.Add(where);
x.stars.Add(where + Vector2.UnitX);
x.stars.Add(where + Vector2.UnitY);
x.stars.Add(where + Vector2.One);
override x.LoadGraphicsContent(loadAllContent) =
if loadAllContent then
x.primitiveBatch <- Some(new PrimitiveBatch(x.graphics.GraphicsDevice))
//x.courierNew <- x.content.Load<SpriteFont>("Courier New")
x.fgBatch <- new SpriteBatch( x.graphics.GraphicsDevice )
override x.UnloadGraphicsContent(unloadAllContent) =
if (unloadAllContent) then
x.content.Unload()
override x.Update(gameTime) =
if ((GamePad.GetState(PlayerIndex.One).Buttons.Back = ButtonState.Pressed) || Keyboard.GetState().IsKeyDown(Keys.Escape)) then
x.Exit();
base.Update(gameTime);
override x.Draw(gameTime) =
x.graphics.GraphicsDevice.Clear(Color.Black)
let screenWidth = float32 x.graphics.GraphicsDevice.Viewport.Width
let screenHeight = float32 x.graphics.GraphicsDevice.Viewport.Height
x.DrawSun(new Vector2(screenWidth / 2.0f, screenHeight / 2.0f))
x.DrawShip(new Vector2(100.0f, screenHeight / 2.0f))
x.DrawShip(new Vector2(screenWidth - 100.0f, screenHeight / 2.0f))
x.DrawStars()
//x.DrawText gameTime
base.Draw(gameTime)
member x.DrawText (time : GameTime) =
let ms = float time.ElapsedGameTime.Milliseconds
let str = "ms = " ^ string_of_float ms ^ " fps = " ^ string_of_float (1000.0 / ms)
let FontRotation = 0.0f
let FontOrigin = Vector2(0.0f,0.0f)
let FontPos = Vector2(100.0f, 100.0f)
x.fgBatch.Begin()
x.fgBatch.DrawString(x.courierNew, str, FontPos, Color.LightGreen, FontRotation, FontOrigin, 1.0f, SpriteEffects.None, 0.5f);
x.fgBatch.End()
member x.DrawStars() =
let Opt primitiveBatch = x.primitiveBatch
primitiveBatch.Begin(PrimitiveType.PointList)
for i=0 to x.stars.Count - 1 do
primitiveBatch.AddVertex(x.stars.[ i ], x.starColors.[ i ])
primitiveBatch.End()
member x.DrawShip(where) =
let Opt primitiveBatch = x.primitiveBatch
primitiveBatch.Begin(PrimitiveType.LineList);
primitiveBatch.AddVertex(where + new Vector2(0.0f, -x.ShipSizeY), Color.White);
primitiveBatch.AddVertex(where + new Vector2(-x.ShipSizeX, x.ShipSizeY), Color.White);
primitiveBatch.AddVertex(where + new Vector2(-x.ShipSizeX, x.ShipSizeY), Color.White);
primitiveBatch.AddVertex(where + new Vector2(0.0f, x.ShipSizeY - x.ShipCutoutSize), Color.White);
primitiveBatch.AddVertex(where + new Vector2(0.0f, x.ShipSizeY - x.ShipCutoutSize), Color.White);
primitiveBatch.AddVertex(where + new Vector2(x.ShipSizeX, x.ShipSizeY), Color.White);
primitiveBatch.AddVertex(where + new Vector2(x.ShipSizeX, x.ShipSizeY), Color.White);
primitiveBatch.AddVertex(where + new Vector2(0.0f, -x.ShipSizeY), Color.White);
primitiveBatch.End();
member x.DrawSun(where) =
let Opt primitiveBatch = x.primitiveBatch
primitiveBatch.Begin(PrimitiveType.LineList)
primitiveBatch.AddVertex(where + new Vector2(0.0f, x.SunSize), Color.White);
primitiveBatch.AddVertex(where + new Vector2(0.0f, -x.SunSize), Color.White);
primitiveBatch.AddVertex(where + new Vector2(x.SunSize, 0.0f), Color.White);
primitiveBatch.AddVertex(where + new Vector2(-x.SunSize, 0.0f), Color.White);
let sunSizeDiagonal = (float32 (Math.Cos(float MathHelper.PiOver4)) * x.SunSize)
primitiveBatch.AddVertex(where + new Vector2(-sunSizeDiagonal, sunSizeDiagonal), Color.Gray);
primitiveBatch.AddVertex(where + new Vector2(sunSizeDiagonal, -sunSizeDiagonal), Color.Gray);
primitiveBatch.AddVertex(where + new Vector2(sunSizeDiagonal, sunSizeDiagonal), Color.Gray);
primitiveBatch.AddVertex(where + new Vector2(-sunSizeDiagonal, -sunSizeDiagonal), Color.Gray);
primitiveBatch.End();
end
(**************************************************************************)
let main() =
let game = new MyGame()
game.Run()
(**************************************************************************)
do main()