How to match Class type

Godot Version

v.4.4.1

Question

As the title says I want to understand if it’s possible to do something like this:

var hit: Node3D = raycast.get_collider()

match hit:
	Item:
		some_logic()
	SomethingElse:
		some_other_logic()
	_:
		some_default_logic()

I tried implementing this and it does not work, however you can easily check for

if hit is Item:
	some_logic()

I just wanted to have a match since I believe it looks quite cleaner instead of chaining if-else(s) or creating multiple stand-alone, non-mutual, if clauses.

You can use the get_class() method, although it won’t work with classes declared with class_name.

match object.get_class():
	"Node3D":
		some_logic()
	_:
		some_default_logic()
1 Like

Thanks for the info. Unfortunately I need it to work with custom classes.

Do you happen to know if we already have an issue on GitHub about this or if it is in scope for any future update? I’ve read they will finally allow the declaration of abstract classes, perhaps more gdscript polish in on the way?

1 Like

It’s a kludge, but you can add a const to your class which you can use in your match statement.

class_name Item

const CLASS_TYPE = "Item"

You could use pattern guards in your match statement, but that essentially turns your match into if statements in-disguise. But maybe you will still prefer it this way.

var hit: Node3D = raycast.get_collider()

match hit:
	_ when hit is Item:
		some_logic()
	_ when hit is SomethingElse:
		some_other_logic()
	_:
		some_default_logic()
1 Like

As others have mentioned, you can’t do match, but you can do is, and it does work with custom classes. get_class() does not work with classes declared with class_name.

However, I’d like to offer you an alternate solution. Instead of using classes use collision layers and masks. Right now, you’re building up for a massive if/else statement. Much cleaner to just put things that should behave differently on a different layer.

Let’s say that I have a character with a sword, and when I swing that sword, it can break a box, hit an enemy, or do nothing but play a tink sound because whatever it hit is metal and isn’t susceptible to the sword.

So we set up our collision layers:

  1. Environment (walls, floors, etc.)
  2. Player
  3. Enemy
  4. Boxes
  5. Metal Items

Our sword has three Area3Ds with CollisionShape3Ds. They all share the same shape. Each Area3D has no layers set, and each has one of 3 masks set: 3 (enemy), 4 (boxes), or 5 (metal items).

var damage_amount = 5.0

@onready var enemy_area_3d: Area3D = $EnemyArea3D
@onready var boxes_area_3d: Area3D = $BoxesArea3D
@onready var metal_items_area_3d: Area3D = $MetalItemsArea3D


func _ready() -> void:
	enemy_area_3d.body_entered(_on_hit_enemy)
	boxes_area_3d.body_entered(_on_hit_box)
	metal_items_area_3d.body_entered(_on_hit_metal_item)

# I know that this will only be called when I hit an enemy that has a damage() function.
func _on_hit_enemy(body: Node3D) -> void:
	body.damage(damage_amount )

# I know that this will only be called when I hit a box that has a destroy function().
func _on_hit_box(body: Node3D) -> void:
	body.detroy()

# I know that this will only be called when I hit something I cannot damage.
func _on_hit_metal_item(body: Node3D) -> void:
	play_tink_sound()

If you want to use a raycast because let’s say you’re shooting a bullet and you don’t have a physical bullet to put an Area3D on, you can do the same with 3 raycasts.

In my opinion, that’s a better way of dealing with the problem you presented in code. However, I’m going to take it another step further. You can simplify this problem even further. Just give anything that can be hit by the sword a damage() function. You can keep the three layers or use only one or two, but you only need one collision for all of them.

var damage_amount = 5.0

@onready var weapon_area_3d: Area3D = $WeaponArea3D

func _ready() -> void:
	weapon_area_3d.body_entered(_on_hit)

# I still know that this will only be called when I hit something that has a damage() function.
func _on_hit(body: Node3D) -> void:
	body.damage(damage_amount )

Each object that can take a hit then deals witht he damage in its own way. Enemy applies it to hp, boxes just break, and metal items play tink sound.

If you want the environment to respond even to a hit, and all your walls and floors are stone and you don’t want to add code to them, you can turn the Area3D mask 1 on and do this:

var damage_amount = 5.0

@onready var weapon_area_3d: Area3D = $WeaponArea3D

func _ready() -> void:
	weapon_area_3d.body_entered(_on_hit)

# I still know that this will only be called when I hit something that has a damage() function.
func _on_hit(body: Node3D) -> void:
	if body.has_method("damage"):
		body.damage(damage_amount )
	else:
		play_tink_on_stone_sound()

I really think this is the logic you want. Let each item know how to deal with its damage, not the player.

3 Likes

This is quite a nice trick you pointed out there. I will definitely try this out, thanks :slight_smile:

Also another neat trick/solution. This made me wonder, however, do you think this could be done better through groups?

I am not a huge fan of the multiple Area3Ds, and even less of a fan about adding a damage function to all my “damageable” entities without explicitly defining some sort of contract for them. Unfortunately Godot does not offer interfaces and sometimes inheritance can’t fit well in this circumstances. This is where composition plays a good role and that’s my approach. However, my question I guess is, would defining a “damageable” group make sense and the check if the hit is in that group or not? This way it can be easily scaled (I believe).

