This article describes an easy to use custom XNA game component that allows to run computations in parallel with rendering.
The typical game loop is often divided in two steps, update and draw. I suggest a slightly different workflow divided in three steps, two of which run in parallel: update, compute and draw. The traditional update stage is divided in two new steps, update and compute.
In this new setting, update is used for receiving inputs from the player and the network, in the case of an online multiplayer game. Compute is potentially cpu-hungry operation implemented in a purely functional way. Rendering plays the same role as usual.
The code for the entire component fits in little over 100 lines, see below.
namespace CleverRake.XnaUtils
open Microsoft.Xna.Framework
open System.Threading
type IFramePerSecondCounter =
abstract FramesPerSecond : float
/// An update-able and drawable game component which performs light updates on the main
/// thread, then draws on a separate thread in parallel of more computation-heavy updates.
/// initialize_fun is called when assets are loaded.
/// dispose is called when the component is disposed, and should be used to unload assets.
/// update_fun takes a GameTime and a state, and should produce draw data and computation
/// data.
/// draw_fun takes a game time and draw data and should return nothing.
/// compute_fun takes a game time and computation data and should return a new state.
type ParallelUpdateDrawGameComponent<'State, 'DrawData, 'ComputationData>
(game,
initial_state : 'State,
initialize_fun : unit -> unit,
update_fun : GameTime -> 'State -> 'DrawData * 'ComputationData,
compute_fun : GameTime -> 'ComputationData -> 'State,
draw_fun : GameTime -> 'DrawData -> unit,
dispose : unit -> unit) =
inherit DrawableGameComponent(game)
let mutable state = initial_state
let mutable draw_data = Unchecked.defaultof<'DrawData>
let mutable compute_data = Unchecked.defaultof<'ComputationData>
let mutable gt_shared = GameTime()
let mutable enabled = true
let mutable update_order = 0
let signal_start = new AutoResetEvent(false)
let mutable kill_requested = false
let signal_done = new AutoResetEvent(false)
let do_compute() =
#if XBOX360
// Set affinity
// 0 and 2 are reserved, I assume the "main" thread is 1.
Thread.CurrentThread.SetProcessorAffinity(3)
#endif
while not kill_requested do
signal_start.WaitOne() |> ignore
state <- compute_fun gt_shared compute_data
signal_done.Set() |> ignore
let compute_thread = new Thread(new ThreadStart(do_compute))
// Must be called from the main thread.
let post_compute_then_draw gt =
if not kill_requested then
let state = state
gt_shared <- gt
signal_start.Set() |> ignore
draw_fun gt draw_data
signal_done.WaitOne() |> ignore
let mutable frameCounter = 0
let mutable timeCounter = 0.0
let mutable fps = 0.0
let fpsUpdatePeriod = 0.3
do
compute_thread.IsBackground <- true
compute_thread.Start()
override this.Initialize() =
base.Initialize()
initialize_fun()
override this.Update(gt) =
if base.Enabled then
base.Update(gt)
let draw, compute = update_fun gt state
draw_data <- draw
compute_data <- compute
override this.Draw(gt) =
if base.Visible then
base.Draw(gt)
post_compute_then_draw gt
else
state <- compute_fun gt compute_data
timeCounter <- timeCounter + gt.ElapsedGameTime.TotalSeconds
frameCounter <- frameCounter + 1
if timeCounter > fpsUpdatePeriod then
fps <- float frameCounter / timeCounter
timeCounter <- timeCounter - fpsUpdatePeriod
frameCounter <- 0
interface System.IDisposable with
member this.Dispose() =
base.Dispose()
dispose()
signal_start.Dispose()
signal_done.Dispose()
interface IFramePerSecondCounter with
member this.FramesPerSecond = fps
member this.FramesPerSecond = fps
member this.RequestKill() =
kill_requested <- false
signal_start.Set() |> ignore
member this.WaitUntilDead() =
compute_thread.Join()
member this.IsDead = not(compute_thread.IsAlive)
To be noted is that this component isn't meant to be extended the usual way which consists in inheriting and overriding. I'm experimenting with staying away from traditional OOP. I'm not sure it's the nicest way to do things, but building a wrapper from which to inherit shouldn't be hard. The OOP way will definitely be more appealing when using this component from C#, so I'll probably have to take this step anyway.
The constructor of the class takes an initial state and an initializing function which shouldn't be mixed. The initializing function is meant for loading assets. The dispose function should undo the effects of the initializing function and should dispose of any disposable asset.
The initial state represents the state of the world when the game starts. This is typically built from a level description when entering a new level, or by loading a previously saved session.
When the game is running, the state is passed to the update function, which isn't so well named, in retrospect. Its role is to prepare data for the compute and draw functions, which are run in parallel. In most cases I expect that ComputationData and State are the same type.
As is common in parallel update/draw scenarios, the draw function renders the state produced by the compute function in the previous frame. This is necessary to avoid race conditions between the compute and draw functions.
I am not yet sure whether I'm happy with my implementation of IDisposable, and this may change if turns out to be impractical or wrong. I've been using this component in a new game I'm working on, and I have been positively surprised. I hope you will find it useful!