Which approach is best for handling input with analog stick/joypad?

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

I completed this excellent 2D platformer tutorial series by Youtuber BornCG and wanted to modify the final product once completed into a more full project.

Movement is handled in the _physics_process(delta) function of the player script by checking Input.is_action_pressed(action) or Input.is_action_just_pressed(action) for various actions.

After some modifications to how jumping works, I noticed that jump input was not always captured and so repeated presses of the jump button only sometimes resulted in a jump, even when the character was on the floor. After some googling, I found this SO answer which indicated that I should really be capturing my inputs in the _input(event) function. However, the Godot Node documentation indicates that _unhandled_input(event) is likely preferable for gameplay input, so I changed my code to the following:

var right_pressed = false
var right_strength = 0
var left_pressed = false
var left_strength = 0
var down_pressed = false
var jump_pressed = false


func _unhandled_input(event):
	right_pressed = event.is_action("ui_right")
	right_strength = event.get_action_strength("ui_right")
	left_pressed = event.is_action_pressed("ui_left")
	down_pressed = event.is_action_pressed("ui_down")
	jump_pressed = event.is_action_pressed("jump")


func _physics_process(delta):
	
	if right_pressed:
		velocity.x = SPEED*(pow(3,right_strength) - 1)*delta
		$Sprite.play("move_horizontal")
		$Sprite.flip_h = false
			
	elif Input.is_action_pressed("ui_left"):
		left_strength = Input.get_action_strength("ui_left")
		velocity.x = -SPEED*(pow(3,left_strength) - 1)*delta
		$Sprite.play("move_horizontal")
		$Sprite.flip_h = true
		
	if jump_pressed and is_on_floor():
		jump_pressed = false
		velocity.y = JUMPFORCE*delta
		$Sprite.play("jump")
		$SoundJump.play()

  if is_on_floor():
	$Sprite.play("idle")
  else:
		# Not on floor #
		velocity.y = velocity.y + delta*GRAVITY			
		if(velocity.y >= 0):
			$Sprite.play("idle")
		else:
			$Sprite.play("jump")
			
	velocity = move_and_slide(velocity, Vector2.UP)
    if velocity.y > 0:
	    velocity -= Vector2.UP * GRAVITY * (FALL_MULTIPLIER - 1) * delta
    elif velocity.y < 0 and not Input.is_action_pressed("jump"):
	    velocity -= Vector2.UP * GRAVITY * (LOW_JUMP_MULTIPLIER - 1) * delta

    velocity.x = lerp(velocity.x,0,0.2)

This did not fix the jumping issue (perhaps it made it slightly better), but it did cause big issues with recognizing movement to the right on the analog stick. With the code above, movement is only recognized if the joypad is slightly tilted to the right, and even then it is spotty. Movement to the left is still handled with Input.is_action_pressed(action) in the _physics_process(delta) function, and it works much better.

So I guess my question is:

Should I be capturing gameplay input with Input.is_action_pressed(action) within _physics_process(delta) or within _input(event), and whichever is the case, how can I ensure that my inputs are always captured?

I can answer one part I think: if you want a given input to be involved with physics in any way, it should probably be in_physics_process(). Some player inputs aren’t related to physics, like opening a menu, so I would probably handle those events somewhere else, perhaps _unhandled_input().

One question, what is the reason for this code: velocity.y = JUMPFORCE*delta? Specifically involving delta. Maybe that’s a problem? I figured velocity application was the only time that’s really relevant, but I do not personally code much real-time physics stuff.

DDoop | 2021-01-20 19:08

I am using the _physics_process() to handle the inputs, so to speak, but the linked SO response seems to be saying that because _physics_process() isn’t sure to run every frame it may miss inputs and thus the _input() class of functions is where the actual capturing should take place. Do you know if this is accurate?

My plan was to handle things like opening a menu in the _gui_input() function. Should I use _unhandled_input() instead for that?

My reason for multiplying my jumpforce by delta is kind of related to the above: because _physics_process() isn’t sure to run every frame movement might look jittery if, say, 1/59 rather than 1/60 of a second passed since the last call. So by always multiplying velocity perturbations by the amount of time since the last call I can smooth out the movement. That’s what I’ve read anyway.

EDIT
Looking at the Godot documentation on InputEvents, it does seem that _unhandled_input() is preferred for gameplay:

For gameplay input, Node._unhandled_input() is generally a better fit,
because it allows the GUI to intercept the events.

Godont | 2021-01-20 21:45

_physics_process() gets called at a rate independent of your fps. Strick’s answer seems right to me now – I was wrong :wink:

I think capturing all user input should be performed in the _input() family of methods (except things like Control signals), and processing of the input (making the world react to the user) should happen somewhere else (i.e. _physics_process() if it has to do with physics).

Why is there a whole family of different _input() methods and which is best for which case? I don’t really know… I’m sure someone wrote somewhere online why they implemented it. Probably some keypresses in _input() override others in _unhandled_input() depending on the user context, i.e., you open a menu and the d-pad’s inputs are all taken care of in the menu’s _input() function and never propagate to the game world’s _unahndled_input() to move the character.

In any case, I think you can put the jumping code in an _input()

I’m still not convinced you need to involve delta with your line setting the y velocity. I think that basically evaluates to velocity.y = JUMPFORCE * 0.01666667 if the system isn’t under heavy load. If your [users’] computer[s] can’t keep up, then that delta value will just get higher, and the player will end up actually jumping higher, which isn’t what you want; the player’s jump height should be independent of the time from the last frame calculation.

DDoop | 2021-01-20 22:10