Code execution repeats code after await timer timeout

Godot Version

4.2.1

Question

Hello everyone, I am having some problems understanding how to use or how does the await operator work.

I am using a state variable to know which function to use in each situation, between two particular states I have added some timers to timeout. During execution I found that one function is being executed twice when it shouldn’t (That is my intention at least). Using a break point and looking line by line I found that after a correct change of state after the expected timeout, the next function is executed, but right after that, the previous code after that timer is executed again, making a second loop of the same logic.

I hope this image explains it better. For this explanation, assume the current state variable is set to END_QUOTE;

In this situation, by the forth iteration will be like the second one, but this behavior will happen only once, is not an infinite loop. Until the states reach this situation again, of course.

Am I incorrectly using the await operator with the timeout in this context?

Ask me for any additional information you may need. Thank you for your help!

Using await in _process() is generally a bad idea, because it makes it hard to keep track of the code execution. await delays the execution of a function, but _process() is still called every frame, so you’ll end up with a horrible mess of multiple delayed functions.

The diagram you drew is not what is happening so you are probably misunderstanding await

when you do await get_tree().create_timer(...).timeout the current function is suspended and will resume once it receives the timeout signal from the scene tree timer. This will probably not happen during the same frame so at some later point, the rest of this function will execute, but it’s just waiting until then.

For the process frame 1, when you start out with a state of END_QUOTE you will call handle_end_quote_state() which will stay suspended but do nothing else yet.

On process frame 2, your state hasn’t changed yet so you again call handle_end_quote_state() which also does nothing yet but stays suspended.

Somewhere in between process frame 2 and 3, you receive the timeout signal from the timer you created during process frame 1 so the first call to that function resumes executing. This is why it looks like the second frame continued with the function, but that wasn’t the case at all. your currentState is now HIDE_FACE

On process frame 3 your state is HIDE_FACE so you instead call handle_hide_face_state() which plays an animation and then suspends itself until the animation player has finished that animation. Your state is not set to IDLE because the animation hasn’t completed yet.

And finally sometime after process frame 3 you receive the second timeout signal from the second timer that was created during process frame 2 so that function resumes execution and your state is set to HIDE_FACE although it was probably still set to that anyway.

Hope this helps clear up what’s going on.

Fixing it

You can make things less confusing by connecting to a signal rather than awaiting it.

For the quotePlayer you could connect to the animation_finished signal with a method like:

func _on_quotePlayer_animation_finished():
  if currentState == HIDE_FACE:
    currentState == IDLE

The logic for changing state needs to be able to account for the fact that something unexpected may have happened in between when you started the animation and when it finished, but that’s a separate topic

and for the case of waiting for a delay in the end quote state, you could use a Timer node to do the delay instead of the temporary timer, but to use the temporary timer you can connect to it’s signal like this

var delay: = get_tree().create_timer(delayBetweenQuotes)
delay.timeout.connect(_on_quote_delay_finished, CONNECT_ONESHOT)

func _on_quote_delay_finished():
  if currentState != END_QUOTE:
    # Something else happened in the meantime
    return
  else:
    currentState = HIDE_FACE
    quoteLabel.text = ""
    quoteTimer.stop()

Finally, you probably only want to call handle_end_quote or handle_hide_face once not every frame during those states until you change to another state. This is actually the main reason why you’re having problems here. Instead you should probably handle code that needs to run once during a state transition at the same time you switch to another state. like having a change_state(new_state) method that handles all of the one-time tasks that need to be performed.

If that isn’t a good option for some reason and you want the one time action to be performed on the first frame of the new state instead you can just make sure the logic only runs once. There are a lot of ways to handle this but I would suggest doing something like this:

var processStateEnabled: = true

# I'm using a setter to ensure that every time I change state, processing is enabled
var currentState: = State.END_QUOTE:
  set(new_state):
    currentState = new_state
    processStateEnabled = true

func _process(_delta):
  # Check if we should call the process method for the current state
  if not processStateEnabled:
    return
  
  match currentState:
    ...

func handle_end_quote_state() -> void:
  # This should only run for first frame of being in this state, so disable processing
  processStateEnabled = false
  ...

# Same for any other state method that happens only once
2 Likes

Then I just misunderstood completely what the await does.

Before I thought the await was some kind of stopper to the code, that until the timeout was active the code execution will continue but would stop there, and continue after that.

If now I am not mistaken, the await actually does a save state, a checkpoint of sorts that will stay frozen, without taking care of what continues to happen in the next frames, until the timeout reaches and is time to continue the execution. Given that, having an await in any kind of loop will create the behavior I was experiencing and, in this case, looks like the second time execution is a delay coming from nowhere when it was actually the save point resuming its process.

I think I understand now, I will read carefully the solutions given and check if I should go for them or rethink what I want to do.

Thank you very much for the responses!

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.