basic ECS spawner

This commit is contained in:
2026-01-15 15:27:48 +01:00
parent 24a781f36a
commit eb737b469c
860 changed files with 58621 additions and 32 deletions

View File

@@ -0,0 +1,128 @@
class_name GECSEditorDebugger
extends EditorDebuggerPlugin
## The Debugger session for the current game
var session: EditorDebuggerSession
## The tab that will be added to the debugger window
var debugger_tab: GECSEditorDebuggerTab = preload("res://addons/gecs/debug/gecs_editor_debugger_tab.tscn").instantiate()
## The debugger messages that will be sent to the editor debugger
var Msg := GECSEditorDebuggerMessages.Msg
## Reference to editor interface for selecting nodes
var editor_interface: EditorInterface = null
func _has_capture(capture):
# Return true if you wish to handle messages with the prefix "gecs:".
return capture == "gecs"
func _capture(message: String, data: Array, session_id: int) -> bool:
if message == Msg.WORLD_INIT:
# data: [World.get_path()]
var world = data[0]
var world_path = data[1]
debugger_tab.world_init(data[0], data[1])
return true
elif message == Msg.SYSTEM_METRIC:
# data: [system, system_name, elapsed_time]
var system = data[0]
var system_name = data[1]
var elapsed_time = data[2]
debugger_tab.system_metric(system, system_name, elapsed_time)
return true
elif message == Msg.SYSTEM_LAST_RUN_DATA:
# data: [system_id, system_name, last_run_data]
var system_id = data[0]
var system_name = data[1]
var last_run_data = data[2]
debugger_tab.system_last_run_data(system_id, system_name, last_run_data)
return true
elif message == Msg.SET_WORLD:
if data.size() == 0:
return true
var world = data[0]
var world_path = data[1]
debugger_tab.set_world(world, world_path)
return true
elif message == Msg.PROCESS_WORLD:
# data: [float, String]
var delta = data[0]
var group_name = data[1]
debugger_tab.process_world(delta, group_name)
return true
elif message == Msg.EXIT_WORLD:
debugger_tab.exit_world()
return true
elif message == Msg.ENTITY_ADDED:
# data: [Entity, NodePath]
debugger_tab.entity_added(data[0], data[1])
return true
elif message == Msg.ENTITY_REMOVED:
# data: [Entity, NodePath]
debugger_tab.entity_removed(data[0], data[1])
return true
elif message == Msg.ENTITY_DISABLED:
# data: [Entity, NodePath]
debugger_tab.entity_disabled(data[0], data[1])
return true
elif message == Msg.ENTITY_ENABLED:
# data: [Entity, NodePath]
debugger_tab.entity_enabled(data[0], data[1])
return true
elif message == Msg.SYSTEM_ADDED:
# data: [System, group, process_empty, active, paused, NodePath]
debugger_tab.system_added(data[0], data[1], data[2], data[3], data[4], data[5])
return true
elif message == Msg.SYSTEM_REMOVED:
# data: [System, NodePath]
debugger_tab.system_removed(data[0], data[1])
return true
elif message == Msg.ENTITY_COMPONENT_ADDED:
# data: [ent.get_instance_id(), comp.get_instance_id(), ClassUtils.get_type_name(comp), comp.serialize()]
debugger_tab.entity_component_added(data[0], data[1], data[2], data[3])
return true
elif message == Msg.ENTITY_COMPONENT_REMOVED:
# data: [Entity, Variant]
debugger_tab.entity_component_removed(data[0], data[1])
return true
elif message == Msg.ENTITY_RELATIONSHIP_ADDED:
# data: [ent_id, rel_id, rel_data]
debugger_tab.entity_relationship_added(data[0], data[1], data[2])
return true
elif message == Msg.ENTITY_RELATIONSHIP_REMOVED:
# data: [Entity, Relationship]
debugger_tab.entity_relationship_removed(data[0], data[1])
return true
elif message == Msg.COMPONENT_PROPERTY_CHANGED:
# data: [Entity, Component, property_name, old_value, new_value]
debugger_tab.entity_component_property_changed(data[0], data[1], data[2], data[3], data[4])
return true
return false
func _setup_session(session_id):
# Add a new tab in the debugger session UI containing a label.
debugger_tab.name = "GECS" # Will be used as the tab title.
session = get_session(session_id)
# Pass session reference to the tab for sending messages
debugger_tab.set_debugger_session(session)
# Pass editor interface to the tab for selecting nodes
debugger_tab.set_editor_interface(editor_interface)
# Listens to the session started and stopped signals.
if not session.started.is_connected(_on_session_started):
session.started.connect(_on_session_started)
if not session.stopped.is_connected(_on_session_stopped):
session.stopped.connect(_on_session_stopped)
session.add_session_tab(debugger_tab)
func _on_session_started():
print("GECS Debug Session started")
debugger_tab.clear_all_data()
debugger_tab.active = true
func _on_session_stopped():
print("GECS Debug Session stopped")
debugger_tab.active = false

