Table of Contents

Windows and display

A game's window is set up from WindowOptions, passed to the Game constructor. After that, the game works with the window through the Window property, and with the display through DisplayController and RenderSurface.

Window options

public class MyGame : Game
{
  public MyGame()
    : base(new WindowOptions { Title = "My Game", Width = 1280, Height = 720, Resizable = true })
  {
  }
}
Option Default Meaning
Title "Burningstorm Studios Game" The window's title. Change it later with Window.Title.
Width, Height 800 and 600 The window's size, in window units.
Fullscreen false Goes fullscreen once the game has initialised.
Borderless false No title bar or border.
Resizable false The user can resize the window.
CenterScreen true Centres the window when it opens.
Offscreen false The window is created but never shown, and the game still draws, for running without a window.
PreferredRenderBackend null The renderer to use, as described below.
AllowRenderBackendFallback true Whether the game starts on OpenGL when the Vulkan renderer fails to start. See Falling back to OpenGL.
MouseVisible true Whether the cursor is shown. Change it later with CursorVisible on IMouseManager.

The window is created with its size and flags when the game starts, so Width, Height, Resizable, Borderless and CenterScreen cannot be changed afterwards. Fullscreen is applied after Initialize.

The window

Window is an IGameWindow, available from Initialize onwards:

  • Title gets or sets the title.
  • Size is the window's size, in window units.
  • UsableDisplaySize is the area of the display the window is on that a window may occupy, less taskbars and docks. It is null when that is not known, such as for an offscreen window.
  • Show(), Hide() and CenterWindow() show, hide and centre the window.
  • SetSize(width, height) resizes it, and raises OnResize.
  • ToggleFullscreen(true) goes fullscreen, and ToggleFullscreen(false) leaves it. Fullscreen uses the desktop's resolution and does not change the display's mode.
  • OnResize, OnShow and OnHide are events.
protected override void Initialize()
{
  Window.Title = "My Game - Level 2";
  Window.SetSize(1600, 900);
  base.Initialize();
}

Logical size and high-density displays

Drawing uses a logical size, which is the coordinate space a game draws in. By default it follows the window, so one logical unit is one window unit. RenderSurface is an IRenderSurface:

  • LogicalWidth and LogicalHeight give the logical size.
  • WindowSize is the window's size.
  • PixelSize is the back buffer's size in physical pixels. It is larger than WindowSize on a high-density display.
  • SetLogicalRenderSize(width, height) fixes the logical size, so a game can draw at 1280 by 720 whatever the window's size. It can be called at any time, not only at start-up.
  • TrackWindowSize() makes the logical size follow the window again, and TracksWindowSize says whether it does.
  • LogicalSizeChanged is raised when the logical size changes, set or following the window.
  • WindowToLogical(position) converts a window position to the logical space.

A fixed logical size is stretched to fill the window. The picture is not letterboxed, so a window whose shape differs from the logical size is drawn distorted. A game that needs to keep its aspect ratio draws its own borders.

protected override void Initialize()
{
  RenderSurface.SetLogicalRenderSize(1280, 720);
  base.Initialize();
}

A logical size changed while the game runs applies from the next frame: the projection, the mouse, and frame-graph targets sized from it (ForColorTarget(0, 0), ForScaledColorTarget) all follow. Targets at a size no longer used are freed after 120 frame-graph runs without use (two seconds at 60 frames a second with one graph a frame), so changing the size on every frame of a drag costs a target allocation per new size but doesn't pile them up. Targets a game creates itself are its own to resize.

To draw one logical unit per physical pixel, for example a game drawn into an editor panel at the panel's real size, set the logical size in pixels: the panel's size times PixelSize / WindowSize.

The mouse reports its position in the logical size, and so do light positions: a light placed at the same point a sprite is drawn lights that spot, whatever the logical size or the display's pixel density. See Materials, effects and lighting.

VSync and fullscreen

DisplayController is an IDisplayController, with IsUsingVSync and ToggleFullscreen. VSync is on by default, and IsUsingVSync can be changed while the game runs. On OpenGL it sets the swap interval at once. On Vulkan the swapchain is rebuilt with the new present mode, which takes effect from the next frame.

Displays

DisplayManager lists the displays the game can see:

var display = GetService<DisplayManager>().PrimaryDisplay;
var resolutions = display.SupportedResolutions;