My reason for explaining the multiple Area3Ds was just to show you, and anyone else who is reading this in the future after a Google search, what is possible. It was more of an academic exercise. My real point was that I believe your code would benefit from more encapsulation.

Whatever you hit with your weapon, your weapon doesn’t need to know anything about what it hit.

My other reason for posting the example, is that it has been my understanding that Area3Ds are cheaper than Raycasts processor-wise.

You haven’t read the 4.5 beta 1 release notes yet. The abstract keyword (later to be converted to the @abstract annotation) was just introduced. So were variadic arguments - which I’m personally very excited about.

So yes, you can now create a completely abstract class and create an interface.

Agreed. But again, that’s what the masks and layers are. With the approach I outlined, when you attach a layer to an object such as enemy you are explicitly saying “this thing has a way to accept damage”.

If you really want to use composition and Godot’s methodology you could add a Node called HealthNode as a child of anything that can receive damage.

class_name HealthNode extends Node

@export var health: float = 20.0

func damage(amount: float) -> void:
	health -= amount;
	if health <= 0.0:
		get_parent().die()

Then in your collision code:

var damage_amount = 5.0

@onready var weapon_area_3d: Area3D = $WeaponArea3D

func _ready() -> void:
	weapon_area_3d.body_entered(_on_hit)

# I still know that this will only be called when I hit something that has a HealthNode.
func _on_hit(body: Node3D) -> void:
	for node in body.get_children():
		if  node is HealthNode:
			node.damage(damage_amount )

I have done this, but I found that for me it makes sense to put health in a base character class for players and enemies, and then do all the health stuff in the setter of the variable. If it needs to be more complex for the player for example, you just override the base class.

@onready var health = max_health:
	set(value):
		health = value
		if health < max_health * 0.166:
			low_health_audio_stream_player.set_stream(really_low_health_sound)
			low_health_audio_stream_player.play()
		elif health < max_health * 0.33:
			if not (low_health_audio_stream_player.stream == low_health_sound and low_health_audio_stream_player.playing == true):
				low_health_audio_stream_player.set_stream(low_health_sound)
				low_health_audio_stream_player.play()
		elif health <= 0:
			low_health_audio_stream_player.stop()
		else:
			low_health_audio_stream_player.stop()

Yes, you can do this. But here’s why I don’t personally: Big-O notation. Collision detection is going to run regardless. This is 1. Once it runs, it’s going to trigger the body_entered signal, but we are still at 1. For every conditional statement in the function that gets run, we go up by 1. So it doesn’t matter what check you do - you’re now at least a 2. That means that every collision now take twice as many cycles.

I put in some example code to show how you can do checks if you really want - but going back to my point being that masks and layers used appropriately are composition in my opinion, then you don’t need to check. Because if you put something on layer 4, and it doesn’t have a damage() function, you will get an error the first time you try to break that vase which says “There’s no damage function() on vase1.” You want it to fail right then and there, and tell you that you built your object incorrectly.

So from a performance perspective, I think groups aren’t a great idea here. If you’re only making one game, and it’s not a bullet hell game, you probably won’t encounter this issue. I personally do it because I feel it’s the most performative, and I adopt agile methodologies whenever I can. I want my code to fail fast.

Yes, it would be. It is a valid approach.

However, I think you’re trying to future-proof your code against some undefined future scenario. My suggestion is an attempt to future-proof your code against your code lagging or failing on a player’s system that isn’t as performative as your development box. Something I just had happen in my last game jam. (Though the reason was the amount of unoptimized visual resources I was throwing at them.) But, I may be worrying about something that isn’t really an issue, and doesn’t need to be optimized.

In the end, I think it’s more of a philosophical discussion than anything. It’s your code. Groups will work, and if that works for you and your design practices, go for it.

1 Like

I did read about the update and I’m also fairly excited about that - if not just to organize your code better with clear class “roles”.
But as far as I’m aware implementation does not exist in Godot (at least yet) and you are also not able to do multiple class inheritance, which means that if you want an abstract class A to be implemented by a class B you’ll be forced to extend from it. Which wouldn’t be a problem on its own, unless you need class B to extend from a class C which does not have any business with class A - I hope my point makes sense.

This is my approach at the moment. Now, I’m not a crazy fan of having to cycle through all the child nodes, so I usually expose the nodes I want to be interacted with through my collision class/node and access them “directly”.

This I really resonate with. Unfortunately I try to over-engineer something just for the sake of having to do less work in the future :sweat_smile:.

Anyhow, I really appreciate the effort in your explanation. I will definitely try the layer approach. I did not know that Area3D was less compute-intensive.

And lastly, perhaps I need to cover some basic courses again haha, but I don’t see the difference between the group approach and the layer approach. As you mentioned, you are still triggering body_entered, which brings us to 1. And then in the _on_hit call you still have to run at least one condition to check if the hit has a damage method. So that should be the exact same, right?

Yes it does. It already did. That’s what you do when you create a _ready() or _input() or _physics_process() function. Before you could only declare something abstract in C code. Now you can do it in GDScript.

