I am trying to create a utility-based AI system in a Godot project (reference). Basically, it funnels game data into considerations which drive actions. In order to do this, I need to store relevant game data in Godot.
For example, suppose I have a LineOfSightComponent
which has target
and ray_to_target
within it. Whether an NPC has line of sight on a target or not determines whether they can Chase the target. If they decide to Chase, the NPC also needs this information to move towards the target (say, the Player). This can apply to lots of different situations (e.g. a HealthRemaining
consideration needing hp
and MAX_HP
game data from NPC, a Threat
consideration needing data from the Player, etc.).
With that in mind, what is the best way to store this data? I’ve been reading about Resources in Godot, but I’m not sure what advantages it has over my current setup. I made a Dictionary-based Database that is stored in the “Game” scene and collects data on anything that extends a “Data” class I made.
Here is the Database class:
# database.gd
extends Node
class_name Database
var game_state: Dictionary
func _ready():
traverse_tree(get_tree().root)
## Add all data to game_state. Connect data signals to Database
func traverse_tree(node):
# Add CharacterBody2D nodes to Database
if node is CharacterBody2D:
# Ensure the node dictionary exists in game_state
if not game_state.has(node.name.to_lower()):
game_state[node.name.to_lower()] = {}
# Add CharacterBody2D node
game_state[node.name.to_lower()]["self"] = node
for child in node.get_children():
# Add Data node to Database
if child is Data:
# Ensure the node dictionary exists in game_state
if not game_state.has(node.name.to_lower()):
game_state[node.name.to_lower()] = {}
# Add data
game_state[node.name.to_lower()][child.name.to_lower()] = child.get_data()
#print("game state: ", game_state) # Debug
# Connect data_changed signal in Data to _on_data_changed() method in Database
child.connect("data_changed", Callable(self, "_on_data_changed"))
# Allow Considerations to request Database data
elif child is Consideration:
# Connect data_request signal in Consideration to _on_request_data() method in Database
child.connect("data_request", Callable(self, "_on_request_data"))
# Allow any child with request_data() method and data_request signal,
# i.e. with proper infrastructure, to request Database data
else:
# Check if child has request_data() method
if child.has_method("request_data"):
var properties = child.get_property_list()
# Check if child has data_request signal
for property in properties:
if property.name == "data_request":
print("Connecting %s to Database..." % [child.name.to_lower()])
child.connect("data_request", Callable(self, "_on_request_data"))
break
traverse_tree(child) # Recursively check all sub-trees
## Update game_state when data changes
func _on_data_changed(node_name, child_name, data):
game_state[node_name][child_name] = data
func _on_request_data(node_key, child_key, requester):
# Check game_state has requested data
if node_key in game_state:
if child_key in game_state[node_key]:
if requester.has_method("receive_data"): # Check requester can receive data
requester.receive_data(node_key, child_key, game_state[node_key][child_key]) # Send data
Here is the Data class:
# data.gd
extends Node
class_name Data
signal data_changed(node_name, new_data)
func get_data() -> Dictionary:
return {} # Returns data formatted as a Dictionary
func update_data():
data_changed.emit(self.get_parent().name.to_lower(), self.name.to_lower(), get_data())
Here is an example extension of the Data class:
extends Data
# Line of Sight Component
class_name LoSComponent
var target: CharacterBody2D = null
var ray_to_target: RayCast2D = null
signal target_found(entity: CharacterBody2D, ray: RayCast2D)
func get_data():
return {
"target": target,
"ray_to_target": ray_to_target
}
# ... (implementation goes here) ... #
And here is an example of calling this data for use in a consideration:
extends Consideration
func _ready():
self.parent_keys = ["npc"]
self.data_keys = ["loscomponent"]
## Return 1.0 if there is line of sight, else 0.0
func get_derived_value() -> float:
if data.has("npc"):
if data["npc"]["loscomponent"]["target"] != null:
return 1.0
return 0.0
Would using Resources be better than this system? As the code scales in complexity, I would like something that doesn’t rely on a strict file structure; if I move a scene or node somewhere, I wouldn’t like want ten other files to break. Resources seem to require the load()
method, which requires a file path. On the other hand, I can see some potential downsides to nested Dictionaries. For one thing, I really only need to read the data; I don’t want to create copies, and any data change should be sent out as a signal.
All this has left me a bit stuck on what’s best for this case. Any advice?
Michael Peters is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.