Wired
ss-gameforge-wired is a complete input management layer on top of Godot’s native input handling.
It adds per-player input isolation, runtime rebinding with persistence, automatic device detection, and “press to join” support for local co-op.
Installation
Section titled “Installation”- Download
ss-gameforge-wiredfrom the releases page. - Copy
addons/ss-gameforge-wired/into your project’sres://addons/. - Enable ss-gameforge-singleton and ss-gameforge-wired in Project Settings → Plugins.
Editor panel
Section titled “Editor panel”After enabling the plugin, a GodotWired tab appears in the Godot editor’s main screen (next to 2D/3D/Script).
Use it to:
- Set the player mode (
SINGLE,LOCAL_COOP,ONLINE) and max local players. - Add, rename, and remove actions.
- Set each action’s display name, description, category, type (Button or Axis), and whether it allows rebinding.
- Save the configuration as a
GWConfigresource (default:res://gw_config.tres).
Default bindings are added via code with define_action(), or set directly on the GWConfig resource in the inspector.
Core classes
Section titled “Core classes”| Class | Description |
|---|---|
GWInputManager | Central coordinator — singleton entry point for all input operations. |
GWConfig | Project-level configuration resource (player mode, max players, action list). |
GWActionConfig | Template defining a single action (name, bindings, category, type). |
GWPlayer | Logical player with its own devices and independent copies of all actions. |
GWAction | Per-player runtime action instance (independently rebindable). |
GWBinding | A single physical input mapping (key, button, axis, mouse button). |
GWDevice | Abstraction over a physical device (keyboard/mouse or gamepad). |
GWProfileManager | Static utility for named save/load binding profiles. |
GWConfig
Section titled “GWConfig”Create via the editor panel, or New Resource → GWConfig in the inspector.
enum PlayerMode { SINGLE, # One player — keyboard + any gamepad LOCAL_COOP, # N players — each gets an exclusive device ONLINE, # One local player, the rest managed by the network layer}
@export var player_mode: PlayerMode = SINGLE@export_range(2, 8) var max_local_players: int = 2@export var actions: Array[GWActionConfig] = []Load the config at startup:
var config := preload("res://gw_config.tres") as GWConfigGWInputManager.i.load_config(config)GWActionConfig — action fields
Section titled “GWActionConfig — action fields”| Field | Type | Description |
|---|---|---|
action_name | String | Internal identifier used in gameplay code (e.g. "jump"). |
display_name | String | Label shown in the rebind UI (e.g. "Jump"). |
description | String | Optional tooltip in the rebind screen. |
category | String | Groups actions in the UI (e.g. "Movement", "Combat"). |
max_bindings | int | Max simultaneous bindings per player. 0 = unlimited. |
action_type | ActionType | BUTTON or AXIS. |
positive_name | String | Axis only — label for the +1.0 direction (e.g. "right"). |
negative_name | String | Axis only — label for the -1.0 direction (e.g. "left"). |
allow_rebind | bool | If false, the action does not appear as editable in the rebind UI. |
default_bindings | Array[GWBinding] | Bindings cloned into each player on load_config(). |
GWBinding — input mapping
Section titled “GWBinding — input mapping”Use the static factory methods to create bindings:
GWBinding.from_key(KEY_SPACE)GWBinding.from_key(KEY_W)GWBinding.from_joy_button(JOY_BUTTON_A)GWBinding.from_joy_axis(JOY_AXIS_LEFT_Y, -1.0) # Analog upGWBinding.from_mouse_button(MOUSE_BUTTON_LEFT)Use get_display_name() to get a human-readable label for any binding:
var b: GWBinding = action.bindings[0]label.text = b.get_display_name() # "Space", "A", "LX +", "Mouse L", etc.GWInputManager API
Section titled “GWInputManager API”Initialize
Section titled “Initialize”func _enter_tree() -> void: GWInputManager.ensure(get_tree().root)Players
Section titled “Players”var p1 := GWInputManager.i.create_player(0, "Player 1")var p2 := GWInputManager.i.create_player(1, "Player 2")
GWInputManager.i.get_player(player_id) -> GWPlayerGWInputManager.i.get_default_player() -> GWPlayer # shorthand for get_player(0)GWInputManager.i.remove_player(player_id)Polling input
Section titled “Polling input”Safe to call from _process() or _physics_process() — no InputEvent required:
GWInputManager.i.is_action_pressed("jump", player_id)GWInputManager.i.is_action_just_pressed("jump", null, player_id) # frame-trackedGWInputManager.i.is_action_just_released("jump", null, player_id) # frame-trackedGWInputManager.i.get_action_strength("move_right", player_id)GWInputManager.i.get_action_axis("move_horizontal", player_id)GWInputManager.i.get_vector("move_left", "move_right", "move_up", "move_down", player_id)For event-based queries (from _input() or _unhandled_input()), pass the event instead of null:
func _input(event: InputEvent) -> void: if GWInputManager.i.is_action_just_pressed("jump", event): velocity.y = -400.0Define actions in code
Section titled “Define actions in code”GWInputManager.i.define_action( "jump", # action_name "Jump", # display_name [GWBinding.from_key(KEY_SPACE), GWBinding.from_joy_button(JOY_BUTTON_A)], "Movement", # category 2 # max_bindings)Runtime rebinding
Section titled “Runtime rebinding”# Start listening for the player's next inputGWInputManager.i.start_rebind_listen(player_id, "jump", binding_index)
# Cancel an active listenGWInputManager.i.cancel_rebind_listen()
# Check if currently listeningGWInputManager.i.is_listening_for_rebind() -> boolPersistence
Section titled “Persistence”GWInputManager.i.save_all_profiles("user://input_profiles.json")GWInputManager.i.load_all_profiles("user://input_profiles.json")GWInputManager.i.reset_all_to_defaults()Devices
Section titled “Devices”GWInputManager.i.get_connected_devices() -> Array # all devices including keyboard/mouseGWInputManager.i.get_unassigned_gamepads() -> Array # gamepads not yet assigned to any playerSignals
Section titled “Signals”signal player_created(player_id: int)signal player_removed(player_id: int)signal device_connected(device_id: int, device_name: String)signal device_disconnected(device_id: int)signal binding_remapped(player_id: int, action_name: String)signal rebind_listening(player_id: int, action_name: String, binding_index: int)signal rebind_cancelled(player_id: int)signal unassigned_device_pressed(device_id: int) # "press to join" supportGWPlayer API
Section titled “GWPlayer API”# Device assignmentp1.assign_gamepad(device_id)p1.unassign_gamepad(device_id)p1.unassign_all_gamepads()p1.accepts_keyboard = true # allow this player to use keyboard/mousep1.has_any_device() -> bool
# Input queries (polling — no InputEvent needed)p1.is_action_pressed("jump")p1.get_action_strength("move_right")p1.get_action_axis("move_horizontal") # returns -1.0 to 1.0p1.get_vector("move_left", "move_right", "move_up", "move_down")
# Event-based queries (from _input / _unhandled_input)p1.is_action_just_pressed("jump", event)p1.is_action_just_released("jump", event)
# Rebindingp1.set_binding("jump", index, binding)p1.clear_action_bindings("jump")p1.find_conflicts(event, exclude_action) # returns Array[GWAction]Signals on GWPlayer
Section titled “Signals on GWPlayer”signal device_assigned(device_id: int)signal device_unassigned(device_id: int)signal action_rebound(action_name: String)GWProfileManager — named profiles
Section titled “GWProfileManager — named profiles”Static utility for saving and loading named binding profiles.
Each profile is stored as a separate file: gw_profile_{name}.json.
GWProfileManager.save(GWInputManager.i, "player1_config")GWProfileManager.load_profile(GWInputManager.i, "player1_config")GWProfileManager.profile_exists("player1_config") -> boolGWProfileManager.list_profiles() -> Array[String]GWProfileManager.delete_profile("player1_config")Quick start — single player
Section titled “Quick start — single player”extends Node
func _enter_tree() -> void: GWInputManager.ensure(get_tree().root)
func _ready() -> void: GWInputManager.i.define_action( "jump", "Jump", [GWBinding.from_key(KEY_SPACE), GWBinding.from_joy_button(JOY_BUTTON_A)], "Movement", 2 ) GWInputManager.i.define_action( "move_right", "Move Right", [GWBinding.from_key(KEY_D), GWBinding.from_joy_axis(JOY_AXIS_LEFT_X, 1.0)], "Movement", 2 ) GWInputManager.i.define_action( "move_left", "Move Left", [GWBinding.from_key(KEY_A), GWBinding.from_joy_axis(JOY_AXIS_LEFT_X, -1.0)], "Movement", 2 )
var p1 := GWInputManager.i.create_player(0) p1.accepts_keyboard = true
func _physics_process(_delta: float) -> void: if GWInputManager.i.is_action_just_pressed("jump"): velocity.y = -400.0 velocity.x = GWInputManager.i.get_action_axis("move_horizontal") * 200.0Quick start — local co-op
Section titled “Quick start — local co-op”func _ready() -> void: var config := preload("res://gw_config.tres") as GWConfig GWInputManager.i.load_config(config) # player_mode = LOCAL_COOP, max = 4
GWInputManager.i.unassigned_device_pressed.connect(_on_press_to_join)
var _player_count := 0
func _on_press_to_join(device_id: int) -> void: var p := GWInputManager.i.create_player(_player_count) p.assign_gamepad(device_id) _player_count += 1 print("Player %d joined with device %d" % [_player_count, device_id])Runtime rebind UI
Section titled “Runtime rebind UI”Use GWInputManager signals to build your own rebind screen:
# Show the current binding labelfunc refresh_binding_label(player_id: int, action_name: String) -> void: var p := GWInputManager.i.get_player(player_id) var action: GWAction = p.actions[action_name] if action.bindings.is_empty(): label.text = "Unassigned" else: label.text = action.bindings[0].get_display_name()
# Start listeningfunc _on_rebind_button_pressed() -> void: GWInputManager.i.start_rebind_listen(0, "jump", 0)
# React when remapping completesfunc _ready() -> void: GWInputManager.i.binding_remapped.connect( func(pid, action): refresh_binding_label(pid, action) )