How to better handle controller inputs on UI

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By paoloforti96

Hi, I am currently working on a Menu, that is divided into sections.

I am trying to code the UI so that it can be fully compatible and navigable with a joystick.

Right now, each section can be accessed by using r1 and l1 to navigate between them.
Everything works fine and the code is still acceptable. I can also open and close the menu with the “start” button.

The problem is that, as I was coding the logic for the inventory, which should allow the user to interact with the items, the code is getting more and more messy.

There is probably no unique way to organize the code to handle different kinds of inputs across stacked popup windows, but I would like to know if any of you could be so nice to give me some guidance or show me an example of how this could be done in a better and cleaner way.

One example of my code so far is this:
This code is attached to the Inventory node itself.
Inside the inventory slot I handle part of the input that is used to navigate across the different possible actions on the items, such as “use item” and “throw/drop item”


func _input(event):
	match InventoryManager.interaction_stage:
		InventoryManager.interaction_stages.NONE:
			handle_normal_input(event)
		InventoryManager.interaction_stages.USE_ITEM:
			handle_use_item_input(event)
		InventoryManager.interaction_stages.THROW_ITEM:
			handle_throw_item_input()
		InventoryManager.interaction_stages.CONFIRM_ACTION:
			handle_confirm_input()

func handle_normal_input(event):
	if Input.is_action_pressed("ui_up"):
		change_active_slot(-1)
	elif Input.is_action_pressed("ui_down"):
		change_active_slot(1)
	elif event.is_action_pressed("ui_left"):
		change_active_item_category(-1)
	elif event.is_action_pressed("ui_right"):
		change_active_item_category(1)
	elif event.is_action_pressed("menu_interact"):
		if !items_list.get_child(active_slot).is_empty():
			items_list.get_child(active_slot).show_actions_popup()
			set_process_input(false)
	
func handle_use_item_input(event):
	if event.is_action_pressed("menu_interact"):
		use_item()
		print("here")
	elif event.is_action_pressed("menu_go_back"):
		perform_action_dialog.hide()
		items_list.get_child(active_slot).can_interact = true
		InventoryManager.interaction_stage = InventoryManager.interaction_stages.SELECT_ACTION
	elif Input.is_action_pressed("ui_left"):
		perform_action_dialog_quantity.value -= 1
	elif Input.is_action_pressed("ui_right"):
		perform_action_dialog_quantity.value += 1

func handle_throw_item_input():
	if Input.is_action_pressed("ui_up"):
		perform_action_dialog_quantity.value -= 1
	elif Input.is_action_pressed("ui_down"):
		perform_action_dialog_quantity.value += 1

func handle_confirm_input():
	pass

func change_active_interaction_action(direction):
	pass

...

Thanks in advance to anyone that will try to help me

I rewrote a part of the code and decided to divide each dialog into its own scene, in order to have a different _unhandled_input method for each of them.
Now the code is not all compressed into a single script file, and I can now handle different inputs, without the risk of them overlapping and thus firing two or more at once, resulting in an unpredictable input action flow.

The main drawback of this method is that I have to disable the process input of the dialogs every time a new dialog pops up on top of another to allow each dialog to handle its own input.

So the flow is as follows:

Inventory Input → select and click on an item → diable inventory input → enable item actions popup input → choose an action → open a new action dialog and enable its input → disable previous popup input → perform action → close dialog, disable its input and re-enable the inventory input.

Codewise it is not as messy as before but has a lot of in-between steps that need to be handled to achieve the desired result.

I will probably iterate again through this solution, but the overall concept will most likely remain the same

paoloforti96 | 2020-11-25 08:59

:bust_in_silhouette: Reply From: magicalogic

You should probably use the unhandled_input(event) method to handle input in the game and _input(event) to handle input in the UI.

Hi, thanks for your answer.

Yeah, I already did so, after finding a video about input handling.
But I still had to keep almost the same logic as before to make it work.
Is there no smarter way to organize the code?

paoloforti96 | 2020-12-01 16:01

:bust_in_silhouette: Reply From: paoloforti96

So, after a bit of trial and error, I decided to write an Input Manager, to enable and disable the input depending on where I am on the interface, but can also be used inside the game itself.

For example, the initial input source will only be the actual game instance.
If i then open the menu, the input source will now be the menu, and the game input will be disabled. Each input source can have a sub input source to enable the handling of 2 input sources together, which suited my case, because i still want to be able to navigate the menu, while also handling some section specific input.
If i then toggle some action in the menu sections, the new input source will be the new popup or whatever needs to allow some user interaction, without also triggering any of the menu or section input. So the only input source is always the window in focus.
To do this i wrote the following global script:

extends Node

var active_input_source = null
var previous_active_input_sources : Array = []


# Called when the node enters the scene tree for the first time.
func _ready():
    pass # Replace with function body.

func change_input_source(new_source : Node):
    if active_input_source != null:
	     disable_active_input_source()

     var source = {
	    "main_source": new_source,
	    "sub_source": null
     }

    previous_active_input_sources.push_back(source)

    active_input_source = source
    enable_input_source(active_input_source.main_source)

func return_to_previous_input_source():
    disable_active_input_source()

    previous_active_input_sources.pop_back()
    active_input_source = previous_active_input_sources[previous_active_input_sources.size() - 1]
    enable_active_input_source()

func disable_active_input_source():
    disable_input_source(active_input_source.main_source)
    if active_input_source.sub_source != null:
	    disable_input_source(active_input_source.sub_source)

func enable_active_input_source():
    enable_input_source(active_input_source.main_source)
    if active_input_source.sub_source != null:
	    enable_input_source(active_input_source.sub_source)

func change_sub_input_source(new_source : Node):
    if active_input_source != null:
	    if active_input_source.sub_source != null:
		    disable_input_source(active_input_source.sub_source)
	    active_input_source.sub_source = new_source
	    enable_input_source(active_input_source.sub_source)

func enable_input_source(source : Node):
    source.set_process_input(true)
    source.set_process_unhandled_input(true)

func disable_input_source(source : Node):
    source.set_process_unhandled_input(false)
    source.set_process_input(false)

To make it work you just need to call the change_input_source method once on your main game instance script, or whatever instance controls your main input to switch between game and menu, and pass “self” as a parameter.
To return the input source to a previous one, I added a method to just do that. Just call the return_to_previous_input_source() from whenever and the current input source will be disabled, as well as any sub input source input, re-enabling the previous input source.

Hopefully this solution can help others with the same issue as me, and maybe you can come up with even better and versatile solutions