_process() in a turn-based game - turn changes far too fast!

Godot Version

v4.3.stable.official [77dcf97d8]

Question

Hello,

I’m trying to make a 3D turn-based game and I’m getting strange behaviour when I swap turns. Here’s how I intended to implement it, along with the code:

  • In _ready(), the world script adds everything that has a turn to an array, sorts them by speed, starts counting turns and sets the first node state to “active” - this seems to work okay.
func _ready() -> void:
	for N in get_tree().get_nodes_in_group("takes_turn"):
		turn_counter.append(N)
		
	sort_turns(turn_counter)
	active_turn = 0
	turn_number = 1
	actor_count = turn_counter.size()
	turn_counter[active_turn].state = turn_state.ACTIVE
  • In its _process() function, each node checks to see if it’s active, prints “0” to let me know, then emits an “end turn” signal.
func _process(delta: float) -> void:
	if state == turn_state.ACTIVE: #checks if turn state is active
		print(str(self) + "0")
		end_turn.emit() # emits end turn signal
  • Then the world script uses its own _process() function to await an “end turn” signal from the active node, sets the active node to “idle”, increments through the turn order array and sets the next node to “active”.
func _process(delta: float) -> void:
	change_turn()

func change_turn():
	await turn_counter[active_turn].end_turn #waits for actor's end turn signal
	print(str(turn_counter[active_turn]) + ": ending turn") #prints whose turn is ending
	turn_counter[active_turn].state = turn_state.IDLE #sets current actor to IDLE state
	if active_turn < actor_count - 1: #checks to see if at end of turn order; 
		active_turn += 1 #increments turn order if not
	else:
		active_turn = 0 #if it is, returns turn number to 0
		turn_number += 1 #and increments the turn number
		
	turn_counter[active_turn].state = turn_state.ACTIVE #changes the current turn number's state to ACTIVE
	print(str(turn_counter[active_turn])+ ": starting turn") #prints whose turn is starting
  • When it’s the player’s turn I can press the spacebar to end it - that’s all I need to do for now.
func _physics_process(delta: float) -> void:
	if state == turn_state.ACTIVE:
		if Input.is_action_just_pressed("end_turn"):
			end_turn.emit()

To test it, I have two identical plain Nodes (called watcher and watcher2) and a CharacterBody3D for the player. The first watcher starts its turn, prints 0 and ends its turn. The world script tells me that the first watcher has ended its turn and passes the turn to the second. The second one starts its turn, prints 0, prints 0 again, then ends its turn. The turn passes to the player.

watcher:<Node#29410460979>0
watcher:<Node#29410460979>: ending turn
watcher2:<Node#29427238196>: starting turn
watcher2:<Node#29427238196>0
watcher2:<Node#29427238196>0
watcher2:<Node#29427238196>: ending turn
player:<CharacterBody3D29242688796>: starting turn

When I press the spacebar, the turns pass about 20 times, with no output until the final one, when it waits for player input again.

player:<CharacterBody3D#29242688796>: ending turn
watcher:<Node#29410460979>: starting turn
watcher:<Node#29410460979>: ending turn
watcher2:<Node#29427238196>: starting turn
watcher2:<Node#29427238196>: ending turn
player:<CharacterBody3D#29242688796>: starting turn
player:<CharacterBody3D#29242688796>: ending turn
watcher:<Node#29410460979>: starting turn
watcher:<Node#29410460979>: ending turn
[repeat many times]
watcher2:<Node#29427238196>: starting turn
watcher2:<Node#29427238196>: ending turn
player:<CharacterBody3D#29242688796>: starting turn
player:<CharacterBody3D#29242688796>: ending turn
watcher:<Node#29410460979>: starting turn
watcher:<Node#29410460979>0
watcher:<Node#29410460979>: ending turn
watcher2:<Node#29427238196>: starting turn
watcher2:<Node#29427238196>0
watcher2:<Node#29427238196>0
watcher2:<Node#29427238196>: ending turn
player:<CharacterBody3D#29242688796>: starting turn

I suspect this is an issue with the framerate being too high for the nodes to do anything. They’re clearly emitting the “end turn” signal, or else they wouldn’t change turns at all, but I’m not sure how they’re skipping the “print 0” line. I’ve tried forcing the nodes to wait for a signal from the world script to start but it doesn’t seem to have helped.

Any thoughts on why this is happening, what I can do to solve it, or a better implementation would be greatly appreciated - this is my first real project in Godot and things were going very smoothly until this happened!

Hi, I think you are using the process function wrong. There is no need to check every frame if a signal has been emitted. If you connected the signal then it will be received outside of the process function. By calling a function with await in it every frame, the node is awaiting the same signal many times since process is still called the next frame.

4 Likes

@paintsimmon 's got it, just to hammer home; _process is called every frame no matter what, the awaits halt the current function, but future frames still make a new call to _process and the new function awaits the same. This stacks up until the signal is emitted and they all fire off, or until you run out of memory and crash.

Maybe connecting the signal with CONNECT_ONE_SHOT would behave as you intend?

func change_turn():
	#await turn_counter[active_turn].end_turn # remove this
	print(str(turn_counter[active_turn]) + ": ending turn")
	# etc ...
	print(str(turn_counter[active_turn])+ ": starting turn")
	# connect to next actor on end_turn
	turn_counter[active_turn].end_turn.connect(change_turn, CONNECT_ONE_SHOT)
1 Like

Okay, I think I see the problem now - I assumed that to be listening for the signal I needed them to be listening in every frame, but it would make more sense to call the actors’ turn functions from the parent script. Thank you very much for your help - back to the drawing board for me!

I think I’ve solved it now - here’s my solution in case it helps anyone.

  • In the world’s _ready() function I build the turn tracker (basically the same as before) and run the new start_turn() function, which is much neater. I learnt the hard way not to include the brackets on the callable inside the connect()! :sweat_smile:
func start_turn():
	current_actor = turn_tracker.pop_front() #takes the first actor from the turn tracker
	
	current_actor.state = 1 #sets the first actor to active
	current_actor.turn_end.connect(change_turn) #starts listening for signal, ready to call change_turn
	current_actor.take_turn() #runs the actor's turn function
  • In the actor’s take_turn() function it just prints a line, sets itself to idle, and emits the turn_end signal:
func take_turn():
	if state == 1: #checks if turn state is active
		print(str(self) + ": Hello, taking my turn now")
		state = 0
		turn_end.emit()
  • When it hears the turn_end signal, the world script runs change_turn:
func change_turn():
	current_actor.turn_end.disconnect(change_turn) #stops listening for signal, very important!
	if turn_tracker.is_empty(): 
		set_up_turns() #builds turn tracker again if we've reached the end
	start_turn()

Note that this is only limited by input when it’s the player’s turn, which I haven’t included here. If there’s no player to stop them, the actors will very politely say they’re taking their turn one after the other until the output overflows.

Thank you for your help!

2 Likes

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