How to check if a node has "components" faster without iterating childs

Godot Version

4.5

The common composition pattern is making components like “HealthComponent” and adding that as a child of whatever you want to have health. Right now my bullets are checking every single child of everything it hits. Is there a better way?

If your component nodes are named consistently you could use hit.get_node("HealthComponent") instead

2 Likes

Heres my solution, each component type keeps a hash map so checking if a node has a component of a certain type is a single lookup now.

There is a better solution below this answer.

to get a players health component,

HealthComponent.of(playerNode)
@abstract
class_name ComponentBase
extends Node

# For each subclass/inhereting script, 
# there will be an dictionary in _registries,
# that will be updated with components of that specific type, as the values,
# and their parents as the keys.
static var _registries := {}

# this way, if you want to know if a node has a component as a child,
# you can just do (ex: HealthComponent.of(node))  and it will be able to find it
# quickly in this registry

# IMPORTANT!!!! each inhereting class, must add its own "of" func
# ex (YOU WOULD PUT THIS IN "HealthComponent.gd"): 
#------static func of(node: Node) -> HealthComponent:
#	-----return ComponentBase._of(node, HealthComponent)

static func _registry_for(script: Script) -> Dictionary:
	if not _registries.has(script):
		_registries[script] = {}
	return _registries[script]
	
# Shared internal "of" used by each subclasses
static func _of(node: Node, script: Script) -> ComponentBase:
	var reg := _registry_for(script)
	var list:Array = reg.get(node, [])
	if list.size() > 0:
		return list[0]
	return null

func _notification(what):
	match what:
		NOTIFICATION_PARENTED:
			_register()

		NOTIFICATION_UNPARENTED:
			_unregister()


func _register():
	var p := get_parent()
	if p == null:
		return

	var script:Script = self.get_script()
	var registry := _registry_for(script)

	if not registry.has(p):
		registry[p] = []

	if not self in registry[p]:
		registry[p].append(self)


func _unregister():
	var p := get_parent()
	if p == null:
		return

	var script:Script = self.get_script()
	var registry := _registry_for(script)

	if registry.has(p):
		registry[p].erase(self)
		if registry[p].is_empty():
			registry.erase(p)


Are you handling damage to an object through a method? You can check to see if a method exists inside the object you interact with.

area.body_entered.connect(damage_body)

func damage_body(body: Node2D) -> void:
	
	if body.has_method("take_damage"):
		body.take_damage(value)
		
1 Like

Could not one solution be to add a bool like has_health_component and if true also a reference to the health component.
Then first check is bool true or false, then either perform no other checks or if true check the reference to the component.
Mostly wondering out of curiosity if there are any obvious drawbacks

Edit:

I guess you can skip the bool entirely. Like if node.health_component != Null, check node.health_component

1 Like

Does it though?
Base class static vars are shared among downstream classes.

2 Likes

yeah your right, i found out a way to make it work. thanks for pointing that out. I updated the code above.

How about components just registering themselves into parent’s metadata:

Component base:

class_name Component extends Node

func _ready() -> void:
	var type_name = get_script().get_global_name()
	get_parent().set_meta(type_name, self)
	
static func get_or_null(node: Node, script: Script) -> Node:
	var type_name = script.get_global_name()
	return node.get_meta(type_name) if node.has_meta(type_name) else null

Components:

class_name ComponentA extends Component

func _ready():
	super()
class_name ComponentB extends Component

func _ready():
	super()

Usage:

Component.get_or_null(entity, ComponentA)
Component.get_or_null(entity, ComponentB)

The usage looks a bit less elegant than what you have above but it’s still a simple one liner. Implementation is much much simpler though. It doesn’t maintain any global data, and doesn’t place any requirements on derived component implementations, except to call parent class _ready()

1 Like

This is a good solution, honestly more simple so imma just use this as the answer. Here i implemented it fully. No need to call ready. BUT you can override “of” in the subclass to keep that elegant looking way of calling it.

ComponentBase.gd


class_name ComponentBase
extends Node


func _register_self_in_parents_metadata():
	var parent := get_parent()
	if parent == null:
		return
	var type_name = get_script().get_global_name()
	# Ensure list exists
	if not parent.has_meta(type_name):
		parent.set_meta(type_name, [self])
		
	# Add this instance
	if self not in parent.get_meta(type_name):
		parent.get_meta(type_name).append(self)

func _notification(what):
	match what:
		NOTIFICATION_PARENTED:
			_register_self_in_parents_metadata()

		NOTIFICATION_UNPARENTED:
			_unregister_self_in_parents_metadata()

func _unregister_self_in_parents_metadata() -> void:
	var parent := get_parent()
	if parent == null:
		return

	var type_name = get_script().get_global_name()

	if parent.has_meta(type_name):
		var arr: Array = parent.get_meta(type_name)
		arr.erase(self)

		if arr.is_empty():
			parent.remove_meta(type_name)


# Returns the FIRST component of given type on node
# Usage: var players_health_component = ComponentBase._of(player,HealthComponent)
# OR u can define "of" in the subclass and do something like this
# var players_health_component = HealthComponent.of(player)
static func _of(node: Node, component_script: Script) -> ComponentBase:
	if node == null or component_script == null:
		return null
		
	var type_name := component_script.get_global_name()

	if node.has_meta(type_name):
		var arr: Array = node.get_meta(type_name)
		return arr[0] if arr.size() > 0 else null

	return null

#This is how subclasses could define "of" to allow this kinda code:
# var players_health_component = HealthComponent.of(player)
static func of(node: Node) -> ComponentBase:
	return ComponentBase._of(node, ComponentBase)
	

Example HealthComponent.gd

class_name HealthComponent
extends ComponentBase

var health = 100

#optional override so you can do HealthComponent.of(player),
#instead of ComponentBase._of(player, HealthComponent)
static func of(node: Node) -> HealthComponent:
	return ComponentBase._of(node, HealthComponent)
	

to get a players health component,

HealthComponent.of(playerNode)