View File

@@ -0,0 +1 @@
uid://fndnnk201xlo

View File

@@ -0,0 +1,227 @@
class_name GECSEditorDebuggerMessages
## A mapping of all the messages sent to the editor debugger.
const Msg = {
"WORLD_INIT": "gecs:world_init",
"SYSTEM_METRIC": "gecs:system_metric",
"SYSTEM_LAST_RUN_DATA": "gecs:system_last_run_data",
"SET_WORLD": "gecs:set_world",
"PROCESS_WORLD": "gecs:process_world",
"EXIT_WORLD": "gecs:exit_world",
"ENTITY_ADDED": "gecs:entity_added",
"ENTITY_REMOVED": "gecs:entity_removed",
"ENTITY_DISABLED": "gecs:entity_disabled",
"ENTITY_ENABLED": "gecs:entity_enabled",
"SYSTEM_ADDED": "gecs:system_added",
"SYSTEM_REMOVED": "gecs:system_removed",
"ENTITY_COMPONENT_ADDED": "gecs:entity_component_added",
"ENTITY_COMPONENT_REMOVED": "gecs:entity_component_removed",
"ENTITY_RELATIONSHIP_ADDED": "gecs:entity_relationship_added",
"ENTITY_RELATIONSHIP_REMOVED": "gecs:entity_relationship_removed",
"COMPONENT_PROPERTY_CHANGED": "gecs:component_property_changed",
"POLL_ENTITY": "gecs:poll_entity",
"SELECT_ENTITY": "gecs:select_entity",
}
## Helper function to check if we can send messages to the editor debugger.
static func can_send_message() -> bool:
return not Engine.is_editor_hint() and OS.has_feature("editor")
static func world_init(world: World) -> bool:
if can_send_message():
EngineDebugger.send_message(Msg.WORLD_INIT, [world.get_instance_id(),
world.get_path()
])
return true
static func system_metric(system: System, time: float) -> bool:
if can_send_message():
EngineDebugger.send_message(
Msg.SYSTEM_METRIC, [system.get_instance_id(),
system.name,
time
]
)
return true
static func system_last_run_data(system: System, last_run_data: Dictionary) -> bool:
if can_send_message():
# Send trimmed data to avoid excessive payload; include execution time and entity count primarily
EngineDebugger.send_message(
Msg.SYSTEM_LAST_RUN_DATA,
[
system.get_instance_id(),
system.name,
last_run_data.duplicate() # duplicate so caller's dictionary isn't mutated
]
)
return true
static func set_world(world: World) -> bool:
if can_send_message():
EngineDebugger.send_message(
Msg.SET_WORLD,
[world.get_instance_id(),
world.get_path()
]
if world else []
)
return true
static func process_world(delta: float, group_name: String) -> bool:
if can_send_message():
EngineDebugger.send_message(Msg.PROCESS_WORLD, [delta, group_name])
return true
static func exit_world() -> bool:
if can_send_message():
EngineDebugger.send_message(Msg.EXIT_WORLD, [])
return true
static func entity_added(ent: Entity) -> bool:
if can_send_message():
EngineDebugger.send_message(Msg.ENTITY_ADDED, [ent.get_instance_id(), ent.get_path()])
return true
static func entity_removed(ent: Entity) -> bool:
if can_send_message():
EngineDebugger.send_message(Msg.ENTITY_REMOVED, [ent.get_instance_id(), ent.get_path()])
return true
static func entity_disabled(ent: Entity) -> bool:
if can_send_message():
EngineDebugger.send_message(Msg.ENTITY_DISABLED, [ent.get_instance_id(), ent.get_path()])
return true
static func entity_enabled(ent: Entity) -> bool:
if can_send_message():
EngineDebugger.send_message(Msg.ENTITY_ENABLED, [ent.get_instance_id(), ent.get_path()])
return true
static func system_added(sys: System) -> bool:
if can_send_message():
EngineDebugger.send_message(
Msg.SYSTEM_ADDED,
[
sys.get_instance_id(),
sys.group,
sys.process_empty,
sys.active,
sys.paused,
sys.get_path()
]
)
return true
static func system_removed(sys: System) -> bool:
if can_send_message():
EngineDebugger.send_message(Msg.SYSTEM_REMOVED, [sys.get_instance_id(), sys.get_path()])
return true
static func _get_type_name_for_debugger(obj) -> String:
if obj == null:
return "null"
if obj is Resource or obj is Node:
var script = obj.get_script()
if script:
# Try to get class_name first
var type_name = script.get_class()
if type_name and type_name != "GDScript":
return type_name
# Otherwise use the resource path (e.g., "res://components/C_Health.gd")
if script.resource_path:
return script.resource_path # Returns "C_Health"
return obj.get_class()
elif obj is Object:
return obj.get_class()
return str(typeof(obj))
static func entity_component_added(ent: Entity, comp: Resource) -> bool:
if can_send_message():
EngineDebugger.send_message(
Msg.ENTITY_COMPONENT_ADDED,
[
ent.get_instance_id(),
comp.get_instance_id(),
_get_type_name_for_debugger(comp),
comp.serialize()
]
)
return true
static func entity_component_removed(ent: Entity, comp: Resource) -> bool:
if can_send_message():
EngineDebugger.send_message(
Msg.ENTITY_COMPONENT_REMOVED, [ent.get_instance_id(),
comp.get_instance_id()
]
)
return true
static func entity_component_property_changed(
ent: Entity, comp: Resource, property_name: String, old_value: Variant, new_value: Variant
) -> bool:
if can_send_message():
EngineDebugger.send_message(
Msg.COMPONENT_PROPERTY_CHANGED,
[ent.get_instance_id(),
comp.get_instance_id(),
property_name,
old_value,
new_value
]
)
return true
static func entity_relationship_added(ent: Entity, rel: Relationship) -> bool:
if can_send_message():
# Serialize relationship data for debugger display
var rel_data = {
"relation_type": _get_type_name_for_debugger(rel.relation) if rel.relation else "null",
"relation_data": rel.relation.serialize() if rel.relation else {},
"target_type": "",
"target_data": {}
}
# Format target based on type
if rel.target == null:
rel_data["target_type"] = "null"
elif rel.target is Entity:
rel_data["target_type"] = "Entity"
rel_data["target_data"] = {
"id": rel.target.get_instance_id(),
"path": str(rel.target.get_path())
}
elif rel.target is Component:
rel_data["target_type"] = "Component"
rel_data["target_data"] = {
"type": _get_type_name_for_debugger(rel.target),
"data": rel.target.serialize()
}
elif rel.target is Script:
rel_data["target_type"] = "Archetype"
rel_data["target_data"] = {
"script_path": rel.target.resource_path
}
EngineDebugger.send_message(
Msg.ENTITY_RELATIONSHIP_ADDED,
[ent.get_instance_id(),
rel.get_instance_id(),
rel_data
]
)
return true
static func entity_relationship_removed(ent: Entity, rel: Relationship) -> bool:
if can_send_message():
EngineDebugger.send_message(
Msg.ENTITY_RELATIONSHIP_REMOVED, [ent.get_instance_id(),
rel.get_instance_id()
]
)
return true

