I'm trying to build a simple turn based battle system. To handle turn order, i'm putting enemies into an array, and letting them take turns. When an enemy is done with its turn, it gets popped from the array and the next enemy acts.

However, if an enemy is killed and freed from the array, it still remains as a <Freed Object>, so the array is not truly empty. This can mess up checks for whether enemy_array.is_empty() for example.

I've been using a work_around like this to check if my enemy_array is empty, so the player's turn can start when all enemies have acted:

			if !enemy_turn_order.is_empty() and enemy_turn_order[0] != null:
				enemy_turn_order.pop_front().take_turn()

My main question would be, does a freed object count as null? Should i just treat it as such to avoid errors/bugs?

Also if anyone has better advice for how to handle turn order in general, i'm all ears. I've been at this mini project for a month, but i can't seem to find a good solution for handling multiple enemies. Single enemy is no problem, but multiple actors taking turns becomes confusing fast.

edit:

Seems making the enemy remove itself from the array on death seems to be an option to avoid the Freed Object clogging up the turn order:

enemy_turn_order.erase(self)

  • Squiddy, Jesusemora, and xyz replied to this.
  • Squiddy My main question would be, does a freed object count as null?

    No. Those two things have different values, although both represent an invalid reference. However comparing a freed object to null using operator == will return true. Also implicit casting of freed object to boolean will return false (for example in if statements). So doing an if foo: check on a potentially freed object should be fine.

    Note that each thing in your array is not an actual object but a reference to some object. So when the object is gone (freed) a reference in the array will still be there, just that it will refer to an object that's not alive anymore, hence "<Freed Object>". So when the object is gone, it's your responsibility do dispose of any references to it your code still may hold, either as array elements or otherwise.

    Squiddy

    Just as an addendum, i found a better method for handling turn order in this tutorial series:

    Instead of squeezing enemies into arrays, you use an EnemyHandler Node that holds all the enemies as children. The turn order then depends on the index number of the EnemyHandler's children.

    I find this a much more manageable approach.

    DaveTheCoder

    I tried is_instance_valid() but something about it didn't work as expected.

    I reworked the code heavily since yesterday and don't recall the exact error, but something like it can't check the instance on a base of a script.gd? Because it's checking a script and not an instance... I dunno, i moved away from the array approach anyway, it's too cumbersome.

    Squiddy I added enemy removal yesterday after reading this post, now it's complete and I can answer:

    Squiddy i'm putting enemies into an array, and letting them take turns.

    that's correct

    Squiddy When an enemy is done with its turn, it gets popped from the array and the next enemy acts.

    this part is not.

    the way I did it is, I added all the "acting" units to an array, and then I access the elements of the array using an int current_selected. when the next unit goes, I add 1 to current_selected and when it overflows i reset it to 0.
    when a unit dies, I remove it from the array with remove_at(), but your could use erase() to remove by reference instead of id.

    var TurnOrder : Array[ControllableUnit]
    var current_sel : int = -1
    
    func get_units() -> void:
    	var units = get_tree().get_nodes_in_group("unit")
    	#TODO sort, turn order
    	var w : int = 0
    	if units:
    		for i in units:
    			if i is ControllableUnit:
    				TurnOrder.append(i)
    				i.unit_dies.connect(remove_from_turn_order.bind(i), CONNECT_ONE_SHOT)
    				i.set_turn_position(w)
    				TerrainMap.set_voxel(Vector2i(round(i.global_position.x), round(i.global_position.z)), 1, 0)#TODO 4x4
    				main_UI.spawn_bar(i)
    				w += 1
    
    func next_unit() -> void:
    	current_sel += 1
    	if current_sel >= TurnOrder.size():
    		current_sel = 0
    	selected = TurnOrder[current_sel]
    	print(current_sel)
    	unit_actions = selected.get_num_actions()
    	unit_bonus_actions = selected.get_num_bonus()
    	TerrainMap.set_voxel(Vector2i(round(selected.global_position.x), round(selected.global_position.z)), 0, 0)
    
    func remove_from_turn_order(targ : ControllableUnit) -> void:
    	TurnOrder.remove_at(targ.turn_position)
    	var w : int = 0
    	for i in TurnOrder:
    		i.set_turn_position(w)
    		w += 1

    of course in my case, units are not removed after they die, they are left fallen on the map. but after they are out of the array I could very easily remove them without side effects.

    Squiddy My main question would be, does a freed object count as null?

    No. Those two things have different values, although both represent an invalid reference. However comparing a freed object to null using operator == will return true. Also implicit casting of freed object to boolean will return false (for example in if statements). So doing an if foo: check on a potentially freed object should be fine.

    Note that each thing in your array is not an actual object but a reference to some object. So when the object is gone (freed) a reference in the array will still be there, just that it will refer to an object that's not alive anymore, hence "<Freed Object>". So when the object is gone, it's your responsibility do dispose of any references to it your code still may hold, either as array elements or otherwise.