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?
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)
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
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()
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)