I wanted to write a function which would remove items from an array based on their class-type. The problem seems to lie mostly in the fact that custom classes are not present in ClassDB as I would expect them to. I dug a bit through Github and it seems to be quite an old issue (one of the posts is from 2018, https://github.com/godotengine/godot/issues/21789). The alternative here seems to be using the ProjectSettings.get_global_class_list(), but the problem here lies in the fact that I would have to check for multiple base scripts (Script.get_base_script()), which would quickly become unfeasable with a large number of sub-classes. Here's my (rather simplistic) code for now:

func remove_by_type(card_type : Object):
var check_type = card_type.new()

if check_type is CardDefault:
	var script_type : Script = check_type.get_script()
	
	if collection.is_empty() == false:
		for i in range(collection.size()):
			#if collection[i] is card_type: # Does not work, local variables cannot be used as types (and neither can global ones, it seems)
				##do something
			#if collection[i].get_script() == script_type.get_base_script(): # Works only for the class itself, not its sub-classes
				##do something

Any ideas?

  • abSpaghetti The "proper" way of doing this would actually be:

    func remove_by_type(card_type: Object, collection: Array) -> Array:
    	return collection.filter(func(card): return not is_instance_of(card, card_type) )

    Or, the same thing in a more beginner friendly form, with lambda function that's not inlined:

    func remove_by_type(card_type: Object, collection: Array) -> Array:
    	var filter_func = func(card):
    		return not is_instance_of(card, card_type)
    	return collection.filter(filter_func)

    Using Array::filter() saves you from multiple woes: it delegates iteration to native code and it doesn't require creation of any temp arrays. Win-win 😃

I sort of figured it out. I used the is_instance_of() function, like this:

func remove_by_type(card_type : Object):
	if card_type.new() is CardDefault:
		if collection.is_empty() == false:
			for i in range(collection.size() -1, -1, -1):
				if is_instance_of(collection[i], card_type):
					collection.remove_at(i)

I'll do some more testing, but I am pretty sure this is okay. If anyone can figure out something better, let me know

  • xyz replied to this.

    abSpaghetti Don't mess with the iterated container elements form within the iteration loop. It'll end up in strange bugs.

      xyz Would you reckon it'd be better to make this func return an array instead and then just assigning it to the original collection? I could do something like:

      func remove_by_type(card_type : Object, collection : Array) -> Array:
      	var temp_array : Array[CardDefault] = []
      	if card_type.new() is CardDefault:
      		if collection.is_empty() == false:
      			for i in range(collection.size()):
      				if not is_instance_of(collection[i], card_type):
      					temp_array.append(collection[i])
      			return temp_array
      		else:
      			return collection
      	else:
      		return collection
      
      func _on_some_event():
      	collection = remove_by_type(SomeCardType, collection)

        abSpaghetti Anything that don't alter the array during iteration is better. So, yeah, using some kind of temp array would be fine.

        abSpaghetti if collection.is_empty() == false:

        This is equivalent and (in my opinion) more readable:
        if not collection.is_empty():

        abSpaghetti The "proper" way of doing this would actually be:

        func remove_by_type(card_type: Object, collection: Array) -> Array:
        	return collection.filter(func(card): return not is_instance_of(card, card_type) )

        Or, the same thing in a more beginner friendly form, with lambda function that's not inlined:

        func remove_by_type(card_type: Object, collection: Array) -> Array:
        	var filter_func = func(card):
        		return not is_instance_of(card, card_type)
        	return collection.filter(filter_func)

        Using Array::filter() saves you from multiple woes: it delegates iteration to native code and it doesn't require creation of any temp arrays. Win-win 😃

          xyz I wasn't actually aware of filter, how come? Thanks! Looks like I'll have to do some extensive reading on Array's docs lol

          • xyz replied to this.

            abSpaghetti Most container classes in modern languages have some stuff like filter(), map(), each() etc. Very useful for compacting the code and also boosting performance in interpreted languages.

            The only problem is debugging that stuff. You can't set breakpoints and step through a chain of filter/map/each calls at a low level, as you can for a conventional loop. But I guess the code is supposedly so clear that you don't need to resort to that.

            Here's a snippet of Kotlin code that adds up the number of characters in a list of words:

            fun main() {
                    val animals = listOf("raccoon", "reindeer", "cow", "camel", "giraffe", "goat")
                    val totalLength = animals.map { it.length }.reduce { sum, length -> sum + length }
                    println(totalLength) // prints 34
            
                    // a shorter version
                    val totalLength2 = animals.fold(0) {sum, it -> sum + it.length }
                    println(totalLength2) // prints 34
            }
            • xyz replied to this.

              DaveTheCoder Yeah, there's nothing really to debug as it's just some straightforward brute force algorithm. In variants where you need to supply a custom function/lambda, most interpreters will support breakpoints in their code, GDScript included.