Skip to content

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.

  1. Download ss-gameforge-wired from the releases page.
  2. Copy addons/ss-gameforge-wired/ into your project’s res://addons/.
  3. Enable ss-gameforge-singleton and ss-gameforge-wired in Project Settings → Plugins.

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 GWConfig resource (default: res://gw_config.tres).

Default bindings are added via code with define_action(), or set directly on the GWConfig resource in the inspector.

ClassDescription
GWInputManagerCentral coordinator — singleton entry point for all input operations.
GWConfigProject-level configuration resource (player mode, max players, action list).
GWActionConfigTemplate defining a single action (name, bindings, category, type).
GWPlayerLogical player with its own devices and independent copies of all actions.
GWActionPer-player runtime action instance (independently rebindable).
GWBindingA single physical input mapping (key, button, axis, mouse button).
GWDeviceAbstraction over a physical device (keyboard/mouse or gamepad).
GWProfileManagerStatic utility for named save/load binding profiles.

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 GWConfig
GWInputManager.i.load_config(config)
FieldTypeDescription
action_nameStringInternal identifier used in gameplay code (e.g. "jump").
display_nameStringLabel shown in the rebind UI (e.g. "Jump").
descriptionStringOptional tooltip in the rebind screen.
categoryStringGroups actions in the UI (e.g. "Movement", "Combat").
max_bindingsintMax simultaneous bindings per player. 0 = unlimited.
action_typeActionTypeBUTTON or AXIS.
positive_nameStringAxis only — label for the +1.0 direction (e.g. "right").
negative_nameStringAxis only — label for the -1.0 direction (e.g. "left").
allow_rebindboolIf false, the action does not appear as editable in the rebind UI.
default_bindingsArray[GWBinding]Bindings cloned into each player on load_config().

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 up
GWBinding.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.
func _enter_tree() -> void:
GWInputManager.ensure(get_tree().root)
var p1 := GWInputManager.i.create_player(0, "Player 1")
var p2 := GWInputManager.i.create_player(1, "Player 2")
GWInputManager.i.get_player(player_id) -> GWPlayer
GWInputManager.i.get_default_player() -> GWPlayer # shorthand for get_player(0)
GWInputManager.i.remove_player(player_id)

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-tracked
GWInputManager.i.is_action_just_released("jump", null, player_id) # frame-tracked
GWInputManager.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.0
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
)
# Start listening for the player's next input
GWInputManager.i.start_rebind_listen(player_id, "jump", binding_index)
# Cancel an active listen
GWInputManager.i.cancel_rebind_listen()
# Check if currently listening
GWInputManager.i.is_listening_for_rebind() -> bool
GWInputManager.i.save_all_profiles("user://input_profiles.json")
GWInputManager.i.load_all_profiles("user://input_profiles.json")
GWInputManager.i.reset_all_to_defaults()
GWInputManager.i.get_connected_devices() -> Array # all devices including keyboard/mouse
GWInputManager.i.get_unassigned_gamepads() -> Array # gamepads not yet assigned to any player
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" support
# Device assignment
p1.assign_gamepad(device_id)
p1.unassign_gamepad(device_id)
p1.unassign_all_gamepads()
p1.accepts_keyboard = true # allow this player to use keyboard/mouse
p1.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.0
p1.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)
# Rebinding
p1.set_binding("jump", index, binding)
p1.clear_action_bindings("jump")
p1.find_conflicts(event, exclude_action) # returns Array[GWAction]
signal device_assigned(device_id: int)
signal device_unassigned(device_id: int)
signal action_rebound(action_name: String)

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") -> bool
GWProfileManager.list_profiles() -> Array[String]
GWProfileManager.delete_profile("player1_config")
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.0
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])

Use GWInputManager signals to build your own rebind screen:

# Show the current binding label
func 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 listening
func _on_rebind_button_pressed() -> void:
GWInputManager.i.start_rebind_listen(0, "jump", 0)
# React when remapping completes
func _ready() -> void:
GWInputManager.i.binding_remapped.connect(
func(pid, action): refresh_binding_label(pid, action)
)