Skip to content

Commit 34cbfa4

Browse files
cspranceclaude
andcommitted
Bump to v7.2.0 — int-keyed components, system hot path optimizations, safe_iteration
Switch component dictionary keys from resource_path strings to Script.get_instance_id() ints for faster lookups on every hot path. - Entity: components dict keyed by int, new _comp_key() helper - Archetype: columns/edges keyed by int (components) or String (rel://) - System: FlushMode enum cached once in _internal_setup(), subsystem and non-structural filter results cached, lazy CommandBuffer init - QueryBuilder: cache_version counter detects stale results without signal reliance - New safe_iteration export on System — skip snapshot copy when all structural changes go through CommandBuffer - has_relationship() fast path avoids get_relationship() overhead - Network addon iterates components.values() instead of .keys() - Test and example updates for new keying scheme Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 3ae3a2e commit 34cbfa4

25 files changed

Lines changed: 311 additions & 215 deletions

addons/gecs/docs/TROUBLESHOOTING.md

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -140,8 +140,8 @@ func query():
140140
# Debug what components an entity actually has
141141
func debug_entity_components(entity: Entity):
142142
print("Entity components:")
143-
for component_path in entity.components.keys():
144-
print(" ", component_path)
143+
for component in entity.components.values():
144+
print(" ", component.get_script().resource_path)
145145
```
146146

147147
**Solution**: Ensure components are added correctly:
@@ -382,13 +382,12 @@ func _on_inspect_button_pressed():
382382
for i in range(min(10, entities.size())): # Show first 10
383383
var entity = entities[i]
384384
print("Entity ", i, ":")
385-
print(" Components: ", entity.components.keys())
385+
print(" Components: ", entity.components.values().map(func(c): return c.get_script().resource_path))
386386
print(" Groups: ", entity.get_groups())
387387
388388
# Show component values
389-
for comp_path in entity.components.keys():
390-
var comp = entity.components[comp_path]
391-
print(" ", comp_path, ": ", comp)
389+
for comp in entity.components.values():
390+
print(" ", comp.get_script().resource_path, ": ", comp)
392391
```
393392

394393
## Getting More Help

addons/gecs/ecs/archetype.gd

Lines changed: 40 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ extends RefCounted
3333
## Generated by QueryCacheKey.build() from sorted component types
3434
var signature: int = 0
3535

36-
## Sorted array of component resource paths (e.g., ["res://c_position.gd", "res://c_velocity.gd"])
36+
## Array of component/relationship keys (ints for components, Strings for rel:// keys)
3737
## Used for debugging and archetype matching logic
3838
var component_types: Array = []
3939

@@ -54,15 +54,14 @@ var entity_to_index: Dictionary = {} # Entity -> int
5454
var enabled_bitset: PackedInt64Array = []
5555

5656
## OPTIMIZATION: Structure of Arrays (SoA) column storage for cache-friendly iteration
57-
## Maps component_path -> Array of component instances
57+
## Maps script_instance_id (int) -> Array of component instances
5858
## Enables Flecs-style direct array iteration without dictionary lookups
59-
## Example: columns["res://c_velocity.gd"] = [vel1, vel2, vel3, ...]
60-
var columns: Dictionary = {} # String (component_path) -> Array of components
59+
var columns: Dictionary = {} # int (script_instance_id) -> Array of components
6160

62-
## Archetype edges for fast component add/remove (future optimization)
63-
## Maps: component_path -> Archetype (the archetype you get by adding/removing that component)
64-
var add_edges: Dictionary = {} # String -> Archetype
65-
var remove_edges: Dictionary = {} # String -> Archetype
61+
## Archetype edges for fast component add/remove
62+
## Maps: Variant (int for components, String for relationships) -> Archetype
63+
var add_edges: Dictionary = {} # Variant (int|String) -> Archetype
64+
var remove_edges: Dictionary = {} # Variant (int|String) -> Archetype
6665

6766
## Reverse-edge tracking: which archetypes hold an add_edge or remove_edge pointing to this one.
6867
## Keyed by source archetype instance_id for O(1) operations.
@@ -75,12 +74,12 @@ func _init(p_signature: int, p_component_types: Array):
7574
component_types = p_component_types.duplicate()
7675
component_types.sort() # Ensure sorted for consistent matching
7776

78-
# Separate relationship slot keys from component paths
77+
# Separate relationship slot keys (Strings) from component keys (ints)
7978
for comp_type in component_types:
80-
if (comp_type as String).begins_with("rel://"):
79+
if comp_type is String and comp_type.begins_with("rel://"):
8180
relationship_types.append(comp_type)
8281
else:
83-
# Only create columns for component paths, NOT relationship slots
82+
# Only create columns for component keys, NOT relationship slots
8483
columns[comp_type] = []
8584

8685

@@ -98,14 +97,14 @@ func add_entity(entity: Entity) -> void:
9897

9998
# OPTIMIZATION: Populate column arrays from entity.components
10099
# Iterate columns keys (skips rel:// keys which have no columns)
101-
for comp_path in columns:
102-
if entity.components.has(comp_path):
103-
(columns[comp_path]
104-
.append(entity.components[comp_path]))
100+
for comp_key in columns:
101+
if entity.components.has(comp_key):
102+
(columns[comp_key]
103+
.append(entity.components[comp_key]))
105104
else:
106105
# Entity doesn't have this component yet (might be mid-initialization)
107106
# Push null placeholder, will be fixed when component is added
108-
columns[comp_path].append(null)
107+
columns[comp_key].append(null)
109108

110109

111110
## Remove an entity from this archetype using swap-remove
@@ -125,8 +124,8 @@ func remove_entity(entity: Entity) -> bool:
125124
entity_to_index[last_entity] = index
126125

127126
# OPTIMIZATION: Swap in column arrays too (maintain same ordering)
128-
for comp_path in columns:
129-
columns[comp_path][index] = columns[comp_path][last_index]
127+
for comp_key in columns:
128+
columns[comp_key][index] = columns[comp_key][last_index]
130129

131130
# OPTIMIZATION: Swap enabled bit
132131
var last_enabled = _get_enabled_bit(last_index)
@@ -137,8 +136,8 @@ func remove_entity(entity: Entity) -> bool:
137136
entity_to_index.erase(entity)
138137

139138
# OPTIMIZATION: Remove last element from all columns
140-
for comp_path in columns:
141-
columns[comp_path].pop_back()
139+
for comp_key in columns:
140+
columns[comp_key].pop_back()
142141

143142
# OPTIMIZATION: Update bitset size (no need to clear the bit, just reduce logical size)
144143
# The bit will be overwritten when a new entity is added
@@ -167,8 +166,8 @@ func clear() -> void:
167166
entity_to_index.clear()
168167

169168
# OPTIMIZATION: Clear column arrays
170-
for comp_path in columns:
171-
columns[comp_path].clear()
169+
for comp_key in columns:
170+
columns[comp_key].clear()
172171

173172
# OPTIMIZATION: Clear bitset
174173
enabled_bitset.clear()
@@ -234,10 +233,13 @@ func matches_relationship_query(required_rel_keys: Array, excluded_rel_keys: Arr
234233
func _to_string() -> String:
235234
var comp_names = []
236235
for comp_type in component_types:
237-
# Extract just the class name from the path
238-
var parts = comp_type.split("/")
239-
var filename = parts[parts.size() - 1].replace(".gd", "")
240-
comp_names.append(filename)
236+
if comp_type is String:
237+
# Relationship slot key — extract readable name from path
238+
var parts = comp_type.split("/")
239+
var filename = parts[parts.size() - 1].replace(".gd", "")
240+
comp_names.append(filename)
241+
else:
242+
comp_names.append(str(comp_type))
241243

242244
return "Archetype[sig=%d, comps=%s, entities=%d]" % [
243245
signature,
@@ -248,43 +250,43 @@ func _to_string() -> String:
248250

249251
## Set up an edge to another archetype when a component is added
250252
## Enables O(1) archetype transitions when components change
251-
func set_add_edge(component_path: String, target_archetype: Archetype) -> void:
252-
add_edges[component_path] = target_archetype
253+
func set_add_edge(comp_key: Variant, target_archetype: Archetype) -> void:
254+
add_edges[comp_key] = target_archetype
253255
target_archetype.neighbors[get_instance_id()] = self
254256

255257

256258
## Set up an edge to another archetype when a component is removed
257259
## Enables O(1) archetype transitions when components change
258-
func set_remove_edge(component_path: String, target_archetype: Archetype) -> void:
259-
remove_edges[component_path] = target_archetype
260+
func set_remove_edge(comp_key: Variant, target_archetype: Archetype) -> void:
261+
remove_edges[comp_key] = target_archetype
260262
target_archetype.neighbors[get_instance_id()] = self
261263

262264

263265
## Get the target archetype when adding a component (if edge exists)
264-
func get_add_edge(component_path: String) -> Archetype:
265-
return add_edges.get(component_path, null)
266+
func get_add_edge(comp_key: Variant) -> Archetype:
267+
return add_edges.get(comp_key, null)
266268

267269

268270
## Get the target archetype when removing a component (if edge exists)
269-
func get_remove_edge(component_path: String) -> Archetype:
270-
return remove_edges.get(component_path, null)
271+
func get_remove_edge(comp_key: Variant) -> Archetype:
272+
return remove_edges.get(comp_key, null)
271273

272274

273275
## OPTIMIZATION: Get component column array for cache-friendly iteration
274276
## Enables Flecs-style direct array access instead of dictionary lookups per entity
275-
## [param component_path] The resource path of the component type (e.g., C_Velocity.resource_path)
277+
## [param comp_key] The component key (Script.get_instance_id()) or relationship slot key (String)
276278
## [returns] Array of component instances in entity index order, or empty array if not found
277279
##
278280
## Example:
279281
## [codeblock]
280-
## var velocities = archetype.get_column(C_Velocity.resource_path)
282+
## var velocities = archetype.get_column(C_Velocity.get_instance_id())
281283
## for i in range(velocities.size()):
282284
## var velocity = velocities[i]
283285
## var entity = archetype.entities[i]
284286
## # Process with cache-friendly sequential access
285287
## [/codeblock]
286-
func get_column(component_path: String) -> Array:
287-
return columns.get(component_path, [])
288+
func get_column(comp_key: Variant) -> Array:
289+
return columns.get(comp_key, [])
288290

289291

290292
## OPTIMIZATION: Get entities filtered by enabled state using bitset

addons/gecs/ecs/ecs.gd

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ func process(delta: float, group: String = "") -> void:
8989
func get_components(entities, component_type, default_component = null) -> Array:
9090
var components = []
9191
for entity in entities:
92-
var component = entity.components.get(component_type.resource_path, null)
92+
var component = entity.components.get(component_type.get_instance_id() if component_type is Script else component_type.get_script().get_instance_id(), null)
9393
if not component and not default_component:
9494
assert(component, "Entity does not have component: " + str(component_type))
9595
if not component and default_component:

addons/gecs/ecs/entity.gd

Lines changed: 46 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -67,14 +67,22 @@ signal relationships_batch_removed(entity: Entity, _relationships: Array)
6767
#endregion Exported Variables
6868

6969
#region Public Variables
70-
## [Component]s attached to the [Entity] in the form of Dict[resource_path:String, Component]
70+
## [Component]s attached to the [Entity] in the form of Dict[int (script_instance_id), Component]
7171
var components: Dictionary = {}
7272

7373
## Relationships attached to the entity
7474
var relationships: Array[Relationship] = []
7575

76-
## Cache for component resource paths to avoid repeated .get_script().resource_path calls
77-
var _component_path_cache: Dictionary = {}
76+
## Cache for component keys to avoid repeated .get_script().get_instance_id() calls
77+
var _component_key_cache: Dictionary = {}
78+
79+
80+
## Returns the int key used for component dictionary lookups.
81+
## Accepts either a Script (class reference) or a Component instance.
82+
static func _comp_key(c) -> int:
83+
if c is Script:
84+
return c.get_instance_id()
85+
return c.get_script().get_instance_id()
7886

7987
## Logger for entities to only log to a specific domain
8088
var _entityLogger = GECSLogger.new().domain("Entity")
@@ -162,22 +170,22 @@ func get_effective_serialize_config() -> GECSSerializeConfig:
162170
## [b]Example[/b]:
163171
## [codeblock]entity.add_component(HealthComponent)[/codeblock]
164172
func add_component(component: Resource) -> void:
165-
# Cache the resource path to avoid repeated calls
166-
var resource_path = component.get_script().resource_path
173+
# Cache the component key to avoid repeated calls
174+
var comp_key = _comp_key(component)
167175

168176
# If a component of this type already exists, remove it first
169-
if components.has(resource_path):
170-
var existing_component = components[resource_path]
177+
if components.has(comp_key):
178+
var existing_component = components[comp_key]
171179
remove_component(existing_component)
172180

173-
_component_path_cache[component] = resource_path
174-
components[resource_path] = component
181+
_component_key_cache[component] = comp_key
182+
components[comp_key] = component
175183
component.parent = self
176184
if not component.property_changed.is_connected(_on_component_property_changed):
177185
component.property_changed.connect(_on_component_property_changed)
178186
## Adding components happens through a signal
179187
component_added.emit(self , component)
180-
_entityLogger.trace("Added Component: ", resource_path)
188+
_entityLogger.trace("Added Component: ", comp_key)
181189

182190

183191
func _on_component_property_changed(
@@ -202,9 +210,9 @@ func add_components(_components: Array):
202210
for component in _components:
203211
if component == null:
204212
continue
205-
var component_path = component.get_script().resource_path
206-
if not components.has(component_path):
207-
components[component_path] = component
213+
var comp_key = _comp_key(component)
214+
if not components.has(comp_key):
215+
components[comp_key] = component
208216
added_components.append(component)
209217

210218
# If no new components were actually added, return early
@@ -232,9 +240,9 @@ func add_components(_components: Array):
232240
else:
233241
# Same archetype - just update the column data for new components
234242
for component in added_components:
235-
var comp_path = component.get_script().resource_path
243+
var comp_key = _comp_key(component)
236244
var entity_index = old_archetype.entity_to_index[ self ]
237-
old_archetype.columns[comp_path][entity_index] = component
245+
old_archetype.columns[comp_key][entity_index] = component
238246

239247
# Emit signals for all added components
240248
for component in added_components:
@@ -246,21 +254,20 @@ func add_components(_components: Array):
246254
## [b]Example:[/b]
247255
## [codeblock]entity.remove_component(HealthComponent)[/codeblock]
248256
func remove_component(component: Resource) -> void:
249-
# Use cached path if available, otherwise get it from the component class
250-
var resource_path: String
251-
if _component_path_cache.has(component):
252-
resource_path = _component_path_cache[component]
253-
_component_path_cache.erase(component)
257+
# Use cached key if available, otherwise derive it
258+
var comp_key: int
259+
if _component_key_cache.has(component):
260+
comp_key = _component_key_cache[component]
261+
_component_key_cache.erase(component)
254262
else:
255-
# Component parameter should be a class/script, consistent with has_component
256-
resource_path = component.resource_path
263+
comp_key = _comp_key(component)
257264

258-
if components.has(resource_path):
259-
var component_instance = components[resource_path]
260-
components.erase(resource_path)
265+
if components.has(comp_key):
266+
var component_instance = components[comp_key]
267+
components.erase(comp_key)
261268

262269
# Clean up cache entry for the component instance
263-
_component_path_cache.erase(component_instance)
270+
_component_key_cache.erase(component_instance)
264271

265272
# OBS-03: Disconnect property_changed before emitting removal signal.
266273
# Without this, phantom on_component_changed callbacks arrive whenever
@@ -270,7 +277,7 @@ func remove_component(component: Resource) -> void:
270277

271278
component_removed.emit(self , component_instance)
272279
# ARCHETYPE: Signal handler (_on_entity_component_removed) handles archetype update
273-
_entityLogger.trace("Removed Component: ", resource_path)
280+
_entityLogger.trace("Removed Component: ", comp_key)
274281

275282

276283
func deferred_remove_component(component: Resource) -> void:
@@ -303,12 +310,12 @@ func remove_components(_components: Array):
303310
comp_to_remove = _component
304311

305312
if comp_to_remove:
306-
var component_path = comp_to_remove.get_script().resource_path
307-
if components.has(component_path):
308-
components.erase(component_path)
313+
var comp_key = _comp_key(comp_to_remove)
314+
if components.has(comp_key):
315+
components.erase(comp_key)
309316
# Clean up cache entries for both the class and instance
310-
_component_path_cache.erase(_component)
311-
_component_path_cache.erase(comp_to_remove)
317+
_component_key_cache.erase(_component)
318+
_component_key_cache.erase(comp_to_remove)
312319
# OBS-03: Disconnect property_changed before emitting removal signal.
313320
if comp_to_remove.property_changed.is_connected(_on_component_property_changed):
314321
comp_to_remove.property_changed.disconnect(_on_component_property_changed)
@@ -356,14 +363,14 @@ func remove_all_components() -> void:
356363
## [b]Example:[/b]
357364
## [codeblock]var transform = entity.get_component(Transform)[/codeblock]
358365
func get_component(component: Resource) -> Component:
359-
return components.get(component.resource_path, null)
366+
return components.get(_comp_key(component), null)
360367

361368

362369
## Check to see if an entity has a specific component on it.[br]
363370
## This is useful when you're checking to see if it has a component and not going to use the component itself.[br]
364371
## If you plan on getting and using the component, use [method get_component] instead.
365372
func has_component(component: Resource) -> bool:
366-
return components.has(component.resource_path)
373+
return components.has(_comp_key(component))
367374

368375
#endregion Components
369376

@@ -517,9 +524,13 @@ func get_relationships(relationship: Relationship) -> Array[Relationship]:
517524

518525

519526
## Checks if the entity has a specific relationship.[br]
527+
## Fast path — skips validation/cleanup (use get_relationship when you need the value).[br]
520528
## [param relationship] The [Relationship] to check for.
521529
func has_relationship(relationship: Relationship) -> bool:
522-
return get_relationship(relationship) != null
530+
for rel in relationships:
531+
if rel.matches(relationship):
532+
return true
533+
return false
523534

524535
#endregion Relationships
525536

0 commit comments

Comments
 (0)