Correct way to save keyboard and controller mappings separately?

Godot Version

v4.2.1.stable.mono.official [b09f793f5]

Question

Gonna get straight to the point, I wrote a small function which works quite well for saving the user’s current Input mapping for their keyboard, here it is:

public void SaveKeyboardMappings()
{
	var dictionary = new Dictionary<StringName, Array<InputEventKey>>();
	foreach (StringName action in InputMap.GetActions())
	{
		string actionAsString = action.ToString();
		
		if (actionAsString.Contains("ui") ||
		    actionAsString.Contains("DEBUG")) continue;
		
		Array<InputEvent> events = InputMap.ActionGetEvents(action);

		var arrayOfKeys = new Array<InputEventKey>();
		foreach (InputEvent inputEvent in events)
		{
			if (inputEvent is InputEventKey inputEventKey)
			{
				arrayOfKeys.Add(inputEventKey);
			}
		}
		dictionary.Add(action, arrayOfKeys);
	}

	string jsonString = Json.Stringify(dictionary);
	// TODO: Write to file
}

However, when it comes to saving the controller mappings, I’m not sure how to proceed, since the controller has two different classes for handling Input events, one called InputEventJoypadMotion and another called InputEventJoypadButton.
Something tells me simply using the base abstract class of “InputEvent” is a bad idea, but are there any tips on how to solve this and save the controller mappings in a different file, in a similar way I do with the keyboard mappings?

1 Like

I think I do something very similar. You have to test for both motion and button unfortunately.

Here is a snippet. And I do a similar filtering based on the action string

func get_project_bindings() -> Dictionary:
	var input_actions:Dictionary = {}
	for action in get_input_actions():
		input_actions[action] = {JOYPAD_EVENT:null,MOUSE_KEYBOARD_EVENT:null}
		var events = InputMap.action_get_events(action)
		if events.is_empty():
			continue
		for event in events:
			if event is InputEventJoypadButton or event is InputEventJoypadMotion:
				input_actions[action][JOYPAD_EVENT] = event
			else:
				input_actions[action][MOUSE_KEYBOARD_EVENT] = event
	print(name,": found actions, ", input_actions.size())
	return input_actions

In my setup I allow two input events per action, a keyboard/mouse and a motion/button event.

When the UI updates the binding I keep a dictionary updated and save it to file.

Here is my update snippets

func update_input_bindings():
	print(name,": update input bindings...")
#	debug_print(bindings,"update_input_bindings")
	if bindings.is_empty():
		print(name,": no bindings!!! ", bindings.size(), bindings)
		return
	for action in bindings.keys():
		if InputMap.has_action(action):
			InputMap.erase_action(action)
		InputMap.add_action(action)
		if bindings[action][MOUSE_KEYBOARD_EVENT]:
			InputMap.action_add_event(action, bindings[action][MOUSE_KEYBOARD_EVENT])
		if bindings[action][JOYPAD_EVENT]:
			InputMap.action_add_event(action,  bindings[action][JOYPAD_EVENT])
	print(name,": actions updated ", bindings.size())

func set_binding(action:String, new_event:InputEvent):
	if not bindings.has(action):
		print(name, ": binding failed action not found, ", action)
		return
	if new_event is InputEventJoypadButton or\
		 new_event is InputEventJoypadMotion:
		bindings[action][JOYPAD_EVENT] = new_event
	elif new_event is InputEventKey or\
		 new_event is InputEventMouseButton:
		bindings[action][MOUSE_KEYBOARD_EVENT] = new_event
	update_and_save()
	action_updated.emit(action)

If you intend to deploy on Steam, just be aware they also have an input binder of their own that you could interface with. I think I might myself and I think most of this code will go away because of that.

If your game will fully support controller steam should not affect you

1 Like

I thought I’d update this thread real quick with my findings. It is perfectly fine to use the base class for storing events. Currently my function looks like this, and it works flawlessly for saving the input mappings.

public void SaveControllerMappings()
{
	var dictionary = new Godot.Collections.Dictionary<StringName, Array<InputEvent>>();
	foreach (StringName action in InputRemapHelper.GetActionsWithoutDebugOrDefaults())
	{
		Array<InputEvent> events = InputMap.ActionGetEvents(action);

		var arrayOfInputEvents = new Array<InputEvent>();
		foreach (InputEvent inputEvent in events)
		{
			if (inputEvent is InputEventJoypadButton or InputEventJoypadMotion)
			{
				arrayOfInputEvents.Add(inputEvent);
			}
		}
		dictionary.Add(action, arrayOfInputEvents);
	}

	string jsonString = Json.Stringify(dictionary);
	File.WriteAllText(ControllerMappingsFilePath, jsonString);
	Log.Information("Controller mappings saved successfully!");
}
2 Likes