Sunday, March 29, 2009

Structs that look like records

I've spent some time trying to improve my asteroids clone in F# for the XBox360. I present the first results in this post.

But first, some non-technical considerations. Microsoft has now made available to XNA creators (i.e. game programmers) the download statistics of their games. A vast majority of creators seem disappointed, but I think that's more a sign of too high expectations than a failure of the platform itself.
One of the creators reported that full game downloads went down when Microsoft raised the time limit in trial versions from 4 minutes to 8 minutes. Players have the ability to download a trial version of an XBox Live Community Game before buying the full version. Trial versions typically have some features disabled (this is up to the game creator) and a mandatory time limit (which cannot be controlled by game creators).
What happened there, apparently, is that the game is played in short sessions shorter than 8 minutes at a time, making the free trial version sufficiently appealing that gamers did not feel a need to buy the full version.

Asteroids is not typically a game you play for long periods at a time, so I expect my game may suffer the same problem.

Back to the technical side of things. I found a way to make structs look like records. Here is the declaration:


type State =
struct
val props : Properties
val pos : Vector3
val speed : Vector3
val impulses : Vector3
val force : Vector3

new (props, pos, speed, impulses, force) =
{ props = props ;
pos = pos ;
speed = speed ;
impulses = impulses ;
force = force }

static member inline Default (props, ?pos, ?speed, ?impulses, ?force) =
State(
props,
pos = defaultArg pos Vector3.Zero,
speed = defaultArg speed Vector3.Zero,
impulses = defaultArg impulses Vector3.Zero,
force = defaultArg force Vector3.Zero)

member inline x.Clone (?pos, ?speed, ?impulses, ?force) =
new State (
x.props,
pos = defaultArg pos x.pos,
speed = defaultArg speed x.speed,
impulses = defaultArg impulses x.impulses,
force = defaultArg force x.force )


... and here is how it's used:


member x.ApplyCenterForce (v : Vector3) =
let force = x.force + v
x.Clone (force = force)


This is very similar to the old code:


let applyCenterForce (v : Vector3) (x : State) =
let force = x.force + v
{ x with force = force }


Looking back at the first snipplet, notice how I declared "x.Clone":


static member inline Default (props, ?pos, ?speed, ?impulses, ?force) =

The question marks before the arguments mean that these arguments are optional. If the caller does not provide them, they automatically get the "None" value.

new State (
x.props,
pos = defaultArg pos x.pos,
speed = defaultArg speed x.speed,
impulses = defaultArg impulses x.impulses,
force = defaultArg force x.force )

"defaultArg" is a convenience F# standard function which returns the first value unless it's "None", in which case the second value is returned.

The function was declared "inline" to avoid creating lots of "Option" objects at call sites. For instance, the disassembled version of "ApplyCenterForce" looks like this:

public HeavyObject.State ApplyCenterForce(Vector3 v)
{
return new HeavyObject.State(this._props, this._pos, this._speed, this._impulses, this._force + v);
}


Compare with the version without "inline":


public HeavyObject.State ApplyCenterForce(Vector3 v)
{
Vector3 vector = this._force + v;
return this.Clone(null, null, null, new Option(vector));
}


As the entire point with structs is to avoid allocating objects on the heap, having an "Option" created for each call to ApplyCenterForce is not quite acceptable.

It's not clear that using structs is a clear win in all situations. Performance on the PC is negatively affected, as garbage collection is almost free there, whereas copying structs when passing them as parameters to functions isn't. Performance on the XBox360, however, is improved, as garbage collection is kept under control.

No comments: