Recursive AI

Controller and keyboard navigation

All onegame recipes

Make a game screen playable with a gamepad and a keyboard: a default focus, d-pad movement, confirm and back, focus that stays inside a dialog, and scenarios that play the screen with a controller.

The UI owns one focused element. The host turns the gamepad into UI input by itself: South is confirm, East is back, and the d-pad and shoulder buttons navigate. Tab and Shift+Tab move through focusable elements in order, the arrow keys and the d-pad move to the nearest one in that direction, and Enter, Space and confirm click the focused element. Every kit component works this way, so a screen built from the kit needs only a starting focus.

This uses the market stall from Build a game screen with the UI kit.

Give the screen a starting focus

Without one, a controller player’s first press has to find something. The stall starts in the table:

C#
            // A controller player starts in the table. RequestFocus applies after the next update, once the screen is in the tree.
            game.Ui.RequestFocus(table.Body);

RequestFocus waits for the end of the next update, which is what code inside a screen builder or a binding must use. ui.Focus(element) focuses at once, and FocusHelpers.FocusDefault(game.Ui, screen) focuses the element marked .AsDefaultFocus().

Play it with a controller

YAML
name: buy-with-pad
description: A controller player starts in the table, moves to pears and buys two with the confirm button.
steps:
  - step: 1
  - expect:
      ui: { id: goods/body, focused: true }
  - nav: down              # from apples to pears
  - nav: activate          # the confirm button: onActivate buys the selected row
  - step: 1
  - nav: activate
  - step: 1
expect:
  - state: { entity: Shop, property: "Stock[1]", equals: 6 }
  - state: { entity: Shop, property: Gold, equals: 42 }
Terminal
onegame test --scenario scenarios/buy-with-pad.yaml
Text
PASS buy-with-pad (4 ms, tick 3, hash f8fcbc6f2c91948b)
1 passed, 0 failed

nav: takes up down left right next previous activate cancel, or a button by name (start, back, west, north, left_stick...). Pears cost 4, so two of them take the gold from 50 to 42 and the stock from 8 to 6. In the table, Up and Down move the selection and confirm calls the table’s onActivate. At the first or last row the key isn’t used, so focus moves on to the neighbor: that is how a pad player leaves a table.

Without the starting focus, the shoulder button (nav: next) walks the focusable elements in order. In this screen the first three are the table’s header cells (goods/header/0 to goods/header/2) and the fourth is goods/body.

Dialogs keep focus inside

The sell-back confirm dialog traps focus: while it is open, Tab and the d-pad stay inside it, the pointer can’t reach what is behind it, and Escape or the back button closes it and returns focus to what had it before. A dialog made with danger: true starts on its cancel button, so a stray confirm press doesn’t destroy anything. The sell-back scenario checks this with press: Escape, then press: Tab and press: Enter.

Good to know

  • A component that uses a key itself takes it first: a slider takes Left and Right, a text field the arrows. Unused keys fall back to moving focus.
  • Disabled and hidden elements are skipped, and focusing an element scrolls a scroll view to show it.
  • The UI guide’s rule of thumb: every action reachable by d-pad and confirm, back always leaves, and a default focus on every screen.
  • Input glyphs (“press (A)”) follow the device in use. KitHost.Dispatch tells them which one that is.
  • Steam Input action sets can be scripted in scenarios with steam-input: against a stand-in controller.

Focus, keys and the full input table are in the UI guide, section 9.