Input & Controls
Input in the engine is fixed for now: there is no general rebinding layer and no per-device configuration. The one exception is the camera zoom, which is a rebindable action (see Third-Person Camera). What is tunable are the look-scaling preferences, and those are ordinary client.* preferences — set one from the console and it applies on the very next frame.
Two devices are live at all times: keyboard and mouse, and a gamepad if one is plugged in. Neither has to be selected or enabled, and there is no “gamepad mode” to enter.
Keyboard and mouse
Section titled “Keyboard and mouse”| Action | Binding |
|---|---|
| Move | W / A / S / D |
| Look | Mouse |
| Jump | Space |
| Crouch | Left Ctrl |
| Sprint | Left Shift |
| Use / interact | E |
| Zoom the camera | Scroll wheel, rebindable, works with Shift or Ctrl held. Alt plus the wheel is reserved for avatar scale — see Third-Person Camera |
| Cycle the camera | F5: first person, behind you, first person |
| Noclip (cheat) | V |
| Chat | Enter (or keypad Enter); Escape cancels, Enter sends, clicking away dismisses |
| Pause menu | Escape |
| Console | ` (backquote); Escape also closes it |
| DigitalHeaven overlay | F1 |
| VR on / off | F8 (or vr at the console) — see VR |
The thing E would act on is outlined. While an interactable object is in reach and lined up, it wears the editor’s white selection ring so you can see what a press will reach before you press it — on every host, on by default, and switchable at Options → Interface → Gameplay → Highlight interactive objects (client.highlightInteractive). See the same ring outside the editor.
The console does not pause, and Escape never leaves a window behind. The backquote opens the console straight from gameplay — the world keeps running behind it, you just get a cursor and the keyboard goes to the prompt. Escape there closes the console and hands the game back rather than opening the pause menu; Escape over a paused game puts the menu and the console away together, so one press always ends with a clean screen. What survives either route is the scrollback and whatever was half-typed at the prompt.
The cursor is a property of the state, not something a transition does. One expression decides whether the pointer is freed and shown, and the frame loop asks it once and applies the answer — nothing anywhere locks or unlocks the mouse on its way into or out of a screen. It is freed while a menu is up, while the console or the settings screen is over the scene, while a chat line is being typed, while the window is being dragged or resized, and while a join is still loading — see the loading box. The last one is why ESC over a connecting screen and then Resume leaves the pointer where it was: resuming ends the pause, not the load.
Chat does not pause and does not fight the console, and a focused field is the first layer a press has to get past. Enter opens the prompt only while gameplay actually has the keyboard, so it does nothing behind the pause menu, the settings screen or a front console — and nothing while any text field holds the keyboard, so committing a coordinate in the editor’s inspector commits the coordinate and that is all it does. The same is true of the backquote and of the editor’s bare letters — F and the four tool-mode keys W, E, R, T: while you are typing they are characters, never bindings — and of Escape, which in a focused field abandons the edit and drops the caret and stops there, leaving the pause menu unopened and the editor’s selection intact; the press after it walks the usual ladder — and while the prompt is open it owns the keyboard outright, so typing “was” never walks you forward. The one key it does not swallow is Escape, which always closes it — and clicking away dismisses it rather than leaving a prompt open that nothing can reach. Either way what you had typed is still there when you come back, with the caret at the end of it. See Chat for the feed itself.
Mouse look uses Source-style scaling — degrees turned equals raw mouse counts times client.mYaw / client.mPitch times client.sensitivity — and is applied every frame, client-side, so the view never waits on the server.
Tracing the pointer
Section titled “Tracing the pointer”client.debug.pointerLog 1 writes every pointer event the window receives to the log on the input channel, exactly as SDL delivered it: mouse motion and buttons (with the source, so a touch or pen read as a mouse says so), finger and pen events, window enter, leave and resize. The first line states the window’s size, pixel size and density, which every coordinate after it is read against. Beside them, the interface logs where it saw the pointer and which control a press would land on, whenever the left button or that control changes. Motion is thinned to twenty lines a second; edges are logged whole. It is for a platform whose pointer reaches the interface somewhere other than where it was aimed, such as a VR laser driving a flat window through a compositor. Leave it off otherwise.
The first thing to read in such a log is the bounds of every mouse position. If they stop short of the window’s own size, the pointer is confined to a screen smaller than the window. That is what the Steam Frame showed: Gamescope gives games a 1280x720 X screen, the window asked for 1600x900 and sat centered at (-160, -90), and the laser never reported anything outside [160, 1439] x [90, 809]. Gamescope still drew the whole window, scaled onto the theater screen, so the pointer stuck at an edge the picture went past, and controls along the left and top were out of reach. A window in Windowed mode now never opens larger than its display: a larger size is scaled down to fit, keeping its aspect, and the log says so. The client.display.width and client.display.height preferences keep the size that was asked for, so a bigger display gets it back.
Touchscreen controls
Section titled “Touchscreen controls”Touch-capable hosts use shared Halcyon controls, not a native control overlay. An idle movement ring rests at the lower left while gameplay is available. Touch down in the bottom third of the viewport, within its left 50% on phones or 40% on tablets, to anchor a floating movement ring; drag from that gesture’s origin to move. The form-factor rule uses the shortest logical viewport dimension: at least 600 units selects the tablet region, independently of pixel density or orientation. The left edge and top of the bottom third are included; the region’s right edge and the viewport’s bottom edge are excluded. A contact starting outside the movement region cannot acquire movement later by dragging into it; an already captured movement contact continues outside it. Releasing returns the ring to its lower-left resting position, unless the stick is locked (see below). The anchor is clamped so the ring fits inside the current safe area plus padding, stays fixed during that gesture, and is chosen again on the next touchdown. The knob stays inside the ring. A separate contact in the right gameplay region controls camera look.
Movement, look and UI contacts have independent pointer-ID captures in the same Halcyon tree. A contact starting on a UI surface cannot become movement by dragging away, and a gameplay contact crossing a button does not click through. The bottom-right Menu control opens the existing pause menu and becomes Close menu while paused; a third contact can use it without stealing the movement or look contact. Its circular surface and larger hit target come from the reusable Halcyon CircleButton. This invokes shared pause transitions, not a separate mobile panel or a multiplayer server-pause policy. A larger Jump button above that row forwards held capture state to the shared gameplay controller; release or cancellation clears it, and the release reaches the tick the same frame the thumb lifts. Chat sits immediately left of Menu when the shared session composer is available and opens that composer rather than a native text panel.
Three more buttons share Jump’s corner. The columns are ordered by permanence, corner outward — Jump and Sprint are always drawn, Use only sometimes — so the buttons that come and go are the ones furthest out, and their appearing and disappearing never moves a button that was already under your thumb:
- Sprint is the same size as Jump and sits immediately to its left, on the same row. It is a latched toggle, like Noclip and unlike Jump: one tap turns sprinting on and it stays on with nothing touching the screen, a second tap turns it off, and while it is on the button wears the same engaged style Noclip does. A hold would pin the right thumb to a button for the length of a long walk, which is most of what sprinting is, leaving no thumb for the camera. It feeds the same sprint input
Left Shiftand padLBdo. The latch is client-side — there is nothing for a server to confirm about an input bit — and it clears with every other gameplay hold when capture is canceled: pausing, opening the console, a loading screen or leaving the session all end the sprint rather than resuming a walk nobody asked to keep. It lives on the action side rather than beside the movement ring because the left half of the bottom third is where a movement gesture may start — a button parked there would eat the ring’s own landing area. - Use is the same size as Jump and sits furthest from the corner on that row, left of Sprint. It appears only while something interactable is actually in reach and lined up — the same target the interaction highlight rings and the same one
Ewould act on — and a tap fires exactly one interact. - Noclip is a smaller circle one row above, centered over Jump in the corner column. It is smaller than the action row because it is a mode you set once rather than an action you repeat — but it is sized by its own word rather than by eye: the circle is whatever seats “Noclip” at the label size with clear space on both sides, so the longest label on the row decides the row. It appears only when the server would honor the
noclipline for this client — admin,world.cheats, orworld.noclipAllowedopened for guests. The client asks the same rule the server refuses by, so a button you can see is a button that will work. It is a latched control, the same shape as Sprint: while flight is on it wears the same engaged style, distinct from hovered and pressed. The one difference is where the state lives — Noclip’s follows the state the server confirmed rather than the press that asked for it. It stands over the corner column precisely because that column is never empty; over an optional button it would read as floating off to the left whenever that button was hidden.
Gameplay touch uses the shared ClientUiState.CrosshairVisible gate, not the informational HUD visibility setting. Main menu and loading block gameplay contacts; pausing cancels gameplay capture while retaining the UI contact operating the menu. Lifecycle cancellation or event loss requires fresh touchdowns. Hosts refresh safe-area insets and cancel captured input on geometry changes. Attaching a keyboard or controller does not disable the touchscreen profile.
Movement lock
Section titled “Movement lock”Holding a thumb perfectly still on a ring for the length of a walk is the gesture a touchscreen is worst at, so the stick can be latched. While a thumb is on the movement stick — and for as long as a latch is engaged — a Lock button appears beside the ring, in the same size family as Chat and Menu. Tap it and the stick’s current vector is held: lift the thumb and the ring and its knob stay drawn exactly where you left them, the button wears the same engaged style Sprint and Noclip do, and the latched vector keeps feeding movement every tick as though the thumb were still down. Tap it again to release — the ring returns to its resting position and movement stops. A fresh touch on the stick always wins it back and clears the latch in the same gesture, so a locked stick can never strand you, and the latch clears with every other gameplay hold when capture is canceled: pausing, the console, a loading screen or leaving the session all end it.
The button travels with the ring, and it never stands where a stick could start. It is placed on the ring’s right shoulder, one cluster gap clear of the rim, at the ring’s own height. Two rules move it from there. It is pushed right until it clears the movement region’s right edge, because a button parked inside that region would eat the touchdown that was going to acquire a new stick; and it is lifted above the action cluster’s rows if it would otherwise land on one. On a tablet the first rule is enough and the button simply sits beside the ring. On a phone the movement region reaches half the width and the action cluster starts left of that, so at ring height there is no free column at all, and the button rises above the cluster — the nearest place to the ring that is neither inside the acquisition region nor on top of another control. The stick’s own finger can never press it: a contact the movement capture owns is never forwarded to the UI tree, so the tap is always a second finger’s.
Latching is forgiving. A thumb that means “straight ahead” is a few degrees off it every time, and a few degrees held for a minute is a curve. At the instant of the latch — and only then; a live stick is never corrected — the vector is snapped to the nearest cardinal if it is within client.touch.lockSnapDegrees of one, keeping its magnitude, and its magnitude is snapped to full if it is within client.touch.lockSnapRim of the rim. Either half is disabled on its own by setting its preference to zero.
| Preference | Default | Meaning |
|---|---|---|
client.touch.lockSnapDegrees | 12 | Degrees from a cardinal within which a locked stick snaps to that cardinal; 0 disables |
client.touch.lockSnapRim | 0.12 | Fraction of the ring radius from the rim within which a locked stick snaps to full speed; 0 disables |
Touch camera preferences
Section titled “Touch camera preferences”Options → Controls → Touchscreen provides a persisted camera sensitivity slider (0.1–10, default 1) and two independent, persisted inversion toggles:
| Preference | Default | Meaning |
|---|---|---|
client.touchSensitivity | 1 | Multiply touchscreen camera displacement only; return to 1 for the original speed |
client.touchInvertHorizontal | true | Reverse horizontal touchscreen camera displacement |
client.touchInvertVertical | true | Reverse vertical touchscreen camera displacement |
Both defaults reverse the earlier touchscreen direction. Missing keys use true; an explicitly saved false stays false, without a preference reset or migration. The iOS host saves these in config/profile.toml. Sensitivity and inversion apply once to touch displacement before hardware look is added: mouse and gamepad signs and preferences are unchanged, and touch displacement is not multiplied by frame duration.
The experimental iOS host supports committed Unicode text, but does not implement IME preedit/composition display or relative mouse/pointer lock. Desktop mouse-lock behavior described above is not evidence of iPad trackpad capture. Its app declarations allow portrait and landscape on iPhone, all four orientations on iPad, and flexible app windows; actual Split View/Stage Manager resizing, safe-area input alignment, and in-session touch controls still require physical-device validation. CPU layout tests and signed AOT builds do not prove those platform interactions.
Gamepad
Section titled “Gamepad”Gamepad support is deliberately passive: a fixed default mapping, always on, no rebinding UI. The first connected gamepad is used; connecting or disconnecting one mid-session is handled live and logged at INF, and a session with no pad behaves exactly as it always did.
A Steam Controller works out of the box. Steam Input presents it to the game as a standard XInput/Xbox gamepad, which is what the windowing layer sees, so no special handling is involved — the same is true of any other controller Steam Input remaps.
The mapping
Section titled “The mapping”| Action | Gamepad |
|---|---|
| Move | Left stick (analog — the stick’s magnitude is the walk speed) |
| Look | Right stick |
| Jump | A (south) |
| Crouch | B (east) |
| Use / interact | X (west) |
| Sprint | LB or left stick click |
| Pause menu | Start |
Everything else — Y, RB, both triggers, the D-pad, Back and the guide button — is unmapped. So is noclip: it is a cheat toggle with no obvious pad counterpart, and a face button that flips flight on by accident is worse than one that does nothing.
Menus, the console and the settings screen remain pointer-driven: mouse on desktop and touch on touch-capable hosts. There is no controller menu navigation, so Start opening the pause menu is where the pad’s involvement in UI ends.
How the two devices coexist
Section titled “How the two devices coexist”Held actions are OR’d. Jump is down when either Space or A is down. Nothing arbitrates.
Movement gives the keyboard priority. Whenever any movement key is held, the keyboard’s wish direction is what gets sent and the stick is ignored for that frame; the stick fills in only while the keys are idle. Summing the two would let a stick push a keyboard walk past full speed, and it would mean a drifting stick permanently skews a keyboard-only player’s heading.
Look is summed. An idle device contributes exactly zero, so mouse and stick add with no mode switch — you can nudge the view with the mouse mid-stick-turn and it does the obvious thing.
The UI gate is shared. Gamepad move and look flow through the same gameplay gate as the keyboard and mouse: while the pause menu, the console or the overlay holds input, the sticks drive nothing. Start goes through the same routing rule as Escape, so one press can never close the console and open the pause menu behind it.
Stick shaping
Section titled “Stick shaping”A stick is not a mouse. A mouse delta is a displacement that already happened; a stick reports a held position, so look is a rate — scaled by degrees per second and integrated over the frame’s elapsed time. That is why the pad look preferences are in degrees/second while the mouse ones are per mouse count.
The deadzone is radial: the dead region is a disc around center, not a cross of per-axis bands, so a stick held diagonally is not clipped toward a cardinal direction. Magnitude surviving the disc is rescaled back over the full 0–1 range, which is what keeps the response continuous — without the rescale the stick would jump to the deadzone value the instant it left the dead disc.
The look stick gets a response curve on top: its magnitude is raised to client.padLookExponent, compressing the low end so small deflections aim finely while full deflection still turns at the full rate. Movement is left linear — walking speed should track the stick.
Preferences
Section titled “Preferences”| Preference | Default | Meaning |
|---|---|---|
client.padSensitivity | 1 | Look sensitivity multiplier; scales both axes at once |
client.padYaw | 220 | Yaw degrees per second at full stick deflection, before sensitivity |
client.padPitch | 160 | Pitch degrees per second at full deflection, before sensitivity |
client.padDeadzone | 0.15 | Radial deadzone, in fractions of full deflection; shared by both sticks |
client.padLookExponent | 2 | Look response curve exponent; 1 is a linear stick |
Pitch is slower than yaw by default: the vertical range is only a quarter turn between the pitch clamps, so matching the yaw rate makes the view feel twitchy.
These are console-settable and live — client.padDeadzone 0.08 takes effect on the next frame — and they are on the settings screen under Controls > Gamepad, on every host: the same five sliders, over the same shared bounds, on desktop and on the iPad. Rebinding is still future work; this group sets how the sticks feel, not what the buttons do.
What is not here yet
Section titled “What is not here yet”Rebinding, multi-controller support, a per-device enable/disable toggle, rumble, controller glyphs and controller menu navigation are all future work. The mapping above is a fixed default, not a binding system with defaults filled in.