Input handling in a separate script than the main player one

Godot Version

4.6.3 stable

Question

Hello, I am trying to make a "input_handler" script where every frame it checks for inputs using a function, I would assume a _process one, returning "N" if no movement inputs are received and "None" if no attack inputs are received. I can't return the variables input and att_input into another file though.

extends Node


# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
	
	var input = "N"
	var att_input = "None"
	
	if Input.is_action_pressed("Down"):
		if Input.is_action_pressed("Left"):
			input = "DownLeft"
		elif Input.is_action_pressed("Right"):
			input = "DownRight"
		elif not Input.is_action_pressed("Up"):
			input = "Down"
	if Input.is_action_pressed("Up"):
		if Input.is_action_pressed("Left"):
			input = "UpLeft"
		elif Input.is_action_pressed("Right"):
			input = "UpRight"
		elif not Input.is_action_pressed("Down"):
			input = "Up"
	if Input.is_action_pressed("Right"):
		if Input.is_action_pressed("Down"):
			input = "DownRight"
		elif Input.is_action_pressed("Up"):
			input = "UpRight"
		elif not Input.is_action_pressed("Left"):
			input = "Right"
	if Input.is_action_pressed("Left"):
		if Input.is_action_pressed("Down"):
			input = "DownLeft"
		elif Input.is_action_pressed("Up"):
			input = "UpLeft"
		elif not Input.is_action_pressed("Right"):
			input = "Left"
	
	if Input.is_action_pressed("Light"):
		att_input = "Light"
	if Input.is_action_pressed("Medium"):
		att_input = "Medium"
	if Input.is_action_pressed("Heavy"):
		att_input = "Heavy"
	if Input.is_action_pressed("Unique"):
		att_input = "Unique"
	
	
	print(input)
	print(att_input)
	
	return input
	return att_input
	

This scene works perfectly, every frame, which I capped at 60 FPS in the project settings, it checks for my inputs and prints out the movement (var input) one and the attack one (var att_input). My idea is, I want to put this scene as a child node in my player scene, so I can, after returning input and att_input with this process function, interpret it and make stuff happen(moving left to right, attacking and animation playing). I do this so I can easily make new characters and just use the same input_handler scene. But I can’t understand how to return the input and att_input from this function to another script, I’ve tried “get_node” and @onready var input = $InputHandler._process or something of the sorts and can’t figure out what to do, is it because _process is a function that returns void? I really can’t wrap my head around this. (I understand maybe input_handler isn’t the right name because it doesn’t actually “handle” the inputs but that’s the least of my worries right now.) Any help would be appreciated, thanks. Also I understand that maybe the code isn’t optimized to it’s fullest but since this is my first time actually working with GDScript I don’t expect it to be, so I would appreciate it if you would solely focus on the issue at hand and not some nitpicky details that I might be able to work on after I understand the logic involved with the main issue, would still be helpful of course, just not as “important”.

You could make a global script which does Input Handling.

Then add to the actions what the handler is. You can reference globals from any other scripts after.

Edit: Sorry im not an Expert in GDScript, i mainly use C# so i haven’t provided a good example for that here, may add one later

You cannot return anything from _process, since you don’t call _process yourself. Also, you cannot return more than one value the way you do that, that doesn’t make any sense.
I recommend moving the variables outside of the process function, and simply setting them inside the process. Then you can simply read those properties in another class.

My solution to this sort of thing is to create a Global_Variable script, which I Autoload at start of Project. I would declare the variables in there, instead of in the ‘handler’ script, and write to them in that ‘handler’ using Global_Variable.input and Global_Variable. att_input. These Global variables can then be read from anywhere in the Project.
Disclaimer : this will work, but there may be more elegant ways of doing this. Hope this helps.

What i usually do is create different scripts for Variables (as Globals), and then i also make a Global Inputhandler script which works with Signals. The script which then need Inputs can just add a function to the needed Input. So when an input gets pressed it, all functions get called which subscribed to the Signal.

Here is an example on how you would then use it:

extends CharacterBody2D

@onready var input_handler = $"../InputHandler"

var move_dir := 0
var speed := 200.0

