"Visible cursor blocks game mouse input: no raw bypass exists"
▸ SYMPTOM
With a visible cursor (MouseVisibility.Visible, MouseState=UI):
Input.Down("attack2")orInput.Pressed("attack2")never fires when clicking on a UI panel.Input.Keyboard.Down("mouse2")also doesn't work as a bypass.Input.AnalogLookandInput.MouseDeltaare hard-zeroed: a "hold-button-to-look" mode does nothing while the cursor is visible.
▸ CAUSE
Game code's view of the mouse is triply gated when the cursor is visible:
-
Buttons:
InputContext.OnMouseButtonforwards a mouse-button press to game input only whenMouseState == GameOR the panel under the cursor is null / doesn't want pointer events / hasButtonInput == PanelInputType.Game. A press landing on a pointer-events panel is UI-only.Input.Down/Pressedfor that button's actions never fire. Releases always route to game, so a held action can't stick. -
No raw read:
Input.Keyboard.Down("mouse2")readsCurrentContext.KeysCurrent, which is fed byAccumKeysPressedinside the same gatedInput.OnButton. The true raw store (InputRouter.PressedButtons) isinternal static, unreachable from game code. If UI ate the press, game code cannot see the button, full stop. -
Motion:
Input.AnalogLookis hard-zeroed when the cursor is visible (ComputeAnalogLook:if (MouseCursorVisible) AnalogLook = default). The underlying delta feed is also UI-only unlessMouseState == Game || MouseCapture.Mouse.Positiondoes keep updating, and so does rawMouse.Delta, which is NOT gated likeInput.MouseDelta/Input.AnalogLook. A camera consumingMouse.Deltadirectly keeps spinning under a visible cursor unless the game gates that consumer on its own UI-open state.
▸ FIX
- A press on empty screen (e.g. over a
pointer-events: noneHUD root) does reach game actions even with the cursor visible. Design your HUD overlays withpointer-events: noneon the root if you want click-through. - A "hold-button-to-look" mode must actually lock the cursor (
MouseVisibility.Hidden) for the drag's duration: there is no way to read mouse delta throughInput.MouseDeltaorInput.AnalogLookwith a visible cursor. Mouse.Deltatrap: a camera that readsMouse.Deltadirectly (instead ofInput.AnalogLook) will keep spinning when the cursor is freed by an auto-popping overlay. Gate theMouse.Deltaconsumer on your own UI-open state. Don't rely on the engine's input routing to suppress it.- Per-frame Mouse.Visibility write race: if two components both assert
Mouse.Visibilityevery frame (a panel asserting Visible-while-open vs a camera asserting Hidden-while-dragging), they race by component update order. The last writer wins the frame. Fix: publish a static "dragging" flag from the cursor's owner and make every other per-frame asserter skip its write while the flag is up.
Raw-key read vs bound action: the collision that arms the trap
A dev/debug panel toggled by a raw Input.Keyboard.Pressed("<key>") read is not isolated from gameplay. If that key is also bound to a game action in Input.config, playing the game opens the panel, and because that panel asserts Mouse.Visibility = Visible every frame, an accidental open zeroes Input.AnalogLook and kills mouse-look for the rest of the session, with no error and no obvious connection back to the key press. (A single letter bound to two actions, say drop and a shift-mode, meant every gear change also toggled a dev panel.)
Rule: check a raw key read against the project's bound actions, not just against the F1-F12 keys the editor eats. Read Input.config, enumerate the genuinely free letters rather than picking one that looks unused, and re-check after any keybind pass. Input.config bindings differ per project.
The cursor-modal seam must be registered by EVERY cursor-asserting panel
The write race above is N-way, not two-way. A camera that re-asserts Hidden every frame plus N panels each asserting Visible every frame means the cursor flickers, AnalogLook samples zero on every frame a panel wins, and any right-mouse-drag orbit fallback gated on the same seam never engages either, so the "workaround" is dead for the same reason as the primary path. A seam wired to one panel out of four is not a seam: every cursor-asserting panel must register with it.
Structural limit worth knowing before you plan the fix: a vendored library panel that exposes its open flag as internal cannot register with a consuming project's seam at all. Widening a shared kit's API is a separate decision, so the practical mitigation in the consumer is to remove the accidental-open path (move the raw key off the bound letter) and treat the seam gap as owner-facing rather than fixable downstream.
▸ WHY IT WORKS
The engine's input routing is designed so that UI panels get exclusive ownership of mouse events when the cursor is visible. This prevents game actions from firing through menus and dialogs. The lack of a raw bypass for buttons is intentional: the only way to get game mouse input through the Input API is to actually be in game mouse state. Mouse.Delta sits outside this routing, which is why it needs a separate application-level gate.
- Added the raw-key-vs-bound-action collision that arms the cursor trap, the N-way seam-registration rule, and the vendored-panel internal-flag limit. Re-verified on engine 26.07.22.
- Added Mouse.Delta ungated trap and per-frame visibility write race
- Published