Windows and game input¶
Import system.window to open a window and read input for games and other interactive programs.
The package only creates windows and reports input; drawing into a window is the job of a GPU
surface.
A program calls poll once per frame. Each call returns an InputFrame with:
- the events since the previous poll, in the order they happened
- the keyboard, mouse and gamepad state after those events
- the time since the previous poll
A game loop¶
This loop opens a native window, moves a player with the keyboard and a gamepad stick, and stops when the user closes the window or presses Escape:
import system;
import system.window;
fun main(): void {
let opened = Window.open(WindowDesc("Dream game", 1280, 720).resizable(true).vsync(true));
if opened.is_err() {
System.println("no window: " + opened.unwrap_err().message());
return;
}
let window = opened.unwrap();
let x = 0.0f;
while window.is_open {
let input = window.poll();
for (let event in input.events) {
switch (event) {
WindowEvent.KeyDown(key) => {
if key.code == KeyCode.Escape {
window.close();
}
if key.code == KeyCode.F11 {
window.set_fullscreen(!window.is_fullscreen);
}
}
WindowEvent.Resized(size) => {
System.println("drawable size: " + size.width.to_string());
}
_ => {}
}
}
let speed = 240.0f * input.delta_seconds;
if input.keyboard.is_down(KeyCode.A) {
x = x - speed;
}
if input.keyboard.is_down(KeyCode.D) {
x = x + speed;
}
switch (input.gamepads.first()) {
Some(pad) => {
x = x + pad.axis(GamepadAxis.LeftX) * speed;
}
None => {}
}
}
System.println(x);
}
Window.open returns a WindowError instead of a window when there is no display, such as on a
server, under Node, or in a CI job without a desktop session. Check WindowError.code() for the
reason.
A real game also draws a frame on each pass through the loop. Without that work, this loop polls as fast as the CPU allows.
Levels and edges¶
KeyboardState, MouseState and Gamepad answer three different questions:
is_downis true for as long as a key or button is held.pressedis true only in the frame where it went down.releasedis true only in the frame where it came up.
Use is_down for movement, and pressed for actions that should happen once, such as jumping or
firing. A key that the user taps between two polls reports both pressed and released in the
same frame, while is_down is false. Key repeats from the operating system appear as KeyDown
events with repeat set, but they never count as presses.
KeyCode names physical keys by their position on a US keyboard, so KeyCode.W is the same key on
QWERTY, AZERTY and Dvorak layouts. Use KeyEvent.key for the character the active layout prints,
and WindowEvent.Text for typed text, which includes dead keys and input-method composition.
Mouse look¶
For first-person controls, capture the cursor and read the movement from MouseState.delta:
import system;
import system.window;
fun look(window: Window, input: InputFrame, yaw: float): float {
window.set_cursor_mode(CursorMode.Captured);
return yaw + input.mouse.delta.x * 0.002f;
}
While the cursor is captured it is hidden and locked to the window, and delta keeps reporting
movement without limit. Browsers only grant the lock after a user gesture, so the canvas captures
the cursor on its next click when the first request is refused.
Testing input without a display¶
Window.headless creates a window that only exists in memory. Combined with simulate, it lets
tests and replays drive input-handling code on machines without a display:
import system;
import system.window;
fun main(): void {
let window = Window.headless(WindowDesc("test", 320, 200));
let space = KeyEvent(KeyCode.Space, " ", KeyModifiers.None, false);
window.simulate(WindowEvent.KeyDown(space));
let first = window.poll();
System.println(first.keyboard.pressed(KeyCode.Space));
let second = window.poll();
System.println(second.keyboard.pressed(KeyCode.Space));
System.println(second.keyboard.is_down(KeyCode.Space));
}
This prints true, false and true: the press is an edge in the first frame only, while the
key stays down.
Simulated events also reach real windows. They arrive after the platform's own events in the next poll, which is useful for on-screen controls.
Windows, threads and browsers¶
Native windows use GLFW and belong to the program's main thread. main and its async code run
on that thread, so a normal game loop needs no extra setup. Calls from a Task worker do not
reach the window: Window.open returns a WindowError with code ETHREAD, and the other methods
do nothing.
While a native window is open, an await that has to wait, such as a slow network read, keeps
handling the window system's events, so the operating system never reports the window as not
responding. Your frame loop does not draw while it waits, though. To keep drawing, move slow work
into its own async function that stores its result in an object the frame loop reads, start it
with Promise.start, and let the frame loop await only per-frame work such as presenting a frame
or Time.delay(16).
On macOS, live-resizing a window still pauses the program until the resize ends.
In a browser, use Window.canvas with the id of a <canvas> element on the page.
Window.open also works there: it adds a canvas of the requested size to the page. The browser
only delivers input while your program is waiting, so a browser frame loop must await something
on every frame, such as presenting a GPU frame or Time.delay(16).
Platform differences:
- Gamepads use the SDL controller database natively and the standard Gamepad API mapping in
browsers.
Gamepad.rumblecurrently works only in browsers that support vibration. WindowEvent.FileDroppedcarries a full path natively and only the file name in browsers.- Browsers never report
WindowEvent.Moved, and they only allow fullscreen and cursor capture during a user gesture.