func _ready():
    input_handler.on_move_right_pressed.connect(_on_move_right_pressed)
    input_handler.on_move_left_pressed.connect(_on_move_left_pressed)
    input_handler.on_move_right_released.connect(_on_move_released)
    input_handler.on_move_left_released.connect(_on_move_released)
    input_handler.on_attack_just_pressed.connect(_on_attack_just_pressed)

func _physics_process(delta):
    velocity.x = move_dir * speed
    move_and_slide()

func _on_move_right_pressed():
    move_dir = 1

func _on_move_left_pressed():
    move_dir = -1

func _on_move_released():
    move_dir = 0

func _on_attack_just_pressed():
    print("Attack!")

I see, thanks, also what do you mean by “another class”? My problem with setting them outside the function is I’m not sure it would reset to neutral every frame, but I’m assuming if I just put input = “N” at the start, inside the function it should work, right?

My suggestion would be to store the input state as variables outside of _process() and update them inside the function. Then other scripts can read those values directly. If you want the value to go back to neutral every frame, just set it to a default value at the start of _process() before checking the current input.

If you want to do this part with as little change as possible

That could work, I have another question though, how would I make sure the inputs are being updated every frame? If I call a process(delta) func and then just make it update the inputs, and then in another script can I just call the Global_Variable.input and it would be updated?

Why?

I’m just gonna start there. You are future-proofing something you haven’t even tried to do yet normally. Because if you had, you’d realize that code will not work. (You’ve also turned one line of code into 28 lines of code.)

I’m not going to nitpick the script, as you’ve asked us not to. Your stated goal is to encapsulate the input so you can move it to different characters. Just make this a component on on the player, get a reference to the CharacterBody2D and just manipulate the velocity there. Much easier to do, and if you remove the component, the CharacterBody2D stops getting input from it. Clean and encapsulated.

For what I want to do it’s important I can see exactly what inputs are being pressed and at what times. That’s why I want it to give me the input every frame and possibly later I would have to store it somewhere. To me it just makes sense to put it separately and just make it a child node of the characterbody every time I make a new character

We go back to why? What do you want to do?

You still haven’t told us the problem you’re trying to solve. You’ve told us the solution to the problem and want input. The input I am giving you is this is a classic presentation of the XYProblem. You are asking the wrong question. I am trying to get you to the right question.

Also, there are only 4 inputs if you are only using the keyboard for input. Or you normalize all incoming input.


At any rate, here’s your answer:

This is the default CharacterBody2D code:

extends CharacterBody2D


const SPEED = 300.0
const JUMP_VELOCITY = -400.0


func _physics_process(delta: float) -> void:
	# Add the gravity.
	if not is_on_floor():
		velocity += get_gravity() * delta

	# Handle jump.
	if Input.is_action_just_pressed("ui_accept") and is_on_floor():
		velocity.y = JUMP_VELOCITY

	# Get the input direction and handle the movement/deceleration.
	# As good practice, you should replace UI actions with custom gameplay actions.
	var direction := Input.get_axis("ui_left", "ui_right")
	if direction:
		velocity.x = direction * SPEED
	else:
		velocity.x = move_toward(velocity.x, 0, SPEED)

	move_and_slide()

To do what you want, I’d do this:

Then your Player script becomes this:

extends CharacterBody2D

const SPEED = 300.0
const JUMP_VELOCITY = -400.0


func _physics_process(delta: float) -> void:
	# Add the gravity.
	if not is_on_floor():
		velocity += get_gravity() * delta

	move_and_slide()

And your InputAggregator becomes this:

extends Node

@onready var player = get_parent()

func _physics_process(delta: float) -> void:
	# Handle jump.
	if Input.is_action_just_pressed("ui_accept") and player.is_on_floor():
		player.velocity.y = player.JUMP_VELOCITY

	# Get the input direction and handle the movement/deceleration.
	# As good practice, you should replace UI actions with custom gameplay actions.
	var direction := Input.get_axis("ui_left", "ui_right")
	if direction:
		player.velocity.x = direction * player.SPEED
	else:
		player.velocity.x = move_toward(player.velocity.x, 0, player.SPEED)

But I think what you actually want to do is learn about the Command Pattern.

I made it work by doing this in the input_handling script:

extends Node

var input = "N"
var att_input = "None"

# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
	
	input = "N"
	att_input = "None"

and then in the player script

func _physics_process(delta: float) -> void:
	
	var input = $"input handler".input
	var att_input = $"input handler".att_input