godot_gameinput is a standalone GDExtension addon that brings the Microsoft
GameInput API to Godot 4.x on Windows. It works independently of the
godot_gdk addon — you can ship one, both, or neither. Target machines still
need a compatible GameInput runtime/redist installed (for example
GameInputRedist.msi).
The addon gives GDScript first-class access to:
- The GameInput runtime (initialize, shutdown, per-frame poll)
- Connected gamepads (display name, vendor / product id, vibration support)
- Per-frame readings (button bitmask + axes, with edge-detected press / release)
- Vibration (low + high freq motors, plus trigger rumble)
- Hot-plug signals on the main thread
- An inspector-friendly action bridge (
GameInputBinding+GameInputActionMap+GameInputMapper) that drives Godot'sInput/InputMapsystem from any GameInput device.
| Feature | Status |
|---|---|
GameInput engine singleton |
Shipped |
| Devices + readings + vibration | Shipped |
GameInputDevice.get_device_info() |
Shipped (issue #23, device-info half) |
GameInputBinding / GameInputActionMap / GameInputMapper |
Shipped |
EditorPlugin autoload installer + Project Settings |
Shipped |
Sample integration (sample/tutorial_gameinput/ standalone sample; sample/tutorial_app/ panel deferred) |
Standalone sample shipped; tutorial_app panel deferred |
| Headless test suite | Shipped |
| Manual hardware checklist (docs/gameinput/manual-tests.md) | Shipped |
| Reading callbacks (event-driven) | Deferred — see issue list |
| Force feedback / arcade stick / racing wheel | Deferred |
| KB / mouse raw input | Deferred |
# Build only the GameInput addon
cmake --preset gameinput-only
cmake --build --preset debug-gameinput
# Or build everything (both addons)
cmake --preset default
cmake --build build --preset debugDLLs land in addons/godot_gameinput/bin/ and are copied into every sample
project's addons/godot_gameinput/ by the build's sample-sync step.
- Copy
addons/godot_gameinput/into your project (or symlink it). - Open the project once — Godot will detect
plugin.cfg. - Project → Project Settings → Plugins → enable Godot GameInput.
Enabling the plugin installs an autoload called GameInputBootstrap that
reads project settings and runs the lifecycle for you:
| Setting | Default | Behaviour |
|---|---|---|
game_input/runtime/initialize_on_startup |
false |
When true, the bootstrap calls GameInput.initialize() on _ready. |
game_input/runtime/auto_poll |
true |
When true, the bootstrap calls GameInput.poll() from _process. |
game_input/mapper/default_action_map |
"" |
Path to a .tres GameInputActionMap. When set, the bootstrap spawns a GameInputMapper named DefaultMapper as its own child and assigns the loaded resource — so your project's InputMap can be driven from a GameInput action map without dropping a Mapper node into any scene. User-placed GameInputMapper nodes do not consult this setting; assign their action_map explicitly. |
Disabling the plugin removes the autoload — there is no orphaned state.
If you want full control instead, leave initialize_on_startup off and call
the lifecycle yourself; GameInputMapper nodes also call poll() defensively
(it is per-frame idempotent), so dropping a Mapper into a scene is enough even
without the autoload's auto_poll.
extends Node
func _ready() -> void:
var gi = Engine.get_singleton("GameInput")
if not gi.is_initialized():
gi.initialize()
gi.device_connected.connect(func(device): print("connected: ", device.get_display_name()))
func _process(_delta: float) -> void:
var gi = Engine.get_singleton("GameInput")
if not gi.is_initialized():
return
gi.poll()
var pad := gi.get_primary_device()
if pad == null:
return
var reading := gi.get_current_reading(pad)
if reading != null and reading.was_button_pressed(GameInputDevice.BUTTON_A):
if gi.set_vibration(pad, 0.6, 0.3):
await get_tree().create_timer(0.15).timeout
gi.stop_haptics(pad)In the editor:
- Right-click a folder → Create New Resource →
GameInputActionMap. - Edit the map; add
GameInputBindingrows. For each row:action— a Godot action name that already exists in Project Settings → Input Map (e.g.&"jump").source— aGameInputDevice.SRC_*value (e.g.SRC_BTN_A,SRC_AXIS_LEFT_X).is_axis— toggle on for thumbsticks / triggers.axis_threshold— for axis-as-button, fireaction_presswhen|value| >= threshold.axis_invert— flip sign before evaluating. Handy for thumbstick Y in Godot's "down positive" convention.deadzone— values within[-deadzone, deadzone]are clamped to 0.
- Save the resource.
- In your scene, add a
GameInputMappernode and assign the action map to itsaction_mapproperty — or set the project-widegame_input/mapper/default_action_mapto your.tresand skip the node entirely (the bootstrap spawns aDefaultMapperfor you).
The Mapper calls Input.action_press(action, strength) /
Input.action_release(action) each frame, so the rest of your code can stay
on Godot's standard Input.is_action_pressed("jump") / Input.get_axis()
APIs and gain GameInput device support transparently. If the mapper stops
driving a held binding — the node exits the tree, you swap or mutate the
active GameInputActionMap, the target device disappears, or the frame cannot
produce a fresh reading — it releases the actions it previously held before
clearing its per-binding cache. Runtime InputMap edits also refresh the
native-event suppression cache by the next frame. On every press / release
transition the Mapper also pushes an InputEventAction through
Input.parse_input_event so event-driven consumers — UI focus traversal for
ui_*, _gui_input listeners, _input / _unhandled_input handlers —
actually see the action change. When Godot's built-in joypad backend is
already wired to deliver the same action through a matching
InputEventJoypadButton / InputEventJoypadMotion in your InputMap (the
default project mapping for ui_accept etc.), the Mapper suppresses its own
InputEventAction for that binding so menu actions fire exactly once per
physical press instead of twice.
GameInput.set_vibration() returns false when preflight fails (null or
disconnected device, no rumble motors, uninitialized runtime) and also when an
HRESULT-returning GameInput SDK reports a native SetRumbleState failure, so
callers can skip timers or surface diagnostics.
Every public method on GameInput, GameInputDevice, and GameInputReading
returns a safe default and emits a single push_warning if called before
initialize(), after shutdown(), or on a host where GameInput is
unavailable. GameInput.get_connected_device_count() is the exception —
it reports the cached device count (or 0) without warning, so polling it
from a HUD does not spam the log. Your scene won't crash if the addon
isn't ready yet — checks like if gi.is_initialized(): are optional, just
preferred for clarity.
- Connect / disconnect events from GameInput arrive on a worker thread; the
addon enqueues them under a mutex and emits Godot signals on the main
thread during
poll(). GameInputDevicewrappers hold a session-local monotonic id, never a rawIGameInputDevice*. Stale wrappers stay alive butis_connected()starts returningfalseand other methods return safe defaults.- Device ids are never recycled within a session.
- Shutdown uses
IGameInput::UnregisterCallbackrather thanStopCallback: Microsoft documentsUnregisterCallbackas the point after which callback resources may be removed, whileStopCallbackonly prevents future dispatch (see Microsoft Learn forIGameInput::UnregisterCallbackandIGameInput::StopCallback). This keeps the raw callback context, cached devices, and pending-event queue alive until any in-flight GameInput worker callback has finished.
XML class documentation lives in addons/godot_gameinput/doc_classes/ and is
wired into the addon's CMake target_doc_sources. Inside the editor, hover or
press F1 on any GameInput* symbol to see the full class reference.
The standalone sample/tutorial_gameinput/ project demonstrates the action
bridge end-to-end (initialize / shutdown / list devices / inspect primary /
rumble pulse / stop rumble, with a live device count and a hot-plug event
log). See its README.md for the walkthrough. The GameInput scenario panel
inside sample/tutorial_app/ is deferred to a follow-up PR; until then,
the standalone sample and the
Tutorial — GameInput action bridge
are the recommended entry points.
godot_gameinput is exercised by the tests\godot\gameinput\ host. Coverage lives under tests\godot\gameinput\tests\ and includes files such as test_gameinput_core.gd, test_gameinput_device.gd, test_gameinput_reading.gd, test_gameinput_resource.gd, test_gameinput_mapper.gd, test_gameinput_mapper_extensions.gd, and test_gameinput_threading_smoke.gd. Bootstrap autoload checks live under tests\godot\gameinput\tests\bootstrap\.
GameInput headless tests are deterministic by default and do not require live XBOX or PlayFab credentials. Hardware-specific behavior such as real controllers, rumble feel, and hot-plug should still be checked with gameinput/manual-tests.md.
Run the standard pipeline from the repository root:
pwsh -NoLogo -NoProfile -ExecutionPolicy Bypass -File .\tools\run_all_tests.ps1See gdk/sample-and-tests.md for the orchestrator stages, GUT layout, bootstrap mini-runners, baselines, and troubleshooting pointers.
addons/godot_gameinput/CMakeLists.txt— the build target. Add new sources to_GAMEINPUT_SRCSand new sync files togodot_addon_sync_files_to_sample's FILES list..github/instructions/godot-gameinput.instructions.md— path-scoped Copilot guidance for changes inside the addon and its samples.