Well no, but there’s lot of languages you can’t do multiple class inheritance in. And there are good reasons for that. Don’t get me wrong, I have enjoyed multiple class inheritance in the past. And multiple class inheritance in languages like C++ and python is different than multiple interface inheritance in languages C# and Java.

But again, we get into the fact that Godot does composition well. If you rely on the Single Responsibility Principle, you’ll find that most of the time there’s another way to build the thing you want out of multiple parts. In Godot, nodes with smaller areas of responsibility, such as the HealthNode example I used earlier, allow you to use composition to achieve the same results that multiple inheritance would.

It’s also been my professional experience in over two decades of development that developers tend to not use multiple inheritance well. It’s been my experience that when I personally used multiple inheritance, composition would have been a better solution in the long run, and multiple inheritance tends to confuse me when I go back to debug things.

TBH, this just seems like breaking encapsulation to me. Which I am not a fan of when I don’t have to do it.

Same. And it’s bit me in the behind a few times in the last month.

No problem. I’m not guaranteeing that’s true, but from what I’ve read it is.

No.

Because going back to what I said earlier, this is how my code looks in practice:

@export var damage_amount = 5.0

@onready var weapon_area_3d: Area3D = $WeaponArea3D

func _ready() -> void:
	weapon_area_3d.body_entered(_on_hit)

# I know that this will only be called when I hit something that has a damage() function.
func _on_hit(body: Node3D) -> void:
	body.damage(damage_amount )

I do not do any checks. Because I want this function to fail. It should never be called on anything that doesn’t have a damage function. If it does, I want an error to halt my game immediately because it makes the bug hunting easy. (Especially since we have no throw/try/catch error handling.) If I get an error then I either added something to the wrong layer, or I forgot to add a damage function to something that is supposed to be on that layer. Either way, I want to know and I do not need to be doing any kind of type checking before I run that code. It’s a feature, not a bug. :slight_smile:

1 Like

Just a last off-topic question then, if you can implement interfaces how do you go about that. Because I’ve never seen or read anything like that for what I know.

I’ll also mark this topic as solved as I think your first answer gives a very nice solution to class matching, whilst expanding it with some better design.

1 Like

I haven’t actually done that yet in GDScript. I’m in the middle of a game jam so I don’t want to switch versions until I’m done with that. Though I’m really excited for a number of the 4.5 features.

Having said that, it really depends on what you’re trying to accomplish. Making a function abstract means that the inheriting class must implement it. So I’ve found that abstract classes are best used in places where you literally need an interface to another class. In Godot, I’ve gotten used to using signals as my interfaces. If I want to send information between two classes that shouldn’t know about one another, I use a clearinghouse of some sort.

For example, in my games I have a music player in the Pause menu. Everytime the player encounters a new song, it gets added to the list. All music gets played through a Music autoload. Music has a signal called add_song_to_playlist. Anytime a song is played, Music fires this signal. My music player picks it up, sees if it has the song, and if not, adds it to the list.

The various levels, cutscenes, etc that play the songs just call Music.play(). My interface in this case is my autoload, Music.

Th only place I’ve wanted and abstract keyword so far is my StateMachine and State classes. However, as I started implementing them, I realized that I didn’t want them to have abstract functions for activate, enter, exit and deactivate. Because for one, I wanted them all to have logging at the base level - and you can’t put code in an abstract class. And for two, I don’t want to implement methods I don’t need in an inherited class. Often I only need to override the _enter_state() method.

Having said that, if you want to implement an abstract class it would look just like the documentation says right now in GDScript 4.5 beta 1:

item.gd

abstract class Item:
	abstract func get_name() -> String

	func use() -> void:
		print("Character used %s." % get_name())

healing_potion.gd

class HealingPotion extends Item:
	func get_name() -> String:
		return "Healing Potion"

something_else.gd

func _ready() -> void:
	var potion := HealingPotion.new()
	potion.use() # Prints `Character used Healing Potion.`

	var item := Item.new() # Parser error!

And in the future would look like this:

item.gd

@abstract class Item:
	@abstract func get_name() -> String

	func use() -> void:
		print("Character used %s." % get_name())

In general (all languages of which I am aware), if a class is abstract, all of its methods have to be abstract. I do not know in GDScript if you can create a concrete class with some abstract methods. That is possible in some languages.

Abstract classes and interfaces are 2 completely separate topics and unfortunately one doesn’t resolve the other. Discussion about interfaces in GDScript can be followed in this proposal:

Mimicking behavior of interfaces in GDScript can be achieved with duck typing, which is described here, although it’s far from perfect as there’s currently no way of explicitly defining the contract rules, which is essential part of interface logic.

2 Likes

@wchc Interesting reads thanks. I appreciate the clarification. The TLDR seems to be that the team would implement traits before interfaces, and they aren’t implementing traits right now.

I also found this plugin someone made recently to implement interfaces. The creator acknowledges it’s not necessarily performant: GitHub - Rito13/GDScript-Interfaces-addon: Godot addon which implements interfaces for GDScript in it self.

1 Like