Skip to content

Releases: csprance/gecs

GECS v6.7.2 - Critical Query Cache Bugfix πŸ›

29 Nov 17:34

Choose a tag to compare

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 entities

Bug behavior:

  1. Entity A adds C_Dead β†’ creates new archetype β†’ cache invalidated βœ…
  2. Entity B adds C_Dead β†’ archetype already exists β†’ cache NOT invalidated ❌
  3. DeathSystem queries for C_Dead β†’ returns cached result (only Entity A) ❌
  4. Entity B never gets cleaned up ❌

The Fix

Modified 3 functions in world.gd to always invalidate cache on structural changes:

  1. _on_entity_component_added() - Always invalidate when components added
  2. _on_entity_component_removed() - Always invalidate when components removed
  3. remove_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

03 Nov 23:47

Choose a tag to compare

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:
image

  • 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.parent reference to their containing entity
  • Enhanced Property Change Signal: Added typing to the property_changed signal 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_cache variable 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

30 Oct 16:05
1b1a9e8

Choose a tag to compare

What's Changed

  • Remove specific one relationship when its reference pointers are the identical by @hwh008 in #70

New Contributors

Full Changelog: v6.2.0...v6.2.1

GECS v6.2.0 Release Notes

29 Oct 04:12

Choose a tag to compare

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

28 Oct 21:23

Choose a tag to compare

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

26 Oct 14:58

Choose a tag to compare

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

21 Oct 23:51
86cef01

Choose a tag to compare

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 * delta

New 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 * delta

New 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 * delta

Subsystems

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 * delta

If you DON'T call .iterate():

  • components will be an empty array []
  • Just use entity.get_component() like normal

Component alignment:

  • entities[i] pairs with components[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) to process(entities, components, delta)
  • Add for entity in entities: loop to maintain per-entity processing
  • Update process_all() systems to use unified process() signature
  • Update process_batch() systems to use unified process() signature
  • Remove ExecutionMethod parameters 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 of entity.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 * delta

After (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 * delta

After (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 * delta

Final 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

16 Oct 17:53

Choose a tag to compare

  • 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

15 Oct 23:04

Choose a tag to compare

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

15 Oct 22:07

Choose a tag to compare

[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:

  1. Entity.on_update() removed - Enforces proper ECS separation of concerns
  2. System.process_all() no longer returns bool - Simplified internal API
  3. Relationship system overhaul - Removed weak/strong matching in favor of component queries

Major Performance Improvements:

  1. Query cache key optimization - 85% faster cache key generation
  2. Query system speedup - 96-99% faster for cached queries
  3. System processing - 2-7% faster across all benchmarks
  4. Linear scaling - Query system now scales linearly instead of exponentially

New Features:

  1. Target component queries - Query both relation and target entity properties
  2. Limited relationship removal - Remove specific number of relationships
  3. Topological sort fix - System dependencies now execute in correct order
  4. 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 * delta

Why 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_run variable removed - Internal tracking variable was never used
  • Return type changed from bool to void - 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:

  • weak parameter removed from all relationship methods
  • Component.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_runner pattern
  • Proper lifecycle management - Tests now use auto_free() and world.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 setup
  • addons/gecs/tests/performance/performance_test_entities.gd - Refactored to use auto_free pattern
  • addons/gecs/tests/performance/performance_test_components.gd - Simplified cleanup using world.purge
  • addons/gecs/tests/performance/performance_test_queries.gd - Removed manual cleanup code
  • addons/gecs/tests/performance/performance_test_systems.gd - Uses scene_runner for world management
    ...
Read more