Hello,

The game I'm making involves real-time combat on a tile-based grid. I want to make the input-handling for the player as smooth as possible, but I've come across some issues.

When the player taps the button/key, I want the player character to move one tile:

+-----------------------+
|     |     |     |     |
|     |  X  |     |     |
|     |     |     |     |
+-----------------------+


+-----------------------+
|     |     |     |     |
|     |     |  X  |     |
|     |     |     |     |
+-----------------------+

If the player holds the button/key, I want the player character to move across tiles until they stop holding the button/key:

+-----------------------+
|     |     |     |     |
|  X  |     |     |     |
|     |     |     |     |
+-----------------------+


+-----------------------+
|     |     |     |     |
|     |  X  |     |     |
|     |     |     |     |
+-----------------------+


+-----------------------+
|     |     |     |     |
|     |     |  X  |     |
|     |     |     |     |
+-----------------------+


+-----------------------+
|     |     |     |     |
|     |     |     |  X  |
|     |     |     |     |
+-----------------------+

NOTE: If the player taps the button/key several times, they should get the same result as holding the button/key.

Ultimately, I want player movement to be controlled by how long the button/key is held for - but I want it to be throttled to a fixed speed, instead of as-fast as the system can handle player input.

Here's what I've tried so far:

This allows the player to hold the button/key to move, but input comes in too fast and the player character flies across the screen:

func _process(delta):
	if Input.is_action_pressed("ui_right"):
		emit_signal("moving", "right")

This is more controlled, but does not allow the player to hold the button/key to move. Speed is dependent on player input speed - which is also not ideal:

func _input(event):
	if event.is_action_pressed("ui_right"):
		emit_signal("moving", "right")

NOTE: In both examples above, signal-handling and player character movement is handled by the combat scene at a higher level.

How can I get the best of both worlds, while also controlling the speed of handling user input?

I would suggest using _input() and using is_action_just_pressed and is_action_just_released, to enable and disable a Timer node that spits out your signal at a rate that works. Make that an export variable in your class for editor control. Essentially you are looking for a key repeat type of functionality

You might want to add in other tests if the button has been released for "long" enough in order to stop the signal emission.

So an alternate is to put the is_just_released in the timer response function itself, tracking how long it has been released before shutting itself off.

@dotted said: I would suggest using _input() and using is_action_just_pressed and is_action_just_released, to enable and disable a Timer node that spits out your signal at a rate that works. Make that an export variable in your class for editor control. Essentially you are looking for a key repeat type of functionality

You might want to add in other tests if the button has been released for "long" enough in order to stop the signal emission.

So an alternate is to put the is_just_released in the timer response function itself, tracking how long it has been released before shutting itself off.

Thank you for the suggestion of using a timer!

I've implemented two solutions. They're both almost exactly the same; one uses input event-handling while the other uses input poling:

# Input event-handling solution.

var timer = null
var ignore_input: bool = false

func _ready():
	timer = Timer.new()
	timer.connect("timeout", self, "allow_user_input")
	add_child(timer)
	timer.autostart = false
	timer.one_shot = true
	timer.start(0)

func allow_user_input():
	ignore_input = false

func _input(event):
	if ignore_input == true:
		return
	
	if event.is_action("ui_right"):
		emit_signal("moving", "right")
		
		ignore_input = true
		timer.start(0.5)

# Input poling solution.

var timer = null
var ignore_input: bool = false

func _ready():
	timer = Timer.new()
	timer.connect("timeout", self, "allow_user_input")
	add_child(timer)
	timer.autostart = false
	timer.one_shot = true
	timer.start(0)

func allow_user_input():
	ignore_input = false

func _process(delta):
	if ignore_input == true:
		return
	
	if Input.is_action_pressed("ui_right"):
		emit_signal("moving", "right")
		
		ignore_input = true
		timer.start(0.5)

NOTE: Both of these are implemented on the player character class - which emits a signal to the combat scene at a higher level to actually move the player character in the grid. It would probably make more semantic sense to move this throttling logic up to that level, but I'm not certain if it's more/less efficient - considering the player character would be spamming signals constantly due to player input.

I'll have to do some research to figure out which solution I'll be going with for now.

_input should only be invoked by the engine if an event is directed to the node. "more efficient"

using the Input's is_just_pressed / is_just_released tells you about input transition.

try

func _input(blah blah)

if Input.is_action_just_pressed("ui_right"): timer.start()
elif Input.is_action_just_released("ui_right"): timer.stop()

func _on_Timer_thing():
   	emit_signal("moving", "right") # timer frequency set to desired 'key-repeat'; timer should not be one shot

Alternative the 'animator' code could simply keep moving the player until it is told to stop. In this case no timer is needed, so something like this:

func _input(blah blah)

if Input.is_action_just_pressed("ui_right"): 	emit_signal("moving", "right")
elif Input.is_action_just_released("ui_right"):  	emit_signal("stop_moving", "right")

or more tersely something like:

func _input(blah blah)

if Input.is_action_just_pressed("ui_right"): 	emit_signal("move", move_vector)
elif Input.is_action_just_released("ui_right"):  	emit_signal("move",  Vector3.ZERO)

Lastly in the world of computing even the fastest keyboard spammer is glacial. Don't worry about user spamming the system, rather the resource cost of responding. In other words don't try to defragment your hard drive in response.

@dotted said:

Lastly in the world of computing even the fastest keyboard spammer is glacial. Don't worry about user spamming the system, rather the resource cost of responding. In other words don't try to defragment your hard drive in response.

Fair enough. In that case, I'll move the throttling logic up a level to the combat scene.

2 years later