Releases: csprance/gecs
GECS v6.7.2 - Critical Query Cache Bugfix π
GECS v6.7.2 - Critical Query Cache Bugfix π
This is a critical bugfix release. All users should upgrade immediately.
π¨ Critical Fix
Fixed a query cache bug that caused stale query results when entities moved between existing archetypes.
The Problem
The query cache optimization was too aggressive:
- QueryBuilder.execute() caches the full entity list, not just archetype matches
- Cache was only invalidated when NEW archetypes were created
- Entities moving to EXISTING archetypes did NOT trigger cache invalidation
- Queries would return stale/incomplete results
Example Bug Scenario
# DeathSystem queries for entities with C_Dead component
class_name DeathSystem extends System
func query():
return q.with_all([C_Dead])
func process(entities: Array[Entity], components: Array, delta: float):
for entity in entities:
entity.queue_free() # Clean up dead entitiesBug behavior:
- Entity A adds
C_Deadβ creates new archetype β cache invalidated β - Entity B adds
C_Deadβ archetype already exists β cache NOT invalidated β - DeathSystem queries for
C_Deadβ returns cached result (only Entity A) β - Entity B never gets cleaned up β
The Fix
Modified 3 functions in world.gd to always invalidate cache on structural changes:
_on_entity_component_added()- Always invalidate when components added_on_entity_component_removed()- Always invalidate when components removedremove_entities()- Always invalidate when entities removed
π Performance Impact
- ~20% more cache invalidations in stress tests
- Correctness > optimization - this was a critical bug
- Real-world impact should be minimal (most games don't rapidly add/remove components)
β Testing
- 248 core tests passed
- All cache invalidation tests passed
- All query, entity, and archetype tests passed
- Performance tests show acceptable overhead
π Migration
No code changes required - this is a drop-in bugfix.
Simply update your GECS version to 6.7.2.
π Full Changelog
See CHANGELOG.md for complete details.
π Credits
Bug identified and fixed with assistance from Claude Code.
Upgrade now to ensure correct query behavior in your GECS-based games!
What's Changed
Full Changelog: v6.7.0...v6.7.2
GECS Release v6.7.0
GECS v6.7.0 - Enhanced Debug Viewer & Performance Improvements
This release focuses on dramatically improving the debugging experience with a completely overhauled Debug Viewer and Editor Debugger integration, along with important performance optimizations for archetype management.
π Debug Viewer Enhancements
The Debug Viewer has been significantly enhanced to provide real-time insights into your ECS architecture:

- Real-time Status Bars:
- Entity status bar showing live counts for entities, components, and relationships
- System status bar displaying total execution time and identifying the most expensive system
- Improved Tree Controls:
- Click-to-toggle system status directly in the tree for better UX
- Recursive collapse/expand functionality for both entity and system trees
- Separate collapse/expand controls for the systems panel
- Better User Feedback:
- Overlay notification when debug mode is disabled (debug mode required for viewer functionality)
- Improved sorting and display of entity information
- Enhanced color customization settings in the editor
β¨ New Features
- Component Parent Reference: Components now include a
Component.parentreference to their containing entity - Enhanced Property Change Signal: Added typing to the
property_changedsignal for better type safety - Remove All Relationships: Added ability to remove all relationships from an entity at once
β‘ Performance Optimizations
- Archetype Cache Optimization: Added
should_invalidate_cachevariable to optimize archetype operations during bulk component add/remove operations. This allows you to defer cache invalidation until after bulk operations complete, significantly improving performance when modifying many entities.
π Bug Fixes
- Fixed metadata existence check to prevent errors in the debug viewer
- Fixed handling of both Script and Resource instances in component processing
- Various stability improvements to the editor debugger tab display
π Documentation
- Added comprehensive Debug Viewer documentation
Full Changelog: v6.2.1...v6.7.0
Gecs v6.2.1
GECS v6.2.0 Release Notes
Changed enabled/disabled logic from creating two archetype lists for enabled disabled which could essentially double the number of archetypes you have in memory.
Switched to using a bitset where we can just store that and find enabled/disabled that way.
β
29% improvement in disabled entity query performance
β
Overall system is more memory efficient (50% fewer archetypes)
GECS 6.1.4 - Regression Fixes
This is a critical version to update to. There are some major issues with the cache in v6 up to here. If you're on v6 please move to v6.1.4
- Fixed issues with system execution path regressing and causing lots of issues with caching.
- Reverted to an older v6 version and cherry picked fixes from the future version.
v6.1.3
Problems Fixed
- Systems ignoring relationship filters - with_relationship() and without_relationship() now work correctly in system queries
- Systems ignoring group filters - with_group() and without_group() now work correctly in system queries
without_group() returning empty results - Fixed the logic to query ALL entities first, then exclude those in specified groups - Nonexistent groups returning all entities - Now correctly returns zero entities when querying for groups that don't exist
Debugging Tools
- improve the debugging tools in the Debugger Tab. Shows entities and Systems and Last Run Data of Systems. Can help with finding problem systems
- Was creating lambdas every system call. Moved that out to if ECS.debug
SubSystem Execution path
- Subsystems and Systems now follow the same execution path
- Cleaned up that flow a bit
GECSIO
- Add GECSIO.deserialize_gecs_data to make it so you can now deserialize GecsData to entities without needing to save it to a file first. This was needed so you can move entities between worlds if you want. Serialize to GECS Data and then Deserialize that GECS data and add it to the new world
Cache
- Cache Invalidation was a little too aggressive on a few things
Full Changelog: v6.1.2...v6.1.3
GECS v6.0.0 Release Notes
GECS v6.0.0 Release Notes
π Major Performance Upgrade
Version 6.0.0 introduces archetype-based storage with a unified, simplified System API. This release delivers massive performance improvements while making the framework easier to use.
β¨ What's New
1. Archetype-Based Storage (Automatic Performance Boost)
Entities are now grouped by their component "signature" (archetype) for ultra-fast iteration.
Performance gains (no code changes required):
- Query execution: 66% faster (0.584ms β 0.2ms @ 10k entities)
- System processing: 11% faster (24.2ms β 21.5ms @ 10k entities)
- Cache-friendly memory layout with sequential access
2. Unified System API (Simpler & More Flexible)
We've simplified the System API from three different methods to one universal signature:
Before v6 (three different signatures):
func process(entity: Entity, delta: float) # Per-entity
func process_all(entities: Array, delta: float) # Bulk
func process_batch(entities, components, delta) # Archetype (new in v6)After v6 (one signature for everything):
func process(entities: Array[Entity], components: Array, delta: float)Why this is better:
- β One method to learn, not three
- β Always receive arrays - you choose how to iterate
- β
Components available when you use
.iterate(), empty otherwise - β Simpler mental model
- β Encourages performance-friendly patterns
π Migration Guide
Breaking Change: System Signature
Old v5 signature:
func process(entity: Entity, delta: float):
var velocity = entity.get_component(C_Velocity)
entity.position += velocity.velocity * deltaNew v6 signature (add loop):
func process(entities: Array[Entity], components: Array, delta: float):
# Simple approach: loop through entities
for entity in entities:
var velocity = entity.get_component(C_Velocity)
entity.position += velocity.velocity * deltaNew v6 signature (fast approach with iterate):
func query():
return q.with_all([C_Velocity]).iterate([C_Velocity])
func process(entities: Array[Entity], components: Array, delta: float):
# Fast approach: use component arrays
var velocities = components[0] # C_Velocity from iterate()
for i in entities.size():
entities[i].position += velocities[i].velocity * deltaSubsystems
Subsystems no longer need the ExecutionMethod parameter - all subsystems use the same unified signature.
Old v5:
func sub_systems() -> Array[Array]:
return [
[q.with_all([C_Velocity]), process_velocity, System.ExecutionMethod.PROCESS_BATCH],
[q.with_all([C_Health]), process_health]
]New v6:
func sub_systems() -> Array[Array]:
return [
[q.with_all([C_Velocity]).iterate([C_Velocity]), process_velocity],
[q.with_all([C_Health]), process_health]
]
# All subsystems use same signature
func process_velocity(entities: Array[Entity], components: Array, delta: float):
var velocities = components[0]
for i in entities.size():
entities[i].position += velocities[i].velocity * delta
func process_health(entities: Array[Entity], components: Array, delta: float):
for entity in entities:
var health = entity.get_component(C_Health)
health.regenerate(delta)π― Understanding Components Array
The components parameter is a columnar array (Structure-of-Arrays):
func query():
return q.with_all([C_Velocity, C_Transform]).iterate([C_Velocity, C_Transform])
func process(entities: Array[Entity], components: Array, delta: float):
# components[0] = [velocity_a, velocity_b, velocity_c, ...]
# components[1] = [transform_a, transform_b, transform_c, ...]
# All aligned by index with entities array
var velocities = components[0]
var transforms = components[1]
for i in entities.size():
transforms[i].position += velocities[i].velocity * deltaIf you DON'T call .iterate():
componentswill be an empty array[]- Just use
entity.get_component()like normal
Component alignment:
entities[i]pairs withcomponents[0][i],components[1][i], etc.- Think of it like Python's
zip(entities, components[0], components[1])
β‘ Performance Characteristics
What Got Faster
- Query execution: 66% faster (read-heavy operations)
- System processing: 11% faster
- Iteration patterns: Cache-friendly sequential memory access
What Got Slower
- Component addition: 116% slower (159ms β 346ms @ 10k)
- Component removal: Similar regression
Why? Archetype storage optimizes for reads (queries every frame) at the cost of writes (occasional component changes). This is the right trade-off for game loops.
When to Use v6
β
High entity counts (1000+)
β
Frequent queries (every frame)
β
Read-heavy workloads (typical game loop)
When to Stick with v5
β Very low entity counts (<100)
β Write-heavy workloads (constant component add/remove)
β No performance concerns
π§ What's Removed
ExecutionMethod Enum
No longer needed - all systems use the unified process() signature.
process_all() Method
Replaced by unified process() - just don't loop if you want bulk processing.
process_batch() Method
Replaced by unified process() with .iterate() support.
β What's NOT Breaking
The q Variable
Still works! Use either q.with_all([...]) or ECS.world.query.with_all([...]).
Existing Queries
All query methods work exactly the same:
with_all([Components])with_any([Components])with_none([Components])with_relationship([Relations])with_group("group_name")
π Migration Checklist
Required Changes
- Update all
process(entity, delta)toprocess(entities, components, delta) - Add
for entity in entities:loop to maintain per-entity processing - Update
process_all()systems to use unifiedprocess()signature - Update
process_batch()systems to use unifiedprocess()signature - Remove
ExecutionMethodparameters from subsystems (optional now)
Optional Performance Optimizations
- Add
.iterate([...])to queries for component access - Replace
get_component()calls with direct array access - Use
components[0][i]instead ofentity.get_component() - Profile and measure the improvements
π§ͺ Testing
All core tests pass with the new unified API:
- β System processing tests
- β Archetype system tests
- β Query tests
- β Component tests
- β Performance benchmarks
Run tests:
addons/gdUnit4/runtest.cmd -a "res://addons/gecs/tests"π Example: Complete Before/After
Before (v5.x)
class_name MovementSystem
extends System
func query():
return q.with_all([C_Transform, C_Velocity])
func process(entity: Entity, delta: float):
var transform = entity.get_component(C_Transform)
var velocity = entity.get_component(C_Velocity)
transform.position += velocity.velocity * deltaAfter (v6.0 - Simple Migration)
class_name MovementSystem
extends System
func query():
return q.with_all([C_Transform, C_Velocity])
func process(entities: Array[Entity], components: Array, delta: float):
# Just add the loop
for entity in entities:
var transform = entity.get_component(C_Transform)
var velocity = entity.get_component(C_Velocity)
transform.position += velocity.velocity * deltaAfter (v6.0 - Optimized)
class_name MovementSystem
extends System
func query():
return q.with_all([C_Transform, C_Velocity]).iterate([C_Transform, C_Velocity])
func process(entities: Array[Entity], components: Array, delta: float):
# Use component arrays for max performance
var transforms = components[0]
var velocities = components[1]
for i in entities.size():
transforms[i].position += velocities[i].velocity * deltaFinal Thoughts
This release simplifies the API based on user feedback while delivering the archetype performance benefits. The unified process() signature makes GECS easier to learn and use while encouraging high-performance patterns.
Questions or issues? Open an issue on GitHub or check the documentation.
What's Changed
Full Changelog: v999.999.999...v6.0.0
GECS v5.1.0 Release Notes
- Split up GESCIO class a bit to be able to serialize entities and not just a query.
- Did some documentation and moved methods around
Full Changelog: v5.0.2...v5.1.0
v5.0.2
This mostly exists because of a small bug in 5.0.0 with the type system and because i forgot to push the version change in plugin.cfg :)
Full Changelog: v5.0.1...v5.0.2
GECS v5.0.0 Release Notes
[5.0.0] - 2025-10-15 - Major ECS Overhaul & Performance Awesomeness (Some Small Breaking Changes)
GECS v5.0.0 is a major release with massive performance improvements, API simplification, and relationship system overhaul.
This release combines all improvements from v5.0.0-rc1 through v5.0.0-rc4, delivering the most performant and cleanest GECS API to date.
π¦ What's in This Release
3 Breaking Changes:
- Entity.on_update() removed - Enforces proper ECS separation of concerns
- System.process_all() no longer returns bool - Simplified internal API
- Relationship system overhaul - Removed weak/strong matching in favor of component queries
Major Performance Improvements:
- Query cache key optimization - 85% faster cache key generation
- Query system speedup - 96-99% faster for cached queries
- System processing - 2-7% faster across all benchmarks
- Linear scaling - Query system now scales linearly instead of exponentially
New Features:
- Target component queries - Query both relation and target entity properties
- Limited relationship removal - Remove specific number of relationships
- Topological sort fix - System dependencies now execute in correct order
- Improved test suite - Zero orphan nodes, proper lifecycle management
β οΈ BREAKING CHANGES
Entity.on_update() Lifecycle Method Removed
The on_update(delta) lifecycle method has been removed from the Entity class:
on_update(delta)method removed - This lifecycle hook is no longer called- Use Systems instead - Entity logic should be handled by Systems, not in Entity methods
- Cleaner separation of concerns - Entities are data containers, Systems contain logic
Migration:
| Old (v4.x) | New (v5.0) |
|---|---|
Override on_update(delta) in Entity class |
Create a System that processes the entity |
entity.on_update(delta) called every frame |
System.process(entity, delta) |
Example Migration:
# β Old (v4.x) - Logic in Entity
class_name MyEntity extends Entity:
func on_update(delta: float):
# Entity logic here
position += velocity * delta
# β
New (v5.0) - Logic in System
class_name MySystem extends System:
func query():
return q.with_all([C_Transform, C_Velocity])
func process(entity: Entity, delta: float):
var transform = entity.get_component(C_Transform)
var velocity = entity.get_component(C_Velocity)
transform.position += velocity.direction * velocity.speed * deltaWhy this change?
This enforces proper ECS architecture where Entities are pure data containers and all logic lives in Systems. This makes code more modular, testable, and performant.
System.process_all() and System._process_parallel() No Longer Return Booleans
The process_all() and _process_parallel() methods now return void instead of bool:
did_runvariable removed - Internal tracking variable was never used- Return type changed from
booltovoid- Return values were never checked or used - No functional impact - These were internal implementation details
Migration:
| Old (v4.x) | New (v5.0) |
|---|---|
var result = system.process_all(entities, delta) |
system.process_all(entities, delta) |
Override process_all() returning bool |
Override process_all() returning void |
Why this change?
The boolean return values were historical artifacts that were never actually used anywhere in the codebase. Removing them simplifies the API and makes the code cleaner.
Removed Weak/Strong Matching System
The weak/strong matching system has been completely replaced with a simpler, more intuitive approach:
weakparameter removed from all relationship methodsComponent.equals()method removed - use component queries instead- Type matching is now the default - matches by component type only
- Component queries for property matching - use dictionaries for property-based filtering
Migration:
| Old (v4.x) | New (v5.0) |
|---|---|
entity.has_relationship(Relationship.new(C_Eats.new(5), target), false) |
entity.has_relationship(Relationship.new({C_Eats: {'value': {"_eq": 5}}}, target)) |
entity.has_relationship(Relationship.new(C_Eats.new(), target), true) |
entity.has_relationship(Relationship.new(C_Eats.new(), target)) |
entity.get_relationship(rel, true, true) |
entity.get_relationship(rel) |
entity.get_relationships(rel, true) |
entity.get_relationships(rel) |
Override equals() in component |
Use component queries: {C_Type: {'prop': {"_eq": value}}} |
Component Query Improvements
- Target component queries added - Query both relation AND target component properties
- Cannot add query relationships to entities - Queries are for matching only, not storage
- Fixed bug with falsy values - Component queries now correctly handle
0,false, etc.
β¨ New Features
Simplified Relationship Matching
# Type matching (default) - matches by component type
entity.has_relationship(Relationship.new(C_Damage.new(), target))
# Component query - matches by property criteria
entity.has_relationship(Relationship.new({C_Damage: {'amount': {"_gte": 50}}}, target))
# Query both relation AND target
var strong_buffs = ECS.world.query.with_relationship([
Relationship.new(
{C_Buff: {'duration': {"_gt": 10}}},
{C_Player: {'level': {"_gte": 5}}}
)
]).execute()Target Component Queries (NEW!)
# Query relationships by target component properties
var high_hp_targets = ECS.world.query.with_relationship([
Relationship.new(C_Targeting.new(), {C_Health: {'hp': {"_gte": 100}}})
]).execute()
# Mix relation and target queries
var critical_effects = ECS.world.query.with_relationship([
Relationship.new(
{C_Damage: {'type': {"_in": ["fire", "ice"]}}},
{C_Entity: {'level': {"_gte": 10}}}
)
]).execute()Limited Relationship Removal
# Remove specific number of relationships
entity.remove_relationship(Relationship.new(C_Damage.new(), null), 1) # Remove 1 damage
entity.remove_relationship(Relationship.new(C_Buff.new(), null), 3) # Remove up to 3 buffs
# Combine with component queries
entity.remove_relationship(
Relationship.new({C_Damage: {'amount': {"_gt": 20}}}, null),
2 # Remove up to 2 high-damage effects
)π¨ Migration Guide
1. Remove weak Parameters
# β Old (v4.x)
entity.has_relationship(rel, true)
entity.get_relationship(rel, true, true)
entity.get_relationships(rel, false)
# β
New (v5.0)
entity.has_relationship(rel)
entity.get_relationship(rel)
entity.get_relationships(rel)2. Replace Strong Matching with Component Queries
# β Old (v4.x) - strong matching for exact values
entity.has_relationship(Relationship.new(C_Eats.new(5), target), false)
# β
New (v5.0) - component query
entity.has_relationship(Relationship.new({C_Eats: {'value': {"_eq": 5}}}, target))3. Remove equals() Overrides
# β Old (v4.x) - custom equals() method
class_name C_Damage extends Component:
@export var amount: int = 0
func equals(other: Component) -> bool:
return amount == other.amount
# β
New (v5.0) - use component queries
# No equals() method needed!
# Query by property: {C_Damage: {'amount': {"_eq": 50}}}4. Check any deps function and sorting order
Topological sort was broken in previous versions. It is now fixed and as a result some systems may now be running in the correct order defined in the deps
but it may end up to be the wrong order for your game code. Check these depenencies by doing: print(ECS.world.systems_by_group) this will show you the sorted
systems and how they are running. Do a comparison between this version and the previous versions of GECS.
π§ͺ Test Suite Improvements
Performance Test Cleanup
- Eliminated orphan nodes - Refactored all performance tests to use
scene_runnerpattern - Proper lifecycle management - Tests now use
auto_free()andworld.purge()for cleanup - Consistent test structure - All performance tests follow same pattern as core tests
- Zero orphan nodes - Performance tests now maintain clean test environment
Files Updated:
addons/gecs/tests/performance/performance_test_base.gd- Uses scene_runner for proper test setupaddons/gecs/tests/performance/performance_test_entities.gd- Refactored to use auto_free patternaddons/gecs/tests/performance/performance_test_components.gd- Simplified cleanup using world.purgeaddons/gecs/tests/performance/performance_test_queries.gd- Removed manual cleanup codeaddons/gecs/tests/performance/performance_test_systems.gd- Uses scene_runner for world management
...