Need Advice For Improving Player Input Class

Godot Version

Godot 4.2-stable

Question

Hello! I’m trying to know how to improve this player input / controller class I made where the player’s inputs are cached by the class and then is processed within an unnoticeable timeframe before executing the input.

A reason why I opted for this approach is due to the fact I’m currently prototyping a beat em’ up and I wanted to send ‘events’ that the player character will react to depending on the sequence of inputs the player pressed. I initially also wanted to make it “sequence-agnostic” where, for example, pressing either up+attack or attack+up in very quick succession will result in the playable character doing an ‘up attack’.

I’m also using a plugin called ‘Godot StateCharts’ which helped me immensely in setting up the playable character’s states but made it necessary for me to opt-in into sending events this way.

Is there anything I could learn to improve this?

class_name PlayerInputScript
extends Node

@export_category("Dependency Injections")
@export var state_chart : StateChart = null
@export var horizontal_movement_script : PlayerHorizontalMovementScript = null

@export_category("Input Settings")
@export var input_timeout_time : float = 0.1
@export var maximum_inputs_in_queue : int = 2

var action_queue : Array = []
var recently_pressed_inputs : Array = []
var unhandled_actions : Array = ["move_left", "move_right"] #NOTE: THESE ARE INPUTMAP STRINGS!
var instant_actions : Array = ["jump", "attack", "dodge"]
var basic_actions : Array = ["jump", "attack", "dodge", "look_up", "look_down"] #NOTE: THESE TOO ARE INPUTMAP STRINGS!
var compound_actions : Dictionary = {
	"upward_attack" : ["look_up", "attack"],
	"downward_attack" : ["look_down", "attack"],
	"jump_down" : ["look_down", "jump"],
}
var is_input_window_active : bool = false
var current_input_window_time : float = 0.0

# called by an 'unhandled_input' signal sent by a State in the State Chart which simulates a _unhandled_input function call
func queue_input(input : InputEvent):
#region immediately returns/cancels the function if these checks are failed
	if !(input is InputEventKey):
		return

	if !(input.is_pressed()):
		return

	for action in unhandled_actions:
		if input.is_action_pressed(action):
			return
#endregion
	
	for action in basic_actions:
		if input.is_action_pressed(action):
			recently_pressed_inputs.push_back(action)
	
	if !is_input_window_active:
		is_input_window_active = true


# called 'state_processing' signal sent by a State in the State Chart which simulates a _processing function call
func parse_input(delta : float):
	if !is_input_window_active:
		reset_input_arrays()
		return
	
	if current_input_window_time >= input_timeout_time:
		reset_input_arrays()
		return
	
	current_input_window_time += delta

	#this will add an item in the 'action_queue' array if conditions are met.
	check_for_compound_actions()

	if action_queue.is_empty():
		return

	is_input_window_active = false
	current_input_window_time = 0

	for compound_action in action_queue.duplicate():
		match compound_action:
			"jump_down":
				action_queue.pop_front()
				state_chart.send_event("player_jumped_down")
			"upward_attack":
				action_queue.pop_front()
				state_chart.send_event("player_attacked_upwards")
			"downward_attack":
				action_queue.pop_front()
				state_chart.send_event("player_attacked_downwards")

	for basic_action in action_queue.duplicate():
		match basic_action:
			"jump":
				action_queue.pop_front()
				state_chart.send_event("player_jumped")
			"dodge":
				action_queue.pop_front()
				state_chart.send_event("player_dodged")
			"attack":
				action_queue.pop_front()
				state_chart.send_event("player_attacked")


func check_for_compound_actions():
	if recently_pressed_inputs.is_empty():
		return

	if recently_pressed_inputs.front() in instant_actions:
		action_queue.push_front(recently_pressed_inputs[0])
		return
	
	for compound_action in compound_actions.keys():
		var required_inputs = compound_actions[compound_action]
		var input_sequence = []

		for required_input in required_inputs:
			if required_input in recently_pressed_inputs.duplicate():
				input_sequence.push_back(required_input)

		input_sequence.sort()
		required_inputs.sort()
		
		if input_sequence == required_inputs:
			reset_input_arrays()
			action_queue.push_back(compound_action)
			return


func disable_input_parsing():
	horizontal_movement_script.is_input_disabled = true
	state_chart.send_event("input_disabled")


func enable_input_parsing():
	horizontal_movement_script.is_input_disabled = false
	state_chart.send_event("input_enabled")


func reset_input_arrays():
	recently_pressed_inputs.clear()
	action_queue.clear()
	current_input_window_time = 0

No idea about the state charts addon.

My weird brain went to the thought of key1 + key2 + key3 = 111b (binary) for example. So key combos can map to numbers, then it’s a matter of comparing numbers to get what combo is pushed.

Dunno. hth.

for compound_action in action_queue.duplicate():
for basic_action in action_queue.duplicate():
if required_input in recently_pressed_inputs.duplicate():

Why are you duplicating these arrays?
It isn’t necessary. In none of these cases are you making any changes to the array so don’t duplicate them.
These arrays are going to be small so you probably wouldn’t even notice but each time you use duplicate, the system has to create a new array and copy the elements into it. This is wasted processing.
Just loop on the array itself.

for compound_action in action_queue:
for basic_action in action_queue:
if required_input in recently_pressed_inputs:
1 Like

Honestly, just a habit of mine where I subconsciously duplicate an array each time I iterate over it due to me remembering that both accessing and iterating over the same array is a recipe for disaster. And probably sleep deprivation when I initially wrote this.

But you’re absolutely right, there’s no need to duplicate said arrays since I’m not iterating over them.

Definitely an interesting approach compared to using an array, I can’t imagine myself maintaining this system without tripping up often. Sounds like a good strategy to make inputs sequence-agnostic.

1 Like

You can iterate and access arrays all day long. No problem. It’s when you start altering the arrays that stuff gets weird.

Also, have a look at the simply magical array func call filter. It let’s C++ do the iterating and then you can act on each item in a func.

1 Like

That’s cool. I didn’t know array methods like filter is done via C++. I only discovered it recently, and it took me awhile to get a hang of it when I was trying to use it to assess objects with data attached to them.

Definitely could use it to compare if the two latest additions of the recently_pressed_inputs array forms a compound action.

1 Like