View File

@@ -0,0 +1 @@
uid://d08dvfk13egq7

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
uid://ca7erogu58fca

View File

@@ -0,0 +1,169 @@
[gd_scene load_steps=2 format=3 uid="uid://cbykprebt3jaa"]
[ext_resource type="Script" uid="uid://ca7erogu58fca" path="res://addons/gecs/debug/gecs_editor_debugger_tab.gd" id="1_8dl00"]
[node name="GECSEditorDebuggerTab" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
size_flags_horizontal = 3
size_flags_vertical = 3
script = ExtResource("1_8dl00")
[node name="DebugModeOverlay" type="Panel" parent="."]
unique_name_in_owner = true
visible = false
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="CenterContainer" type="CenterContainer" parent="DebugModeOverlay"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="VBoxContainer" type="VBoxContainer" parent="DebugModeOverlay/CenterContainer"]
layout_mode = 2
[node name="Label" type="Label" parent="DebugModeOverlay/CenterContainer/VBoxContainer"]
layout_mode = 2
theme_type_variation = &"HeaderLarge"
text = "Debug Mode Disabled"
horizontal_alignment = 1
[node name="Message" type="Label" parent="DebugModeOverlay/CenterContainer/VBoxContainer"]
layout_mode = 2
text = "Enable Debug Mode in Project Settings to show Debug Data"
horizontal_alignment = 1
[node name="HSpacer" type="Control" parent="DebugModeOverlay/CenterContainer/VBoxContainer"]
custom_minimum_size = Vector2(0, 10)
layout_mode = 2
[node name="Instructions" type="Label" parent="DebugModeOverlay/CenterContainer/VBoxContainer"]
layout_mode = 2
text = "Project Settings > General > GECS > Settings > Debug Mode"
horizontal_alignment = 1
[node name="HSplit" type="HSplitContainer" parent="."]
process_mode = 3
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="HSplitContainer" type="HSplitContainer" parent="HSplit"]
layout_mode = 2
size_flags_horizontal = 3
[node name="EntitiesVBox" type="VBoxContainer" parent="HSplit/HSplitContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="HBoxContainer" type="HBoxContainer" parent="HSplit/HSplitContainer/EntitiesVBox"]
layout_mode = 2
[node name="EntitiesQueryLineEdit" type="LineEdit" parent="HSplit/HSplitContainer/EntitiesVBox/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
placeholder_text = "Entities filter....."
[node name="CollapseAllBtn" type="Button" parent="HSplit/HSplitContainer/EntitiesVBox/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "Collapse All"
[node name="ExpandAllBtn" type="Button" parent="HSplit/HSplitContainer/EntitiesVBox/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "Expand All"
[node name="QueryBuilderCheckBox" type="CheckBox" parent="HSplit/HSplitContainer/EntitiesVBox/HBoxContainer"]
unique_name_in_owner = true
visible = false
layout_mode = 2
text = "QueryBuilder"
[node name="EntitiesTree" type="Tree" parent="HSplit/HSplitContainer/EntitiesVBox"]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
columns = 4
column_titles_visible = true
allow_rmb_select = true
hide_root = true
[node name="HBoxContainerEntitiesStatus" type="HBoxContainer" parent="HSplit/HSplitContainer/EntitiesVBox"]
custom_minimum_size = Vector2(0, 33)
layout_mode = 2
[node name="EntityStatusBar" type="TextEdit" parent="HSplit/HSplitContainer/EntitiesVBox/HBoxContainerEntitiesStatus"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
text = "Entities: 0 | Components: 0 | Relationships: 0"
editable = false
[node name="SystemsVBox" type="VBoxContainer" parent="HSplit/HSplitContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="HBoxContainer" type="HBoxContainer" parent="HSplit/HSplitContainer/SystemsVBox"]
layout_mode = 2
[node name="SystemsQueryLineEdit" type="LineEdit" parent="HSplit/HSplitContainer/SystemsVBox/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
placeholder_text = "Systems filter...."
[node name="SystemsCollapseAllBtn" type="Button" parent="HSplit/HSplitContainer/SystemsVBox/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "Collapse All"
[node name="SystemsExpandAllBtn" type="Button" parent="HSplit/HSplitContainer/SystemsVBox/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "Expand All"
[node name="PopOutBtn" type="Button" parent="HSplit/HSplitContainer/SystemsVBox/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "Pop Out"
[node name="SystemsTree" type="Tree" parent="HSplit/HSplitContainer/SystemsVBox"]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
columns = 5
column_titles_visible = true
hide_root = true
select_mode = 2
[node name="HBoxContainerSystemsStatus" type="HBoxContainer" parent="HSplit/HSplitContainer/SystemsVBox"]
custom_minimum_size = Vector2(0, 33)
layout_mode = 2
[node name="SystemsStatusBar" type="TextEdit" parent="HSplit/HSplitContainer/SystemsVBox/HBoxContainerSystemsStatus"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
text = "Systems: 0 | Total ms: 0.0ms | Most Expensive: (0.0ms)"
editable = false