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:
Titlegets or sets the title.Sizeis the window's size, in window units.UsableDisplaySizeis the area of the display the window is on that a window may occupy, less taskbars and docks. It isnullwhen that is not known, such as for an offscreen window.Show(),Hide()andCenterWindow()show, hide and centre the window.SetSize(width, height)resizes it, and raisesOnResize.ToggleFullscreen(true)goes fullscreen, andToggleFullscreen(false)leaves it. Fullscreen uses the desktop's resolution and does not change the display's mode.OnResize,OnShowandOnHideare 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:
LogicalWidthandLogicalHeightgive the logical size.WindowSizeis the window's size.PixelSizeis the back buffer's size in physical pixels. It is larger thanWindowSizeon 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, andTracksWindowSizesays whether it does.LogicalSizeChangedis 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:
- The
BSGAMELIB_RENDER_BACKENDenvironment variable. - A
--render-backendargument on the command line, given as--render-backend openglor--render-backend=opengl.-render-backendis also accepted. WindowOptions.PreferredRenderBackend.- 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;
Starting a game by a link
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.