Table of Contents

The game loop

A game derives from Game, passes its window options to the base constructor, and calls Run(). Run() creates the services, opens the window, and runs the loop until the game exits. It returns when the loop ends, and the game is disposed after that:

using var game = new MyGame();
game.Run();

The order of a run

Step Method When it runs
1 Configure(IServiceCollection) Once, first thing in Run(). Register services here.
2 IRunOnInitialization.Initialize() Once, for each service the engine finds that implements it.
3 IAttachOnGameStart.Attach(IGame) Once, for each service that implements it.
4 Initialize() Once, when the window, renderer and services exist.
5 Update(IGameTime) Zero, one or more times per frame, at a fixed rate.
6 BeginFrame(IGameTime) Once per frame, after updating and before drawing.
7 ConfigureRenderGraph(FrameGraphBuilder, IGameTime) Once per frame. Adds the frame's render passes.
8 Draw(IGameTime) Once per frame, inside the default render pass.
9 EndFrame(IGameTime) Once per frame, after the render passes have run.

Steps 5 to 9 repeat until the game exits. The services are built after Configure, so in Configure, and before Run() is called, GetService and AddComponent throw an InvalidOperationException. From step 2 on, the engine's services and the game's own are available.

Once per frame, before any update step, the engine runs the work posted to the game's thread; see Work from other threads. Before each update, the engine pumps window and input events and updates the keyboard, mouse and input bindings. For a game that opts in to being started by a link (a URL with its own scheme, such as mygame://join/abc), it also raises any link that has arrived; see Starting a game by a link. It then runs Update, and after the last update of the frame, the drawing steps.

Update and timing

Update runs at a fixed 60 steps a second, whatever the frame rate. Each step has Elapsed of exactly 1/60 of a second. A frame that takes longer than 1/60 of a second runs more than one update, and a fast frame may run none. Put movement and game logic in Update, and use DeltaSeconds to scale it:

protected override void Update(IGameTime gameTime)
{
  position += velocity * gameTime.DeltaSeconds;
}

Draw runs once per frame, and its gameTime carries the real time since the last frame. That time is capped at a quarter of a second, so a long stall does not make the next frame jump.

IGameTime has three members:

  • TotalTime: the time since the loop started.
  • Elapsed: the time covered by this call, as a TimeSpan.
  • DeltaSeconds: the same as Elapsed, as a float.

Frames are not drawn while the window is hidden. Offscreen games, described in Windows and display, always draw.

Work from other threads

Code that runs on another thread, such as a packet handler on a network thread, must not change game state directly, because the loop reads that state every frame. Post the change to the game's thread instead, with the engine's IGameThread:

var gameThread = GetService<IGameThread>();
gameThread.Post(() => world.Spawn(enemy));

Post is safe to call from any thread, including the game's own. The loop runs the posted actions once a frame, before its update steps, so an action runs at the start of the next frame and that frame's Update already sees its change. Actions run in the order they were posted. An action that throws is logged as an error, and the rest still run. An action posted while the queue is being run waits for the frame after. The queue is also run while the game is paused, so the work is not held until the game resumes.

Drawing steps

ConfigureRenderGraph builds the frame's render passes. Its default adds one pass, which calls Draw and then draws the visible IDrawable components. A game that needs a clear colour, several passes or a render target overrides it. See Render passes.

If ConfigureRenderGraph adds no passes, the engine adds the default pass itself. If it adds passes of its own and does not call AddDefaultRenderGraphPass, then Draw and the drawable components never run.

EndFrame runs after the render passes and before the frame is presented. It is the place for Dear ImGui windows, which are drawn on top.

Exiting

Exit() ends the game. The current frame stops where it is: no further update steps run, and Draw and EndFrame for that frame do not run. Run() returns once the loop ends. Closing the window does the same.

protected override void Update(IGameTime gameTime)
{
  if (Keyboard.IsKeyDown(Key.Escape))
    Exit();

  base.Update(gameTime);
}

Dispose the game after Run() returns, as the first example does. That shuts down the window and the renderer.

Components

A component is an object the loop updates and, if it is drawable, draws, without the game having to call it. Attach one from Initialize or later, once the loop has started:

protected override void Initialize()
{
  AddComponent(new Spinner(GetService<IContentManager>(), GetService<IRenderContextFactory>()));
  base.Initialize();
}
  • Component is the base class for anything that updates. Override Update(IGameTime).
  • DrawableComponent is a Component that also draws. Override Draw(IGameTime). Its DrawOrder sorts it among the other drawables, lowest first, and IsVisible hides it.
  • AddComponent adds either kind. A drawable component is loaded (its LoadContent runs) when it is added, and its Initialize runs first if it implements IInitializable.
  • RemoveComponent takes it out of the loop. It does not call UnloadContent, so release anything the component holds itself.
  • GetComponent<TComponent>() returns the first attached component of that type, or null.

Drawable components are drawn in the default render pass, after Draw. The rules in Drawing steps apply to them as well.

public class Spinner : DrawableComponent
{
  private readonly IRenderContextFactory _renderContext;
  private float _angle;

  public Spinner(IContentManager contentManager, IRenderContextFactory renderContext)
    : base(contentManager, renderContext)
  {
    _renderContext = renderContext;
  }

  public override void Update(IGameTime gameTime)
  {
    _angle += gameTime.DeltaSeconds;
  }

  public override void Draw(IGameTime gameTime)
  {
    using var encoder = _renderContext.Begin();
    encoder.SubmitShape(ShapeDrawOptions.Line(new Vector2(100, 100), new Vector2(100 + MathF.Cos(_angle) * 50, 100 + MathF.Sin(_angle) * 50), Color.White));
  }
}

The interfaces behind these classes, IUpdateable and IDrawable, are public too. A class can implement them directly, but Component and DrawableComponent do the bookkeeping.

Startup services

A class that implements IRunOnInitialization or IAttachOnGameStart is registered automatically and runs without the game having to call it:

public class SaveFileLoader : IRunOnInitialization
{
  public void Initialize()
  {
    // Runs once, before the game's own Initialize.
  }
}

public class MusicBridge : IAttachOnGameStart
{
  public void Attach(IGame game)
  {
    // Runs once, before the game's own Initialize. The game is passed in.
  }
}

The engine registers each such class as a singleton, and it searches only the assemblies that are loaded when Run() starts. The order in which several of them run is not guaranteed. See Services and dependency injection for the registration rules.