Each DisplayInfo has an Index, its position (X, Y), its Width and Height, and its SupportedResolutions, the display modes it offers, largest first. A Resolution has a Width, Height and RefreshRate. The manager reports the displays; it does not change them.

Render backends

The renderer is Vulkan by default. A game can ask for OpenGL instead. The renderer is chosen from the first of these that is set:

  1. The BSGAMELIB_RENDER_BACKEND environment variable.
  2. A --render-backend argument on the command line, given as --render-backend opengl or --render-backend=opengl. -render-backend is also accepted.
  3. WindowOptions.PreferredRenderBackend.
  4. Vulkan.

The names are vulkan, vk or default for Vulkan, and opengl, open-gl or gl for OpenGL. The names are not case-sensitive. A name that is not recognised falls back to Vulkan, with a warning in the log.

new WindowOptions { Title = "My Game", PreferredRenderBackend = "opengl" };

Falling back to OpenGL

If the Vulkan renderer fails to start, the game does not stop. It logs a warning with the reason and starts on OpenGL instead:

The Vulkan render backend failed to start: <reason>. Starting on the OpenGL render backend instead.

The reason is the error the start threw, such as a missing Vulkan loader or a machine with no Vulkan device. The troubleshooting page lists them. This applies whether Vulkan was asked for or was the default.

A window is made for one renderer, so starting on OpenGL makes a new one. The Vulkan window is destroyed with everything the failed start made, and the game makes a new window for OpenGL. The game's Configure method runs again to build the services for OpenGL, so Configure must not do anything that can happen only once. Initialize runs once, after the renderer has started.

Turn the fallback off with AllowRenderBackendFallback = false in WindowOptions, or by setting the BSGAMELIB_RENDER_BACKEND_FALLBACK environment variable to 0, false, no or off. The environment variable takes precedence, as the backend choice's does, and any other value leaves the fallback on. With the fallback off, a failed Vulkan start throws an InvalidOperationException. Its message starts with The Vulkan render backend failed to start: and the reason, and says that starting on OpenGL is turned off. The game does not start.

Message boxes

MessageBox shows a message the user must answer. It waits until the user closes it:

var result = MessageBox.Show(MessageBoxType.Warning, "Save", "Overwrite the save?", MessageBoxButtons.YesNo);
if (result.YesSelected)
{
  // Overwrite.
}

MessageBoxType is Error, Information or Warning. MessageBoxButtons is OkOnly, OkCancel, YesNo or YesNoCancel. The result has OkSelected, CancelSelected, YesSelected and NoSelected, and only the ones for the buttons shown are set.

Pass the window to tie the box to it, either as MessageBox.Show(Window, ...) or with Window.ShowMessageBox(...).

The desktop's colour scheme

ISystemThemeService reports whether the desktop is set to light or dark. Its Current is a SystemTheme: Light, Dark, or Unknown where the platform does not say. Its Changed event is raised on the game's thread when the scheme changes.

The engine does nothing with this value. Follow the desktop only if the game wants to.

var theme = GetService<ISystemThemeService>();
var isDark = theme.Current == SystemTheme.Dark;

A game can be started by a link with its own URL scheme, such as mygame:level/3. The engine does not register the scheme with the operating system, so the game's installer or packaging has to do that. The engine handles the rest: a second copy started by a link hands the link to the copy already running, and the running copy raises it.

First, in the entry point, before the game starts, forward the link and exit if another copy took it:

var options = new LaunchUrlOptions("mygame", "mygame");

if (LaunchUrls.ForwardToRunningCopy(options, args))
  return;

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

Second, register the same options in the game's services, and subscribe to the links in Initialize:

protected override void Configure(IServiceCollection services)
{
  services.AddSingleton(new LaunchUrlOptions("mygame", "mygame"));
}

protected override void Initialize()
{
  GetService<ILaunchUrls>().Received += (sender, e) => OpenLevel(e.Url);
  base.Initialize();
}

Scheme is the scheme without its colon, and only links with that scheme are passed on. InstanceKey names the channel the copies share. Two copies with the same key are the same game. A game that runs several copies side by side for testing should put something that differs between them, such as its settings folder, in the key.

The game's own links are raised once each, on the first frame if the game was started with one. Subscribe in Initialize, before that frame.