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

121
addons/gecs/LICENSE Normal file
View File

@@ -0,0 +1,121 @@
Creative Commons Legal Code
CC0 1.0 Universal
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
HEREUNDER.
Statement of Purpose
The laws of most jurisdictions throughout the world automatically confer
exclusive Copyright and Related Rights (defined below) upon the creator
and subsequent owner(s) (each and all, an "owner") of an original work of
authorship and/or a database (each, a "Work").
Certain owners wish to permanently relinquish those rights to a Work for
the purpose of contributing to a commons of creative, cultural and
scientific works ("Commons") that the public can reliably and without fear
of later claims of infringement build upon, modify, incorporate in other
works, reuse and redistribute as freely as possible in any form whatsoever
and for any purposes, including without limitation commercial purposes.
These owners may contribute to the Commons to promote the ideal of a free
culture and the further production of creative, cultural and scientific
works, or to gain reputation or greater distribution for their Work in
part through the use and efforts of others.
For these and/or other purposes and motivations, and without any
expectation of additional consideration or compensation, the person
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
is an owner of Copyright and Related Rights in the Work, voluntarily
elects to apply CC0 to the Work and publicly distribute the Work under its
terms, with knowledge of his or her Copyright and Related Rights in the
Work and the meaning and intended legal effect of CC0 on those rights.
1. Copyright and Related Rights. A Work made available under CC0 may be
protected by copyright and related or neighboring rights ("Copyright and
Related Rights"). Copyright and Related Rights include, but are not
limited to, the following:
i. the right to reproduce, adapt, distribute, perform, display,
communicate, and translate a Work;
ii. moral rights retained by the original author(s) and/or performer(s);
iii. publicity and privacy rights pertaining to a person's image or
likeness depicted in a Work;
iv. rights protecting against unfair competition in regards to a Work,
subject to the limitations in paragraph 4(a), below;
v. rights protecting the extraction, dissemination, use and reuse of data
in a Work;
vi. database rights (such as those arising under Directive 96/9/EC of the
European Parliament and of the Council of 11 March 1996 on the legal
protection of databases, and under any national implementation
thereof, including any amended or successor version of such
directive); and
vii. other similar, equivalent or corresponding rights throughout the
world based on applicable law or treaty, and any national
implementations thereof.
2. Waiver. To the greatest extent permitted by, but not in contravention
of, applicable law, Affirmer hereby overtly, fully, permanently,
irrevocably and unconditionally waives, abandons, and surrenders all of
Affirmer's Copyright and Related Rights and associated claims and causes
of action, whether now known or unknown (including existing as well as
future claims and causes of action), in the Work (i) in all territories
worldwide, (ii) for the maximum duration provided by applicable law or
treaty (including future time extensions), (iii) in any current or future
medium and for any number of copies, and (iv) for any purpose whatsoever,
including without limitation commercial, advertising or promotional
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
member of the public at large and to the detriment of Affirmer's heirs and
successors, fully intending that such Waiver shall not be subject to
revocation, rescission, cancellation, termination, or any other legal or
equitable action to disrupt the quiet enjoyment of the Work by the public
as contemplated by Affirmer's express Statement of Purpose.
3. Public License Fallback. Should any part of the Waiver for any reason
be judged legally invalid or ineffective under applicable law, then the
Waiver shall be preserved to the maximum extent permitted taking into
account Affirmer's express Statement of Purpose. In addition, to the
extent the Waiver is so judged Affirmer hereby grants to each affected
person a royalty-free, non transferable, non sublicensable, non exclusive,
irrevocable and unconditional license to exercise Affirmer's Copyright and
Related Rights in the Work (i) in all territories worldwide, (ii) for the
maximum duration provided by applicable law or treaty (including future
time extensions), (iii) in any current or future medium and for any number
of copies, and (iv) for any purpose whatsoever, including without
limitation commercial, advertising or promotional purposes (the
"License"). The License shall be deemed effective as of the date CC0 was
applied by Affirmer to the Work. Should any part of the License for any
reason be judged legally invalid or ineffective under applicable law, such
partial invalidity or ineffectiveness shall not invalidate the remainder
of the License, and in such case Affirmer hereby affirms that he or she
will not (i) exercise any of his or her remaining Copyright and Related
Rights in the Work or (ii) assert any associated claims and causes of
action with respect to the Work, in either case contrary to Affirmer's
express Statement of Purpose.
4. Limitations and Disclaimers.
a. No trademark or patent rights held by Affirmer are waived, abandoned,
surrendered, licensed or otherwise affected by this document.
b. Affirmer offers the Work as-is and makes no representations or
warranties of any kind concerning the Work, express, implied,
statutory or otherwise, including without limitation warranties of
title, merchantability, fitness for a particular purpose, non
infringement, or the absence of latent or other defects, accuracy, or
the present or absence of errors, whether or not discoverable, all to
the greatest extent permissible under applicable law.
c. Affirmer disclaims responsibility for clearing rights of other persons
that may apply to the Work or any use thereof, including without
limitation any person's Copyright and Related Rights in the Work.
Further, Affirmer disclaims responsibility for obtaining any necessary
consents, permissions or other rights required for any use of the
Work.
d. Affirmer understands and acknowledges that Creative Commons is not a
party to this document and has no duty or obligation with respect to
this CC0 or use of the Work.

136
addons/gecs/README.md Normal file
View File

@@ -0,0 +1,136 @@
# GECS Documentation
> **Complete documentation for the Godot Entity Component System**
<img src="./assets/logo.png" height="128" align="center">
**Lightning-fast Entity Component System for Godot 4.x** - Build scalable, maintainable games with clean separation of data and logic.
**Discord**: [Join our community](https://discord.gg/eB43XU2tmn)
## 📚 Learning Path
### 🚀 Getting Started (5-10 minutes)
- **[Getting Started Guide](docs/GETTING_STARTED.md)** - Build your first ECS project in 5 minutes
### 🧠 Core Understanding (20-30 minutes)
- **[Core Concepts](docs/CORE_CONCEPTS.md)** - Deep dive into Entities, Components, Systems, and Relationships
- **[Component Queries](docs/COMPONENT_QUERIES.md)** - Advanced property-based entity filtering
### 🛠️ Practical Application (30-60 minutes)
- **[Best Practices](docs/BEST_PRACTICES.md)** - Write maintainable, performant ECS code
- **[Relationships](docs/RELATIONSHIPS.md)** - Link entities together for complex interactions
- **[Observers](docs/OBSERVERS.md)** - Reactive systems that respond to component changes
- **[Serialization](docs/SERIALIZATION.md)** - Save and load game state and entities
### ⚡ Optimization & Advanced (As needed)
- **[Debug Viewer](docs/DEBUG_VIEWER.md)** - Real-time debugging and performance monitoring
- **[Performance Optimization](docs/PERFORMANCE_OPTIMIZATION.md)** - Make your games run fast and smooth
- **[Troubleshooting](docs/TROUBLESHOOTING.md)** - Solve common issues quickly
### 🔬 Framework Development (For contributors)
- **[Performance Testing](docs/PERFORMANCE_TESTING.md)** - Framework-level performance testing guide
## 📖 Documentation by Topic
### Entity Component System Basics
| Topic | Document | Description |
| ----------------- | ------------------------------------------ | ------------------------------------ |
| **Introduction** | [Getting Started](docs/GETTING_STARTED.md) | First ECS project tutorial |
| **Architecture** | [Core Concepts](docs/CORE_CONCEPTS.md) | Complete ECS architecture overview |
| **Data Patterns** | [Best Practices](docs/BEST_PRACTICES.md) | Component and system design patterns |
### Advanced Features
| Topic | Document | Description |
| ---------------------- | ---------------------------------------------- | ----------------------------------- |
| **Entity Linking** | [Relationships](docs/RELATIONSHIPS.md) | Connect entities with relationships |
| **Property Filtering** | [Component Queries](docs/COMPONENT_QUERIES.md) | Query entities by component data |
| **Event Systems** | [Observers](docs/OBSERVERS.md) | React to component changes |
| **Data Persistence** | [Serialization](docs/SERIALIZATION.md) | Save/load entities and game state |
### Optimization & Debugging
| Topic | Document | Description |
| ------------------ | ------------------------------------------------------------ | ----------------------------------- |
| **Debug Viewer** | [Debug Viewer](docs/DEBUG_VIEWER.md) | Real-time debugging and inspection |
| **Performance** | [Performance Optimization](docs/PERFORMANCE_OPTIMIZATION.md) | Game performance optimization |
| **Debugging** | [Troubleshooting](docs/TROUBLESHOOTING.md) | Common problems and solutions |
| **Testing** | [Performance Testing](docs/PERFORMANCE_TESTING.md) | Framework performance testing |
## 🎯 Quick References
### Naming Conventions
- **Entities**: `ClassCase` class, `e_entity_name.gd` file
- **Components**: `C_ComponentName` class, `c_component_name.gd` file
- **Systems**: `SystemNameSystem` class, `s_system_name.gd` file
- **Observers**: `ObserverNameObserver` class, `o_observer_name.gd` file
### Essential Patterns
```gdscript
# Entity creation
var player = Player.new()
player.add_component(C_Health.new(100))
player.add_component(C_Position.new(Vector2.ZERO))
ECS.world.add_entity(player)
# System queries
func query(): return q.with_all([C_Health, C_Position])
func process(entities: Array[Entity], components: Array, delta: float): # Unified signature
# Use .iterate([Components]) for batch component array access
# Relationships
entity.add_relationship(Relationship.new(C_Likes.new(), target_entity))
var likers = ECS.world.query.with_relationship([Relationship.new(C_Likes.new(), entity)]).execute()
# Component queries
var low_health = ECS.world.query.with_all([{C_Health: {"current": {"_lt": 20}}}]).execute()
# Order Independence: with_all/with_any/with_node component order does not affect matching or caching.
# The framework normalizes component sets internally so these yield identical results:
# ECS.world.query.with_all([C_Health, C_Position])
# ECS.world.query.with_all([C_Position, C_Health])
# Cache keys and archetype matching are order-insensitive.
# Serialization
var data = ECS.serialize(ECS.world.query.with_all([C_Persistent]))
ECS.save(data, "user://savegame.tres", true) # Binary format
var entities = ECS.deserialize("user://savegame.tres")
```
## 🎮 Example Projects
Basic examples are included in each guide. For complete game examples, see:
- **Simple Ball Movement** - [Getting Started Guide](docs/GETTING_STARTED.md)
- **Combat Systems** - [Relationships Guide](docs/RELATIONSHIPS.md)
- **UI Synchronization** - [Observers Guide](docs/OBSERVERS.md)
## 🆘 Getting Help
1. **Check documentation** - Most questions are answered in the guides above
2. **Review examples** - Each guide includes working code examples
3. **Try troubleshooting** - [Troubleshooting Guide](docs/TROUBLESHOOTING.md) covers common issues
4. **Community support** - [Join our Discord](https://discord.gg/eB43XU2tmn) for discussions and questions
## 🔄 Documentation Updates
This documentation is actively maintained. If you find errors or have suggestions:
- **Report issues** for bugs or unclear documentation
- **Suggest improvements** for better examples or explanations
- **Contribute examples** showing real-world usage patterns
---
**Ready to start?** Begin with the [Getting Started Guide](docs/GETTING_STARTED.md) and build your first ECS project in just 5 minutes!
_GECS makes building scalable, maintainable games easier by separating data from logic and providing powerful query systems for entity management._

View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#FFF"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="icon icon-tabler icons-tabler-outline icon-tabler-components">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M3 12l3 3l3 -3l-3 -3z"/>
<path d="M15 12l3 3l3 -3l-3 -3z"/>
<path d="M9 6l3 3l3 -3l-3 -3z"/>
<path d="M9 18l3 3l3 -3l-3 -3z"/>
</svg>

After

Width:  |  Height:  |  Size: 468 B

View File

@@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://d38ngung2m08d"
path="res://.godot/imported/component.svg-2925107dbd86d65190091453f68c12a8.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/gecs/assets/component.svg"
dest_files=["res://.godot/imported/component.svg-2925107dbd86d65190091453f68c12a8.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#8245BD"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="icon icon-tabler icons-tabler-outline icon-tabler-grid-scan">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M10 8v8"/>
<path d="M14 8v8"/>
<path d="M8 10h8"/>
<path d="M8 14h8"/>
<path d="M4 8v-2a2 2 0 0 1 2 -2h2"/>
<path d="M4 16v2a2 2 0 0 0 2 2h2"/>
<path d="M16 4h2a2 2 0 0 1 2 2v2"/>
<path d="M16 20h2a2 2 0 0 0 2 -2v-2"/>
</svg>

After

Width:  |  Height:  |  Size: 578 B

View File

@@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://byfliqr1et3o3"
path="res://.godot/imported/entity.svg-6abf599bc1490a75cbc955f854383af9.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/gecs/assets/entity.svg"
dest_files=["res://.godot/imported/entity.svg-6abf599bc1490a75cbc955f854383af9.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

Binary file not shown.

BIN
addons/gecs/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dfuhf6eppvmxn"
path="res://.godot/imported/logo.png-8dce28df5ee6c8739ca6b3801cfe878d.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/gecs/assets/logo.png"
dest_files=["res://.godot/imported/logo.png-8dce28df5ee6c8739ca6b3801cfe878d.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 512 512"><!-- Icon from
Game Icons by GameIcons - https://github.com/game-icons/icons/blob/master/license.txt -->
<path fill="#f409f8"
d="M256.242 19.143q-1.95.011-3.896.054c-8.782.195-17.528.884-26.198 2.053l13.957 18.725L276.9 68.008l-48.474-11.682l-22.586-31.424a235.5 235.5 0 0 0-68.522 27.258l4.67 19.35l58.403 10.51l-2.335 50.812l-17.522-35.623l-53.728-9.347l-6.063-24.886a233.3 233.3 0 0 0-41.679 38.227l13.866 45.06l54.898 8.176l-68.33 13.433l-14.633-48.287a239 239 0 0 0-10.844 17.068c-12.047 20.868-20.527 42.807-25.64 65.114l49.368 23.92l49.06-.58l-49.06 21.607l-18.105-8.81v25.747h30.37l-49.058 18.69v-53.526l-16.13-7.847c-5.85 41.047-.63 82.657 14.546 120.55l21.44-23.553l12.37 11.498l34.103-12.99l-21.453 24.75l16.443 15.285l-40.296-12.264l-14.17 15.908a233.7 233.7 0 0 0 35.44 51.586l40.896-.158l-28.047 13.262a236 236 0 0 0 43.89 32.63c112.427 64.91 255.91 26.462 320.82-85.964c64.91-112.427 26.464-255.91-85.962-320.82c-21.172-12.224-43.447-20.773-66.09-25.862l9.207 25.723l-29.07-29.292a234 234 0 0 0-32.648-2.115zm81.076 126.125c21.167.245 42.198 5.62 61.43 16.72c65.644 37.893 83.97 127.31 42.557 199.026c-41.41 71.71-128.022 100.554-193.666 62.662s-83.97-127.31-42.558-199.026c28.47-49.303 78.305-78.34 128.002-79.363a130 130 0 0 1 4.234-.02zm-.59 18.67a111 111 0 0 0-3.654.023c-18.408.41-36.93 5.29-54.09 14.185l34.21 53.44a87.7 87.7 0 0 0-15.227 10.874l-34.947-54.59c-16.317 11.548-30.75 27.068-41.754 46.126a154 154 0 0 0-2.27 4.092l62.068 24.504a91.4 91.4 0 0 0-8.17 16.867l-61.72-24.366c-10.588 27.475-12.18 56.18-5.824 81.922l62.4-21.23c.464 6.36 1.638 12.59 3.516 18.544l-60.073 20.44c9.103 21.78 24.502 40.32 45.436 52.51l30.73-45a69.4 69.4 0 0 0 14.5 11.904l-28.08 41.12c20.49 7.43 42.64 8.273 64.046 3.23l.31-33.95c6.27.064 12.55-.674 18.71-2.166l-.276 30.12c12.81-5.225 25.06-12.622 36.186-22.013l-11.71-18.29a88 88 0 0 0 14.808-11.53l10.613 16.578a149 149 0 0 0 18.654-25.615c1.91-3.307 3.67-6.652 5.294-10.023l-16.094-6.354c2.808-5.71 4.987-11.582 6.504-17.522l16.633 6.566c5.45-16.308 7.792-32.934 7.25-49.018l-22.186 7.55a79 79 0 0 0-5.09-18.01l25.246-8.588c-3.6-19.748-11.75-38.048-24.008-53.122l-22.637 33.147a70 70 0 0 0-8.602-5.852a71 71 0 0 0-7.487-3.725l25.425-37.23a105.56 105.56 0 0 0-46.547-23.094l-.502 54.96c-2.52-.213-5.05-.3-7.58-.248c-3.716.073-7.43.437-11.115 1.06l.532-58.135q-1.71-.07-3.423-.09zm15.786 75.83c3.027.026 6.037.308 9.006.84c-7.354 7.116-12.168 18.937-12.168 32.326c0 21.752 12.7 39.384 28.367 39.384c12.172 0 22.55-10.647 26.577-25.597c2.1 14.36-.655 30.18-9.07 44.72c-18.287 31.595-55.212 43.19-82.24 27.623c-27.03-15.567-35.414-53.21-17.128-84.805c12.57-21.722 33.953-33.99 54.872-34.477a56 56 0 0 1 1.782-.012z" />
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dqhpkkwhk2kcd"
path="res://.godot/imported/observer.svg-4447d1c14fb8407efb95472a8cbe6c0b.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/gecs/assets/observer.svg"
dest_files=["res://.godot/imported/observer.svg-4447d1c14fb8407efb95472a8cbe6c0b.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View File

@@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#BDAB02"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="icon icon-tabler icons-tabler-outline icon-tabler-cpu">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M5 5m0 1a1 1 0 0 1 1 -1h12a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-12a1 1 0 0 1 -1 -1z"/>
<path d="M9 9h6v6h-6z"/>
<path d="M3 10h2"/>
<path d="M3 14h2"/>
<path d="M10 3v2"/>
<path d="M14 3v2"/>
<path d="M21 10h-2"/>
<path d="M21 14h-2"/>
<path d="M14 21v-2"/>
<path d="M10 21v-2"/>
</svg>

After

Width:  |  Height:  |  Size: 637 B

View File

@@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://d3yy7cagqxvuy"
path="res://.godot/imported/system.svg-3ba90685fdf6d3608f3a7eef4b55f305.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/gecs/assets/system.svg"
dest_files=["res://.godot/imported/system.svg-3ba90685fdf6d3608f3a7eef4b55f305.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="#FFF"><!-- Icon from
TDesign Icons by TDesign - https://github.com/Tencent/tdesign-icons/blob/main/LICENSE -->
<path
d="M23 2H1v16h10.768a6.7 6.7 0 0 1 .96-4.002H3v-10h18v7.23a6.8 6.8 0 0 1 2 1.24zM3 20h9.228a6.8 6.8 0 0 0 1.24 2H3z" />
<path
d="M19.5 13.376V12h-2v1.376a4 4 0 0 0-1.854 1.072l-1.193-.689l-1 1.732l1.192.688a4 4 0 0 0 0 2.142l-1.192.688l1 1.732l1.193-.689a4 4 0 0 0 1.854 1.072V22.5h2v-1.376a4 4 0 0 0 1.854-1.072l1.192.689l1-1.732l-1.191-.688a4 4 0 0 0 0-2.142l1.191-.688l-1-1.732l-1.192.688a4 4 0 0 0-1.854-1.071m-2.715 2.844a2 2 0 0 1 3.43 0l.036.063c.159.287.249.616.249.967c0 .35-.09.68-.249.967l-.037.063a2 2 0 0 1-3.429 0l-.037-.063a2 2 0 0 1-.248-.967a2 2 0 0 1 .248-.967z" />
</svg>

After

Width:  |  Height:  |  Size: 824 B

View File

@@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bebopiehk02k1"
path="res://.godot/imported/system_folder.svg-9f95776b45329abe5c6e0deb4fa1cf7c.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/gecs/assets/system_folder.svg"
dest_files=["res://.godot/imported/system_folder.svg-9f95776b45329abe5c6e0deb4fa1cf7c.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View File

@@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#2F4ABD"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="icon icon-tabler icons-tabler-outline icon-tabler-world">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0"/>
<path d="M3.6 9h16.8"/>
<path d="M3.6 15h16.8"/>
<path d="M11.5 3a17 17 0 0 0 0 18"/>
<path d="M12.5 3a17 17 0 0 1 0 18"/>
</svg>

After

Width:  |  Height:  |  Size: 541 B

View File

@@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dhu1m3rpx1al3"
path="res://.godot/imported/world.svg-4fb4d15549e6aa583bb2553a24189477.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/gecs/assets/world.svg"
dest_files=["res://.godot/imported/world.svg-4fb4d15549e6aa583bb2553a24189477.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

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

View File

@@ -0,0 +1,724 @@
# GECS Best Practices Guide
> **Write maintainable, performant ECS code**
This guide covers proven patterns and practices for building robust games with GECS. Apply these patterns to keep your code clean, fast, and easy to debug.
## 📋 Prerequisites
- Completed [Getting Started Guide](GETTING_STARTED.md)
- Understanding of [Core Concepts](CORE_CONCEPTS.md)
## 🧱 Component Design Patterns
### Keep Components Pure Data
Components should only hold data, never logic or behavior.
```gdscript
# ✅ Good - Pure data component
class_name C_Health
extends Component
@export var current: float = 100.0
@export var maximum: float = 100.0
@export var regeneration_rate: float = 1.0
func _init(max_health: float = 100.0):
maximum = max_health
current = max_health
```
```gdscript
# ❌ Avoid - Logic in components
class_name C_Health
extends Component
@export var current: float = 100.0
@export var maximum: float = 100.0
# This belongs in a system, not a component
func take_damage(amount: float):
current -= amount
if current <= 0:
print("Entity died!")
```
### Use Composition Over Inheritance
Build entities by combining simple components rather than complex inheritance hierarchies.
```gdscript
# ✅ Good - Composable components via define_components() or scene setup
class_name Player
extends Entity
func define_components() -> Array:
return [
C_Health.new(100),
C_Transform.new(),
C_Input.new()
]
class_name Enemy
extends Entity
func define_components() -> Array:
return [
C_Health.new(50),
C_Transform.new(),
C_AI.new()
]
```
### Design for Configuration
Make components easily configurable through export properties.
```gdscript
# ✅ Good - Configurable component
class_name C_Movement
extends Component
@export var speed: float = 100.0
@export var acceleration: float = 500.0
@export var friction: float = 800.0
@export var max_speed: float = 300.0
@export var can_fly: bool = false
func _init(spd: float = 100.0, can_fly_: bool = false):
speed = spd
can_fly = can_fly_
```
## ⚙️ System Design Patterns
### Single Responsibility Principle
Each system should handle one specific concern.
```gdscript
# ✅ Good - Focused systems
class_name MovementSystem extends System
func query(): return q.with_all([C_Position, C_Velocity])
class_name RenderSystem extends System
func query(): return q.with_all([C_Position, C_Sprite])
class_name HealthSystem extends System
func query(): return q.with_all([C_Health])
```
### Use System Groups for Processing Order
Organize systems into logical groups using scene-based organization. Systems are grouped in scene nodes and processed in the correct order.
```gdscript
# main.gd - Process systems in correct order
func _process(delta):
world.process(delta, "run-first") # Initialization systems
world.process(delta, "input") # Input handling
world.process(delta, "gameplay") # Game logic
world.process(delta, "ui") # UI updates
world.process(delta, "run-last") # Cleanup systems
func _physics_process(delta):
world.process(delta, "physics") # Physics systems
world.process(delta, "debug") # Debug systems
```
### Early Exit for Performance
Return early from system processing when no work is needed.
```gdscript
# ✅ Good - Early exit patterns
class_name HealthRegenerationSystem extends System
func query():
return q.with_all([C_Health]).with_none([C_Dead])
func process(entities: Array[Entity], components: Array, delta: float):
for entity in entities:
var health = entity.get_component(C_Health)
# Early exit if already at max health
if health.current >= health.maximum:
continue
# Apply regeneration
health.current = min(health.current + health.regeneration_rate * delta, health.maximum)
```
## 🏗️ Code Organization Patterns
### GECS Naming Conventions
```gdscript
# ✅ GECS Standard naming patterns:
# Components: C_ComponentName class, c_component_name.gd file
class_name C_Health extends Component # c_health.gd
class_name C_Position extends Component # c_position.gd
# Systems: SystemNameSystem class, s_system_name.gd file
class_name MovementSystem extends System # s_movement.gd
class_name RenderSystem extends System # s_render.gd
# Entities: EntityName class, e_entity_name.gd file
class_name Player extends Entity # e_player.gd
class_name Enemy extends Entity # e_enemy.gd
# Observers: ObserverNameObserver class, o_observer_name.gd file
class_name HealthUIObserver extends Observer # o_health_ui.gd
```
### File Organization
Organize your ECS files by theme for better scalability:
```
project/
├── components/
│ ├── ai/ # AI-related components
│ ├── animation/ # Animation components
│ ├── gameplay/ # Core gameplay components
│ ├── gear/ # Equipment/gear components
│ ├── item/ # Item system components
│ ├── multiplayer/ # Multiplayer-specific
│ ├── relationships/ # Relationship components
│ ├── rendering/ # Visual/rendering
│ └── weapon/ # Weapon system
├── entities/
│ ├── enemies/ # Enemy entities
│ ├── gameplay/ # Core entities
│ ├── items/ # Item entities
│ └── ui/ # UI entities
├── systems/
│ ├── combat/ # Combat systems
│ ├── core/ # Core ECS systems
│ ├── gameplay/ # Gameplay systems
│ ├── input/ # Input systems
│ ├── interaction/ # Interaction systems
│ ├── physics/ # Physics systems
│ └── ui/ # UI systems
└── observers/
└── o_transform.gd # Reactive systems
```
## 🎮 Common Game Patterns
### Player Character Pattern
```gdscript
# e_player.gd
class_name Player
extends Entity
func on_ready():
# Common pattern: sync scene transform to component
if has_component(C_Transform):
var transform_comp = get_component(C_Transform)
transform_comp.transform = global_transform
add_to_group("player")
```
### Enemy Pattern
```gdscript
# e_enemy.gd
class_name Enemy
extends Entity
func on_ready():
# Sync transform and add to enemy group
if has_component(C_Transform):
var transform_comp = get_component(C_Transform)
transform_comp.transform = global_transform
add_to_group("enemies")
```
## 🚀 Performance Best Practices
### Choose the Right Query Method ⭐ NEW!
**Query Performance Ranking** (v5.0.0-rc4+):
```gdscript
# 🏆 FASTEST - Enabled/disabled queries (constant time)
class_name ActiveEntitiesOnly extends System
func query():
return q.enabled(true) # ~0.05ms for any number of entities
# 🥈 EXCELLENT - Component queries (heavily optimized)
class_name MovementSystem extends System
func query():
return q.with_all([C_Position, C_Velocity]) # ~0.6ms for 10K entities
# 🥉 GOOD - Use with_any strategically
class_name DamageableSystem extends System
func query():
return q.with_any([C_Player, C_Enemy]).with_all([C_Health]) # ~5.6ms for 10K
# 🐌 AVOID - Group queries are slowest
class_name PlayerSystem extends System
func query():
return q.with_group("player") # ~16ms for 10K entities
# Better: q.with_all([C_Player])
```
### Use iterate() for Batch Performance
```gdscript
# ✅ Good - Batch processing with iterate()
class_name TransformSystem
extends System
func query():
# Use iterate() to get component arrays
return q.with_all([C_Transform]).iterate([C_Transform])
func process(entities: Array[Entity], components: Array, delta: float):
# Batch access to components for better performance
var transforms = components[0] # C_Transform array from iterate()
for i in range(entities.size()):
entities[i].global_transform = transforms[i].transform
```
### Use Specific Queries
```gdscript
# ✅ BEST - Combine enabled filter with components
class_name ActivePlayerInputSystem extends System
func query():
return q.with_all([C_Input, C_Movement]).enabled(true)
# Super fast: enabled filtering + component matching
# ✅ GOOD - Specific component query
class_name ProjectileSystem extends System
func query():
return q.with_all([C_Projectile, C_Velocity]) # Fast and specific
# ❌ AVOID - Group-based queries (slow)
class_name PlayerSystem extends System
func query():
return q.with_group("player") # Use q.with_all([C_Player]) instead
# ❌ AVOID - Overly broad queries
class_name UniversalMovementSystem extends System
func query():
return q.with_all([C_Transform]) # Too broad - matches everything
```
## 🎭 Entity Prefabs (Scene Files)
### Using Godot Scenes as Entity Prefabs
The most powerful pattern in GECS is using Godot's scene system (.tscn files) as entity prefabs. This combines ECS data with Godot's visual editor:
```
e_player.tscn Structure:
├── Player (Entity node - extends your e_player.gd class)
│ ├── MeshInstance3D (visual representation)
│ ├── CollisionShape3D (physics collision)
│ ├── AudioStreamPlayer3D (sound effects)
│ └── SkeletonAttachment3D (for equipment)
```
**Benefits of Scene-based Prefabs:**
- **Visual Editing**: Design entities in Godot's 3D editor
- **Component Assignment**: Set up ECS components in the Inspector
- **Godot Integration**: Leverage existing Godot nodes and systems
- **Reusability**: Instantiate the same prefab multiple times
- **Version Control**: Scene files work well with git
**Setting up Entity Prefabs:**
1. **Create scene with Entity as root**: `e_player.tscn` with `Player` entity node.
- Another trick here is to add a CharacterBody3d and then extend that CharacterBody3D with the e_player.gd script this way you get Entity class and CharacterBody3D class data
2. **Add visual/physics children**: Add MeshInstance3D, CollisionShape3D, etc. as children
3. **Configure components in Inspector**: Add components to the `component_resources` array
4. **Save as reusable prefab**: Save the .tscn file for instantiation
5. **Set up on_ready()**: Handle any initialization logic
### Component Assignment in Prefabs
**Method 1: Inspector Assignment (Recommended)**
Set up components directly in the Godot Inspector:
```gdscript
# In e_player.tscn entity root node Inspector:
# Component Resources array:
# - [0] C_Health.new() (max: 100, current: 100)
# - [1] C_Transform.new() (synced with scene transform)
# - [2] C_Input.new() (for player controls)
# - [3] C_LocalPlayer.new() (mark as local player)
```
**Method 2: define_components() (Programmatic)**
```gdscript
# e_player.gd attached to Player.tscn root
class_name Player
extends Entity
func define_components() -> Array:
return [
C_Health.new(100),
C_Transform.new(),
C_Input.new(),
C_LocalPlayer.new()
]
func on_ready():
# Initialize after components are ready
if has_component(C_Transform):
var transform_comp = get_component(C_Transform)
transform_comp.transform = global_transform
add_to_group("player")
```
**Method 3: Hybrid Approach**
```gdscript
# Core components via Inspector, dynamic components via script
func on_ready():
# Sync scene transform to component
if has_component(C_Transform):
var transform_comp = get_component(C_Transform)
transform_comp.transform = global_transform
# Add conditional components based on game state
if GameState.is_multiplayer:
add_component(C_NetworkSync.new())
if GameState.debug_mode:
add_component(C_DebugInfo.new())
```
### Instantiating Entity Prefabs
**Basic Spawning Pattern:**
```gdscript
# Spawn system or main scene
@export var player_prefab: PackedScene
@export var enemy_prefab: PackedScene
func spawn_player(position: Vector3) -> Entity:
var player = player_prefab.instantiate() as Entity
player.global_position = position
get_tree().current_scene.add_child(player) # Add to scene
ECS.world.add_entity(player) # Register with ECS
return player
func spawn_enemy(position: Vector3) -> Entity:
var enemy = enemy_prefab.instantiate() as Entity
enemy.global_position = position
get_tree().current_scene.add_child(enemy)
ECS.world.add_entity(enemy)
return enemy
```
**Advanced Spawning with SpawnSystem:**
```gdscript
# s_spawner.gd
class_name SpawnerSystem
extends System
func query():
return q.with_all([C_SpawnPoint])
func process(entities: Array[Entity], components: Array, delta: float):
for entity in entities:
var spawn_point = entity.get_component(C_SpawnPoint)
if spawn_point.should_spawn():
var spawned = spawn_point.prefab.instantiate() as Entity
spawned.global_position = entity.global_position
get_tree().current_scene.add_child(spawned)
ECS.world.add_entity(spawned)
spawn_point.mark_spawned()
```
**Prefab Management Best Practices:**
```gdscript
# Organize prefabs in preload statements
const PLAYER_PREFAB = preload("res://entities/gameplay/e_player.tscn")
const ENEMY_PREFAB = preload("res://entities/enemies/e_enemy.tscn")
const WEAPON_PREFAB = preload("res://entities/items/e_weapon.tscn")
# Or use a prefab registry
class_name PrefabRegistry
static var prefabs = {
"player": preload("res://entities/gameplay/e_player.tscn"),
"enemy": preload("res://entities/enemies/e_enemy.tscn"),
"weapon": preload("res://entities/items/e_weapon.tscn")
}
static func spawn(prefab_name: String, position: Vector3) -> Entity:
var prefab = prefabs[prefab_name]
var entity = prefab.instantiate() as Entity
entity.global_position = position
get_tree().current_scene.add_child(entity)
ECS.world.add_entity(entity)
return entity
```
## 🏗️ Main Scene Architecture
### Scene Structure Pattern
Organize your main scene using the proven structure pattern:
```
Main.tscn
├── World (World node)
├── DefaultSystems (Node - instantiated from default_systems.tscn)
│ ├── run-first (Node - SystemGroup)
│ │ ├── VictimInitSystem
│ │ └── EcsStorageLoad
│ ├── input (Node - SystemGroup)
│ │ ├── ItemSystem
│ │ ├── WeaponsSystem
│ │ └── PlayerControlsSystem
│ ├── gameplay (Node - SystemGroup)
│ │ ├── GearSystem
│ │ ├── DeathSystem
│ │ └── EventSystem
│ ├── physics (Node - SystemGroup)
│ │ ├── FrictionSystem
│ │ ├── CharacterBody3DSystem
│ │ └── TransformSystem
│ ├── ui (Node - SystemGroup)
│ │ └── UiVisibilitySystem
│ ├── debug (Node - SystemGroup)
│ │ └── DebugLabel3DSystem
│ └── run-last (Node - SystemGroup)
│ ├── ActionsSystem
│ └── PendingDeleteSystem
├── Level (Node3D - for level geometry)
└── Entities (Node3D - spawned entities go here)
```
### Systems Setup in Main Scene
**Scene-based Systems Setup (Recommended)**
Use scene composition to organize systems. The default_systems.tscn contains all systems organized by execution groups:
```gdscript
# main.gd - Simple main scene setup
extends Node
@onready var world: World = $World
func _ready():
Bootstrap.bootstrap() # Initialize any game-specific setup
ECS.world = world
# Systems are automatically registered via scene composition
```
**Creating a Default Systems Scene:**
1. Create `default_systems.tscn` with system groups as Node children
2. Add individual system scripts as children of each group
3. Instantiate this scene in your main scene
4. Systems are automatically discovered and registered by the World
### Processing Systems by Group
```gdscript
# main.gd - Process systems in correct order
extends Node3D
func _process(delta):
if ECS.world:
ECS.process(delta, "input") # Handle input first
ECS.process(delta, "core") # Core logic
ECS.process(delta, "gameplay") # Game mechanics
ECS.process(delta, "render") # UI/visual updates last
func _physics_process(delta):
if ECS.world:
ECS.process(delta, "physics") # Physics systems
```
## 🛠️ Common Utility Patterns
### Transform Synchronization
Common transform synchronization patterns:
```gdscript
# Sync entity transform TO component (scene → component)
static func sync_transform_to_component(entity: Entity):
if entity.has_component(C_Transform):
var transform_comp = entity.get_component(C_Transform)
transform_comp.transform = entity.global_transform
# Sync component transform TO entity (component → scene)
static func sync_component_to_transform(entity: Entity):
if entity.has_component(C_Transform):
var transform_comp = entity.get_component(C_Transform)
entity.global_transform = transform_comp.transform
# Common usage in entity on_ready()
func on_ready():
sync_transform_to_component(self) # Sync scene position to C_Transform
```
### Component Helpers
Build helpers for common component operations:
```gdscript
# Helper functions you can add to your project
static func add_health_to_entity(entity: Entity, max_health: float):
var health = C_Health.new(max_health)
entity.add_component(health)
return health
static func damage_entity(entity: Entity, amount: float):
if entity.has_component(C_Health):
var health = entity.get_component(C_Health)
health.current = max(0, health.current - amount)
return health.current <= 0 # Return true if entity died
return false
```
## 🎛️ Relationship Management Best Practices
### Limited Removal Patterns
**Use Descriptive Constants:**
```gdscript
# ✅ Good - Clear intent with constants
const WEAK_CLEANSE = 1
const MEDIUM_CLEANSE = 3
const STRONG_CLEANSE = -1 # All
# ✅ Good - Stack-based constants
const SINGLE_STACK = 1
const PARTIAL_STACKS = 3
const ALL_STACKS = -1
func cleanse_debuffs(entity: Entity, power: int):
match power:
1: entity.remove_relationship(Relations.any_debuff(), WEAK_CLEANSE)
2: entity.remove_relationship(Relations.any_debuff(), MEDIUM_CLEANSE)
3: entity.remove_relationship(Relations.any_debuff(), STRONG_CLEANSE)
```
**Validate Before Removal:**
```gdscript
# ✅ Excellent - Safe removal with validation
func safe_partial_heal(entity: Entity, heal_amount: int):
var damage_rels = entity.get_relationships(Relations.any_damage())
if damage_rels.is_empty():
print("Entity has no damage to heal")
return
var to_heal = min(heal_amount, damage_rels.size())
entity.remove_relationship(Relations.any_damage(), to_heal)
print("Healed ", to_heal, " damage effects")
# ✅ Good - Helper function with built-in safety
func remove_poison_stacks(entity: Entity, stacks_to_remove: int):
if stacks_to_remove <= 0:
return
entity.remove_relationship(Relations.poison_effect(), stacks_to_remove)
```
**System Integration Patterns:**
```gdscript
# ✅ Excellent - Integration with game systems
class_name StatusEffectSystem extends System
func process(entities: Array[Entity], components: Array, delta: float):
# Example: process spell casting entities
for entity in entities:
var spell = entity.get_component(C_SpellCaster)
if spell.is_casting_cleanse():
process_cleanse_spell(entity, spell.target, spell.power)
func process_cleanse_spell(caster: Entity, target: Entity, spell_power: int):
# Calculate cleanse strength based on spell power and caster stats
var cleanse_strength = calculate_cleanse_strength(caster, spell_power)
# Apply graduated cleansing based on strength
match cleanse_strength:
1..3: target.remove_relationship(Relations.any_debuff(), 1)
4..6: target.remove_relationship(Relations.any_debuff(), 2)
7..9: target.remove_relationship(Relations.any_debuff(), 3)
_: target.remove_relationship(Relations.any_debuff()) # Remove all
func process_antidote_item(user: Entity, antidote_strength: int):
# Remove poison based on antidote quality
user.remove_relationship(Relations.poison_effect(), antidote_strength)
# Remove poison resistance temporarily to prevent immediate repoison
user.add_relationship(Relations.poison_immunity(), 5.0) # 5 second immunity
class_name InventorySystem extends System
func consume_item_stack(entity: Entity, item_type: Script, count: int):
# Consume specific number of items from inventory
entity.remove_relationship(
Relationship.new(C_HasItem.new(), item_type),
count
)
func use_consumable(entity: Entity, item: Component, quantity: int = 1):
# Use consumable items with quantity
entity.remove_relationship(
Relationship.new(C_HasItem.new(), item),
quantity
)
```
**Performance Optimization:**
```gdscript
# ✅ Good - Cache relationships for multiple operations
func optimize_bulk_removal(entity: Entity):
# Cache the relationship for reuse
var poison_rel = Relations.poison_effect()
var damage_rel = Relations.any_damage()
# Multiple targeted removals
entity.remove_relationship(poison_rel, 2) # Remove 2 poison
entity.remove_relationship(damage_rel, 1) # Remove 1 damage
entity.remove_relationship(poison_rel, 1) # Remove 1 more poison
# ✅ Excellent - Batch removal patterns
func batch_cleanup(entities: Array[Entity]):
var cleanup_rel = Relations.temporary_effect()
for entity in entities:
# Remove up to 3 temporary effects from each entity
entity.remove_relationship(cleanup_rel, 3)
```
## 🎯 Next Steps
Now that you understand best practices:
1. **Apply these patterns** in your projects
2. **Learn advanced topics** in [Core Concepts](CORE_CONCEPTS.md)
3. **Optimize performance** with [Performance Guide](PERFORMANCE_OPTIMIZATION.md)
**Need help?** [Join our Discord](https://discord.gg/eB43XU2tmn) for community discussions and support.
---
_"Good ECS code is like a well-organized toolbox - every component has its place, every system has its purpose, and everything works together smoothly."_

View File

@@ -0,0 +1,182 @@
# Component Queries in GECS
> **Advanced property-based entity filtering**
Component Queries provide a powerful way to filter entities not just based on the presence of components but also on the data within those components. This allows for precise, data-driven entity selection in your game systems.
## 📋 Prerequisites
- Understanding of [Core Concepts](CORE_CONCEPTS.md)
- Familiarity with [Basic Queries](CORE_CONCEPTS.md#query-system)
## 🎯 Introduction
In standard ECS queries, you filter entities by which components they have or don't have. Component Queries take this further by letting you filter based on the **values** inside those components.
Instead of just asking "which entities have a HealthComponent?", you can ask "which entities have a HealthComponent with current health less than 20?"
## Using Component Queries with `QueryBuilder`
The `QueryBuilder` class allows you to construct queries to retrieve entities that match certain criteria. With component queries, you can specify conditions on component properties within `with_all` and `with_any` methods.
### Syntax
A component query is a `Dictionary` that maps a component class to a query `Dictionary` specifying property conditions.
```gdscript
{ ComponentClass: { property_name: { operator: value } } }
```
### Supported Operators
- `_eq`: Equal to
- `_ne`: Not equal to
- `_gt`: Greater than
- `_lt`: Less than
- `_gte`: Greater than or equal to
- `_lte`: Less than or equal to
- `_in`: Value is in a list
- `_nin`: Value is not in a list
### Examples
#### 1. Basic Component Query
Retrieve entities where `C_TestC.value` is equal to `25`.
```gdscript
var result = QueryBuilder.new(world).with_all([
{ C_TestC: { "value": { "_eq": 25 } } }
]).execute()
```
#### 2. Multiple Conditions on a Single Component
Retrieve entities where `C_TestC.value` is between `20` and `25`.
```gdscript
var result = QueryBuilder.new(world).with_all([
{ C_TestC: { "value": { "_gte": 20, "_lte": 25 } } }
]).execute()
```
#### 3. Combining Component Queries and Regular Components
Retrieve entities that have `C_TestD` component and `C_TestC.value` greater than `20`.
```gdscript
var result = QueryBuilder.new(world).with_all([
C_TestD,
{ C_TestC: { "value": { "_gt": 20 } } }
]).execute()
```
#### 4. Using `with_any` with Component Queries
Retrieve entities where `C_TestC.value` is less than `15` **or** `C_TestD.points` is greater than or equal to `100`.
```gdscript
var result = QueryBuilder.new(world).with_any([
{ C_TestC: { "value": { "_lt": 15 } } },
{ C_TestD: { "points": { "_gte": 100 } } }
]).execute()
```
#### 5. Using `_in` and `_nin` Operators
Retrieve entities where `C_TestC.value` is either `10` or `25`.
```gdscript
var result = QueryBuilder.new(world).with_all([
{ C_TestC: { "value": { "_in": [10, 25] } } }
]).execute()
```
#### 6. Complex Queries
Retrieve entities where:
- `C_TestC.value` is greater than or equal to `25`, and
- `C_TestD.points` is greater than `75` **or** less than `30`, and
- Excludes entities with `C_TestE` component.
```gdscript
var result = QueryBuilder.new(world).with_all([
{ C_TestC: { "value": { "_gte": 25 } } }
]).with_any([
{ C_TestD: { "points": { "_gt": 75 } } },
{ C_TestD: { "points": { "_lt": 30 } } }
]).with_none([C_TestE]).execute()
```
## Important Notes
- **Component Queries with `with_none`**: Component queries are **not supported** with the `with_none` method. This is because querying properties of components that should not exist on the entity doesn't make logical sense. Use `with_none` to exclude entities that have certain components.
```gdscript
# Correct usage of with_none
var result = QueryBuilder.new(world).with_none([C_Inactive]).execute()
```
- **Empty Queries Match All Instances of the Component**
If you provide an empty query dictionary for a component, it will match all entities that have that component, regardless of its properties.
```gdscript
# This will match all entities that have C_TestC component
var result = QueryBuilder.new(world).with_all([
{ C_TestC: {} }
]).execute()
```
- **Non-existent Properties**
If you query a property that doesn't exist on the component, it will not match any entities.
```gdscript
# Assuming 'non_existent' is not a property of C_TestC
var result = QueryBuilder.new(world).with_all([
{ C_TestC: { "non_existent": { "_eq": 10 } } }
]).execute()
# result will be empty
```
## Comprehensive Example
Here's a full example demonstrating several component queries:
```gdscript
# Setting up entities with components
var entity1 = Entity.new()
entity1.add_component(C_TestC.new(25))
entity1.add_component(C_TestD.new(100))
var entity2 = Entity.new()
entity2.add_component(C_TestC.new(10))
entity2.add_component(C_TestD.new(50))
var entity3 = Entity.new()
entity3.add_component(C_TestC.new(25))
entity3.add_component(C_TestD.new(25))
var entity4 = Entity.new()
entity4.add_component(C_TestC.new(30))
world.add_entity(entity1)
world.add_entity(entity2)
world.add_entity(entity3)
world.add_entity(entity4)
# Query: Entities with C_TestC.value == 25 and C_TestD.points > 50
var result = QueryBuilder.new(world).with_all([
{ C_TestC: { "value": { "_eq": 25 } } },
{ C_TestD: { "points": { "_gt": 50 } } }
]).execute()
# result will include entity1
```
## Conclusion
Component Queries extend the querying capabilities of the GECS framework by allowing you to filter entities based on component data. By utilizing the supported operators and combining component queries with traditional component filters, you can precisely target the entities you need for your game's logic.
For more information on how to use the `QueryBuilder`, refer to the `query_builder.gd` documentation and the test cases in `test_query_builder.gd`.

View File

@@ -0,0 +1,699 @@
# GECS Core Concepts Guide
> **Deep understanding of Entity Component System architecture**
This guide explains the fundamental concepts that make GECS powerful. After reading this, you'll understand how to architect games using ECS principles and leverage GECS's unique features.
## 📋 Prerequisites
- Completed [Getting Started Guide](GETTING_STARTED.md)
- Basic GDScript knowledge
- Understanding of Godot's node system
## 🎯 Why ECS?
### The Problem with Traditional OOP
Traditional object-oriented approaches often bundle data and behavior together. Over time, this can become unwieldy and force complicated inheritance structures:
```gdscript
# ❌ Traditional OOP problems
class BaseCharacter:
# Lots of shared code
class Player extends BaseCharacter:
# Player-specific code mixed with shared code
class Enemy extends BaseCharacter:
# Enemy-specific code, some overlap with Player
class Boss extends Enemy:
# Even more inheritance complexity
```
### The ECS Solution
ECS keeps data (components) separate from logic (systems), providing clear organization around three core concepts:
1. **Entities** IDs or "slots" for your game objects
2. **Components** Pure data objects that define state (e.g., velocity, health)
3. **Systems** Logic that processes entities with specific components
This pattern simplifies organization, collaboration, and refactoring. Systems only act upon relevant components. Entities can freely change their makeup without breaking the overall design.
## 🏗️ GECS Architecture
GECS extends standard ECS with Godot-specific features:
- **Integration with Godot nodes** - Entities can be scenes, Components are resources
- **World management** - Central coordination of entities and systems
- **ECS singleton** - Global access point for queries and processing
- **Advanced queries** - Property-based filtering and relationship support
- **Relationship system** - Define complex associations between entities
## 🎭 Entities
### Entity Fundamentals
Entities are the core data containers you work with in GECS. They're Godot nodes extending `Entity.gd` that hold components and relationships.
**Creating Entities in Code:**
```gdscript
# Create entity class with components
class_name MyEntity extends Entity
func define_components() -> Array:
return [C_Transform.new(), C_Velocity.new(Vector3.UP)]
# Use the entity
var e_my_entity = MyEntity.new()
ECS.world.add_entity(e_my_entity)
```
**Entity Prefabs (Recommended):**
Since GECS integrates with Godot, create scenes with Entity root nodes and save as `.tscn` files. These "prefabs" can include child nodes for visualization while maintaining ECS data organization.
```gdscript
# e_player.gd - Entity prefab
class_name Player
extends Entity
func on_ready():
# Sync transform from scene to component
var c_trs = get_component(C_Transform) as C_Transform
if not c_trs:
return
transform_comp.transform = self.global_transform # This works because the TSCN base type is Node3D and we extend Node3D with Entity (Which itself extends from Node)
```
### Entity Lifecycle
Entities have a managed lifecycle:
1. **Initialization** - Entity added to world, components loaded from `component_resources`
2. **define_components()** - Called to add components via code
3. **on_ready()** - Setup initial states, sync transforms
4. **on_destroy()** - Cleanup before removal
5. **on_disable()/on_enable()** - Handle enable/disable states
> **Note:** In GECS v5.0+, entity logic should be handled by Systems, not in entity methods. Entities are pure data containers.
### Entity Naming Conventions
**GECS follows consistent naming patterns throughout the framework:**
- **Class names**: `ClassCase` representing the thing they are
- **File names**: `e_entity_name.gd` using snake_case
**Examples:**
```gdscript
# e_player.gd
class_name Player extends Entity
# e_enemy.gd
class_name Enemy extends Entity
# e_projectile.gd
class_name Projectile extends Entity
# e_pickup_item.gd
class_name PickupItem extends Entity
```
### Entity as Glue Code
Entities can serve as initialization and connection points:
```gdscript
class_name Player
extends Entity
@onready var mesh_instance = $MeshInstance3D
@onready var collision_shape = $CollisionShape3D
func on_ready():
# Connect scene nodes to components
var c_sprite = get_component(C_Sprite)
if c_sprite:
sprite_comp.mesh_instance = mesh_instance
# Sync editor-placed transform to component
var c_trs = get_component(C_Transform)
if c_trs:
transform_comp.transform = self.global_transform
```
## 📦 Components
### Component Fundamentals
Components are pure data containers - they store state but contain no game logic. They can emit signals for reactive systems.
```gdscript
# c_health.gd - Example component
class_name C_Health
extends Component
signal health_changed
## How much total health this entity has
@export var maximum := 100.0
## The current health value
@export var current := 100.0
func _init(max_health: float = 100.0):
maximum = max_health
current = max_health
```
### Component Design Principles
**Data Only:**
```gdscript
# ✅ Good - Pure data
class_name C_Health
extends Component
@export var current: float = 100.0
@export var maximum: float = 100.0
@export var regeneration_rate: float = 1.0
```
**No Game Logic:**
```gdscript
# ❌ Avoid - Logic in components
class_name C_Health
extends Component
@export var current: float = 100.0
func take_damage(amount: float): # This belongs in a system!
current -= amount
if current <= 0:
print("Entity died!")
```
### Component Naming Conventions
**GECS uses a consistent C\_ prefix system:**
- **Class names**: `C_ComponentName` in ClassCase
- **File names**: `c_component_name.gd` in snake_case
- **Organization**: Group by purpose in folders
**Examples:**
```gdscript
# c_health.gd
class_name C_Health extends Component
# c_transform.gd
class_name C_Transform extends Component
# c_velocity.gd
class_name C_Velocity extends Component
# c_user_input.gd
class_name C_UserInput extends Component
# c_sprite_renderer.gd
class_name C_SpriteRenderer extends Component
```
**File Organization:**
```
components/
├── gameplay/
│ ├── c_health.gd
│ ├── c_damage.gd
│ └── c_inventory.gd
├── physics/
│ ├── c_transform.gd
│ ├── c_velocity.gd
│ └── c_collision.gd
└── rendering/
├── c_sprite.gd
└── c_mesh.gd
```
### Adding Components
**Via Editor (Recommended):**
Add to entity's `component_resources` array in Inspector - these auto-load when entity is added to world.
**Via define_components():**
```gdscript
# e_player.gd - Define components programmatically
class_name Player
extends Entity
func define_components() -> Array:
return [
C_Health.new(100),
C_Transform.new(),
C_Input.new()
]
# Via Inspector: Add to component_resources array
# Components automatically loaded when entity added to world
# Dynamic addition (less common):
var entity = Player.new()
entity.add_component(C_StatusEffect.new("poison"))
ECS.world.add_entity(entity)
```
## ⚙️ Systems
### System Fundamentals
Systems contain game logic and process entities based on component queries. They should be small, atomic, and focused on one responsibility.
Systems have two main parts:
- **Query** - Defines which entities to process based on components/relationships
- **Process** - The function that runs on entities
### System Types
**Entity Processing:**
```gdscript
class_name LifetimeSystem
extends System
func query() -> QueryBuilder:
return q.with_all([C_Lifetime])
func process(entities: Array[Entity], components: Array, delta: float):
# Process each entity - all systems use the same signature
for entity in entities:
var c_lifetime = entity.get_component(C_Lifetime) as C_Lifetime
c_lifetime.lifetime -= delta
if c_lifetime.lifetime <= 0:
ECS.world.remove_entity(entity)
```
**Optimized Batch Processing with iterate():**
```gdscript
class_name VelocitySystem
extends System
func query() -> QueryBuilder:
# Use iterate() to get component arrays for faster access
return q.with_all([C_Velocity]).iterate([C_Velocity])
func process(entities: Array[Entity], components: Array, delta: float):
# components[0] contains all C_Velocity components
var velocities = components[0]
for i in entities.size():
# Direct array access is faster than get_component()
var position: Vector3 = entities[i].transform.origin
position += velocities[i].velocity * delta
entities[i].transform.origin = position
```
### Sub-Systems
Group related logic into one system file - all subsystems use the unified signature:
```gdscript
class_name DamageSystem
extends System
func sub_systems():
return [
# [query, callable] - all use same unified process signature
[
q
.with_all([C_Health, C_Damage]),
damage_entities
],
[
q
.with_all([C_Health])
.with_none([C_Dead])
.iterate([C_Health]),
regenerate_health
]
]
func damage_entities(entities: Array[Entity], components: Array, delta: float):
# Process entities with damage
for entity in entities:
var c_health = entity.get_component(C_Health)
var c_damage = entity.get_component(C_Damage)
c_health.current -= c_damage.amount
entity.remove_component(c_damage)
if c_health.current <= 0:
entity.add_component(C_Dead.new())
func regenerate_health(entities: Array[Entity], components: Array, delta: float):
# Batch process using component arrays from iterate()
var healths = components[0]
for i in entities.size():
healths[i].current = min(healths[i].current + 1 * delta, healths[i].maximum)
```
### System Dependencies
Control system execution order with dependencies:
```gdscript
class_name RenderSystem
extends System
func deps() -> Dictionary[int, Array]:
return {
Runs.After: [MovementSystem, TransformSystem], # Run after these
Runs.Before: [UISystem] # Run before this
}
# Special case: run after ALL other systems
class_name TransformSystem
extends System
func deps() -> Dictionary[int, Array]:
return {
Runs.After: [ECS.wildcard] # Runs after everything else
}
```
### System Naming Conventions
- **Class names**: `SystemNameSystem` in ClassCase (TransformSystem, PhysicsSystem)
- **File names**: `s_system_name.gd` (s_transform.gd, s_physics.gd)
### System Lifecycle
Systems follow Godot node lifecycle:
- `setup()` - Initial setup after system is added to world
- `process(entities, components, delta)` - Unified method called each frame for matching entities
- System groups for organized processing order
## 🔍 Query System
### Query Builder
GECS uses a fluent API for building entity queries:
```gdscript
ECS.world.query
.with_all([C_Health, C_Position]) # Must have all these components
.with_any([C_Player, C_Enemy]) # Must have at least one of these
.with_none([C_Dead, C_Disabled]) # Must not have any of these
.with_relationship([r_attacking_player]) # Must have these relationships
.without_relationship([r_fleeing]) # Must not have these relationships
.with_reverse_relationship([r_parent_of]) # Must be target of these relationships
.iterate([C_Health]) # Fetch these components and add to components array for quick iteration
```
### Query Methods
**Basic Query Operations:**
```gdscript
var entities = query.execute() # Get matching entities
var filtered = query.matches(entity_list) # Filter existing list
var combined = query.combine(another_query) # Combine queries
```
### Query Types Explained
**with_all** - Entities must have ALL specified components:
```gdscript
# Find entities that can move and be damaged
q.with_all([C_Position, C_Velocity, C_Health])
```
**with_any** - Entities must have AT LEAST ONE of the components:
```gdscript
# Find players or enemies (anything controllable)
q.with_any([C_Player, C_Enemy])
```
**with_none** - Entities must NOT have any of these components:
```gdscript
# Find living entities (exclude dead/disabled)
q.with_all([C_Health]).with_none([C_Dead, C_Disabled])
```
### Component Property Queries
Query based on component data values:
```gdscript
# Find entities with low health
q.with_all([{C_Health: {"current": {"_lt": 20}}}])
# Find fast-moving entities
q.with_all([{C_Velocity: {"speed": {"_gt": 100}}}])
# Find entities with specific states
q.with_all([{C_State: {"current_state": {"_eq": "attacking"}}}])
```
**Supported Operators:**
- `_eq` - Equal to
- `_ne` - Not equal to
- `_gt` - Greater than
- `_lt` - Less than
- `_gte` - Greater than or equal
- `_lte` - Less than or equal
- `_in` - Value in list
- `_nin` - Value not in list
## 🔗 Relationships
### Relationship Fundamentals
Relationships link entities together for complex associations. They consist of:
- **Source** - Entity that has the relationship
- **Relation** - Component defining the relationship type
- **Target** - Entity or type being related to
```gdscript
# Create relationship components
class_name C_Likes extends Component
class_name C_Loves extends Component
class_name C_Eats extends Component
@export var quantity: int = 1
# Create entities
var e_bob = Entity.new()
var e_alice = Entity.new()
var e_heather = Entity.new()
var e_apple = Food.new()
# Add relationships
e_bob.add_relationship(Relationship.new(C_Likes.new(), e_alice)) # bob likes alice
e_alice.add_relationship(Relationship.new(C_Loves.new(), e_heather)) # alice loves heather
e_heather.add_relationship(Relationship.new(C_Likes.new(), Food)) # heather likes food (type)
e_heather.add_relationship(Relationship.new(C_Eats.new(5), e_apple)) # heather eats 5 apples
```
### Relationship Queries
**Specific Relationships:**
```gdscript
# Any entity that likes alice
ECS.world.query.with_relationship([Relationship.new(C_Likes.new(), e_alice)])
# Any entity that eats 5 apples
ECS.world.query.with_relationship([Relationship.new(C_Eats.new(5), e_apple)])
# Any entity that likes the Food type
ECS.world.query.with_relationship([Relationship.new(C_Likes.new(), Food)])
```
**Wildcard Relationships:**
```gdscript
# Any entity with any relation toward heather
ECS.world.query.with_relationship([Relationship.new(ECS.wildcard, e_heather)])
# Any entity that likes anything
ECS.world.query.with_relationship([Relationship.new(C_Likes.new(), ECS.wildcard)])
# Any entity with any relation to Enemy type
ECS.world.query.with_relationship([Relationship.new(ECS.wildcard, Enemy)])
```
**Reverse Relationships:**
```gdscript
# Find entities that are being liked by someone
ECS.world.query.with_reverse_relationship([Relationship.new(C_Likes.new(), ECS.wildcard)])
```
### Relationship Best Practices
**Reuse Relationship Objects:**
```gdscript
# Reuse for performance
var r_likes_apples = Relationship.new(C_Likes.new(), e_apple)
var r_attacking_players = Relationship.new(C_IsAttacking.new(), Player)
# Consider a static relationships class
class_name Relationships
static func attacking_players():
return Relationship.new(C_IsAttacking.new(), Player)
static func chasing_anything():
return Relationship.new(C_IsChasing.new(), ECS.wildcard)
```
## 🌍 World Management
### World Lifecycle
The World is the central manager for all entities and systems:
```gdscript
# main.gd - Simple scene-based setup
extends Node
@onready var world: World = $World
func _ready():
Bootstrap.bootstrap() # Initialize game-specific setup
ECS.world = world
# Systems are automatically registered via scene composition
# Process systems by groups in order
func _process(delta):
world.process(delta, "run-first") # Initialization
world.process(delta, "input") # Input handling
world.process(delta, "gameplay") # Game logic
world.process(delta, "ui") # UI updates
world.process(delta, "run-last") # Cleanup
func _physics_process(delta):
world.process(delta, "physics") # Physics systems
world.process(delta, "debug") # Debug systems
```
### System Groups and Processing Order
Organize systems using scene-based composition with execution groups:
```
default_systems.tscn Structure:
├── run-first (SystemGroup)
│ ├── VictimInitSystem
│ └── EcsStorageLoad
├── input (SystemGroup)
│ ├── ItemSystem
│ ├── WeaponsSystem
│ └── PlayerControlsSystem
├── gameplay (SystemGroup)
│ ├── GearSystem
│ ├── DeathSystem
│ └── EventSystem
├── physics (SystemGroup)
│ ├── FrictionSystem
│ ├── CharacterBody3DSystem
│ └── TransformSystem
├── ui (SystemGroup)
│ └── UiVisibilitySystem
├── debug (SystemGroup)
│ └── DebugLabel3DSystem
└── run-last (SystemGroup)
├── ActionsSystem
└── PendingDeleteSystem
```
**Scene Setup Benefits:**
- **Visual Organization**: See system hierarchy in Godot editor
- **Easy Reordering**: Drag systems between groups
- **Inspector Configuration**: Set system properties in editor
- **Reusable Scenes**: Share system configurations between projects
## 🔄 Data-Driven Architecture
### Composition Over Inheritance
Build entities by combining simple components rather than complex inheritance:
```gdscript
# ✅ Composition approach in entity definition
class_name Player extends Entity
func define_components() -> Array:
return [
C_Health.new(100),
C_Movement.new(200.0),
C_Input.new(),
C_Inventory.new()
]
# Same components reused for different entity types
enemy.add_component(C_Health.new(50))
enemy.add_component(C_Movement.new(100.0))
enemy.add_component(C_AI.new())
enemy.add_component(C_Sprite.new("enemy.png"))
```
### Modular System Design
Keep systems small and focused:
```gdscript
# ✅ Focused systems
class_name MovementSystem extends System
# Only handles position updates
class_name CollisionSystem extends System
# Only handles collision detection
class_name HealthSystem extends System
# Only handles health changes
```
This ensures:
- **Easier debugging** - Clear separation of concerns
- **Better reusability** - Systems work across different entity types
- **Simplified testing** - Each system can be tested independently
- **Performance optimization** - Systems can be profiled and optimized individually
## 🎯 Next Steps
Now that you understand GECS's core concepts:
1. **Apply these patterns** in your own projects
2. **Experiment with relationships** for complex entity interactions
3. **Design component hierarchies** that support your game's needs
4. **Learn optimization techniques** in [Performance Guide](PERFORMANCE_OPTIMIZATION.md)
5. **Master common patterns** in [Best Practices Guide](BEST_PRACTICES.md)
## 📚 Related Documentation
- **[Getting Started](GETTING_STARTED.md)** - Build your first ECS project
- **[Best Practices](BEST_PRACTICES.md)** - Write maintainable ECS code
- **[Performance Optimization](PERFORMANCE_OPTIMIZATION.md)** - Make your games run fast
- **[Troubleshooting](TROUBLESHOOTING.md)** - Solve common issues
---
_"Understanding ECS is about shifting from 'what things are' to 'what things have' and 'what operates on them.' This separation of data and logic is the key to scalable game architecture."_

View File

@@ -0,0 +1,357 @@
# Debug Viewer
> **Real-time debugging and visualization for your ECS projects**
The GECS Debug Viewer provides live inspection of entities, components, systems, and relationships while your game is running. Perfect for understanding entity behavior, optimizing system performance, and debugging complex interactions.
## 📋 Prerequisites
- GECS plugin enabled in your project
- Debug mode enabled: `Project > Project Settings > GECS > Debug Mode`
- Game running from the editor (F5 or F6)
## 🎯 Quick Start
### Opening the Debug Viewer
1. **Run your game** from the Godot editor (F5 for current scene, F6 for main scene)
2. **Open the debugger panel** (bottom of editor, usually appears automatically)
3. **Click the "GECS" tab** next to "Debugger", "Errors", and "Profiler"
> 💡 **Debug Mode Required**: If you see an overlay saying "Debug mode is disabled", go to `Project > Project Settings > GECS` and enable "Debug Mode"
## 🔍 Features Overview
The debug viewer is split into two main panels:
### Systems Panel (Right)
Monitor system execution and performance in real-time.
**Features:**
- **System execution time** - See how long each system takes to process (milliseconds)
- **Entity count** - Number of entities processed per system
- **Active/Inactive status** - Toggle systems on/off at runtime
- **Sortable columns** - Click column headers to sort by name, time, or status
- **Performance metrics** - Archetype count, parallel processing info
**Status Bar:**
- Total system count
- Combined execution time
- Most expensive system highlighted
### Entities Panel (Left)
Inspect individual entities and their components.
**Features:**
- **Entity hierarchy** - See all entities in your world
- **Component data** - View component properties in real-time (WIP)
- **Relationships** - Visualize entity connections and associations
- **Search/filter** - Find entities or components by name
## 🎮 Using the Debug Viewer
### Monitoring System Performance
**Sort by execution time:**
1. Click the **"Time (ms)"** column header in the Systems panel
2. Systems are now sorted by performance (slowest first by default)
3. Click again to reverse the sort order
**Identify bottlenecks:**
- Look for systems with high execution times (> 5ms)
- Check the entity count - more entities = more processing
- Consider optimization strategies from [Performance Optimization](PERFORMANCE_OPTIMIZATION.md)
**Example:**
```
Name Time (ms) Status
PhysicsSystem 8.234 ms ACTIVE ← Bottleneck!
RenderSystem 2.156 ms ACTIVE
AISystem 0.892 ms ACTIVE
```
### Toggling Systems On/Off
**Disable a system at runtime:**
1. Locate the system in the Systems panel
2. Click on the **Status** column (shows "ACTIVE" or "INACTIVE")
3. System immediately stops processing entities
4. Click again to re-enable
**Use cases:**
- Test game behavior without specific systems
- Isolate bugs by disabling systems one at a time
- Temporarily disable expensive systems during debugging
- Verify system dependencies
> ⚠️ **Important**: System state resets when you restart the game. This is a debugging tool, not a save/load feature.
### Inspecting Entities
**View entity components:**
1. Expand an entity in the Entities panel
2. See all attached components (e.g., `C_Health`, `C_Transform`)
3. Expand a component to view its properties
4. Values update in real-time as your game runs
**Example entity structure:**
```
Entity #123 : /root/World/Player
├── C_Health
│ ├── current: 87.5
│ └── maximum: 100.0
├── C_Transform
│ └── position: (15.2, 0.0, 23.8)
└── C_Velocity
└── velocity: (2.5, 0.0, 1.3)
```
### Viewing Relationships
Relationships show how entities are connected to each other.
**Relationship types displayed:**
- **Entity → Entity**: `Relationship: C_ChildOf -> Entity /root/World/Parent`
- **Entity → Component**: `Relationship: C_Damaged -> C_FireDamage`
- **Entity → Archetype**: `Relationship: C_Buff -> Archetype Player`
- **Entity → Wildcard**: `Relationship: C_Damage -> Wildcard`
**Expand relationships to see:**
- Relation component properties
- Target component properties (for component relationships)
- Full relationship metadata
> 💡 **Learn More**: See [Relationships](RELATIONSHIPS.md) for details on creating and querying entity relationships
### Using Search and Filters
**Systems panel:**
- Type in the "Filter Systems" box to find systems by name
- Only matching systems remain visible
**Entities panel:**
- Type in the "Filter Entities" box to search
- Searches entity names, component names, and property names
- Useful for finding specific entities in large worlds
### Multi-Monitor Setup
**Pop-out window:**
1. Click **"Pop Out"** button at the top of the debug viewer
2. Debug viewer moves to a separate window
3. Position on second monitor for permanent visibility
4. Click **"Pop In"** to return to the editor tab
**Benefits:**
- Keep debug info visible while editing scenes
- Monitor performance during gameplay
- Track entity changes without switching panels
### Collapse/Expand Controls
**Quick controls:**
- **Collapse All** / **Expand All** - Manage all entities at once
- **Systems Collapse All** / **Systems Expand All** - Manage all systems at once
- Individual items can be collapsed/expanded by clicking
## 🔧 Common Workflows
### Performance Optimization Workflow
1. **Sort systems by execution time** (click "Time (ms)" header)
2. **Identify slowest system** (top of sorted list)
3. **Expand system details** to see entity count and archetype count
4. **Review system implementation** for optimization opportunities
5. **Apply optimizations** from [Performance Optimization](PERFORMANCE_OPTIMIZATION.md)
6. **Re-run and compare** execution times
### Debugging Workflow
1. **Identify the problematic entity** using search/filter
2. **Expand entity** to view all components
3. **Watch component values** update in real-time
4. **Toggle related systems off/on** to isolate the issue
5. **Check relationships** if entity interactions are involved
6. **Fix the issue** in your code
### Testing System Dependencies
1. **Run your game** from the editor
2. **Disable systems one at a time** using the Status column
3. **Observe game behavior** for each disabled system
4. **Document dependencies** you discover
5. **Design systems to be more independent** if needed
## 📊 Understanding System Metrics
When you expand a system in the Systems panel, you'll see detailed metrics:
**Execution Time (ms):**
- Time spent in the system's `process()` function
- Lower is better (aim for < 1ms for most systems)
- Spikes indicate performance issues
**Entity Count:**
- Number of entities that matched the system's query
- High counts + high execution time = optimization needed
- Zero entities may indicate query issues
**Archetype Count:**
- Number of unique component combinations processed
- Higher counts can impact performance
- See [Performance Optimization](PERFORMANCE_OPTIMIZATION.md#archetype-optimization)
**Parallel Processing:**
- `true` if system uses parallel iteration
- `false` for sequential processing
- Parallel systems can process entities faster
**Subsystem Info:**
- For multi-subsystem systems (advanced feature)
- Shows entity count per subsystem
## ⚠️ Troubleshooting
### Debug Viewer Shows "Debug mode is disabled"
**Solution:**
1. Go to `Project > Project Settings`
2. Navigate to `GECS` category
3. Enable "Debug Mode" checkbox
4. Restart your game
> 💡 **Performance Note**: Debug mode adds overhead. Disable it for production builds.
### No Entities/Systems Appearing
**Possible causes:**
1. Game isn't running - Press F5 or F6 to run from editor
2. World not created - Verify `ECS.world` exists in your code
3. Entities/Systems not added to world - Check `world.add_child()` calls
### Component Properties Not Updating
**Solution:**
- Component properties update when they change
- Properties without `@export` won't be visible
- Make sure your systems are modifying component properties correctly
### Systems Not Toggling
**Possible causes:**
1. System has `paused` property set - Check system code
2. Debugger connection lost - Restart the game
3. System is critical - Some systems might ignore toggle requests
## 🎯 Best Practices
### During Development
**Do:**
- Keep debug viewer open while testing gameplay
- Sort systems by time regularly to catch performance regressions
- Use entity search to track specific entities
- Disable systems to test game behavior
**Don't:**
- Leave debug mode enabled in production builds
- Rely on system toggling for game logic (use proper activation patterns)
- Expect perfect frame timing (debug mode adds overhead)
### For Performance Tuning
1. **Baseline first**: Run game without debug viewer, note FPS
2. **Enable debug viewer**: Identify expensive systems
3. **Focus on top 3**: Optimize the slowest systems first
4. **Measure impact**: Re-check execution times after changes
5. **Disable debug mode**: Always profile final builds without debug overhead
## 🚀 Advanced Tips
### Custom Component Serialization
If your component properties aren't showing up properly:
```gdscript
# Mark properties with @export for debug visibility
class_name C_CustomData
extends Component
@export var visible_property: int = 0 # ✅ Shows in debug viewer
var hidden_property: int = 0 # ❌ Won't appear
```
### Relationship Debugging
Use the debug viewer to verify complex relationship queries:
1. **Create test entities** with relationships
2. **Check relationship display** in Entities panel
3. **Verify relationship properties** are correct
4. **Test relationship queries** in your systems
### Performance Profiling Workflow
Combine debug viewer with Godot's profiler:
1. **Debug Viewer**: Identify slow ECS systems
2. **Godot Profiler**: Deep-dive into specific functions
3. **Fix bottlenecks**: Optimize based on both tools
4. **Verify improvements**: Check both metrics improve
## 📚 Related Documentation
- **[Core Concepts](CORE_CONCEPTS.md)** - Understanding entities, components, and systems
- **[Performance Optimization](PERFORMANCE_OPTIMIZATION.md)** - Optimize systems identified as bottlenecks
- **[Relationships](RELATIONSHIPS.md)** - Working with entity relationships
- **[Troubleshooting](TROUBLESHOOTING.md)** - Common issues and solutions
## 💡 Summary
The Debug Viewer is your window into the ECS runtime. Use it to:
- 🔍 Monitor system performance and identify bottlenecks
- 🎮 Inspect entities and components in real-time
- 🔗 Visualize relationships between entities
- Toggle systems on/off for debugging
- 📊 Track entity counts and archetype distribution
> **Pro Tip**: Pop out the debug viewer to a second monitor and leave it visible while developing. You'll catch performance issues and bugs much faster!
---
**Next Steps:**
- Learn about [Performance Optimization](PERFORMANCE_OPTIMIZATION.md) to fix bottlenecks you discover
- Explore [Relationships](RELATIONSHIPS.md) to understand entity connections better
- Check [Troubleshooting](TROUBLESHOOTING.md) if you encounter issues

View File

@@ -0,0 +1,341 @@
# Getting Started with GECS
> **Build your first ECS project in 5 minutes**
This guide will walk you through creating a simple player entity with health and transform components using GECS. By the end, you'll understand the core concepts and have a working example.
## 📋 Prerequisites
- Godot 4.x installed
- Basic GDScript knowledge
- 5 minutes of your time
## ⚡ Step 1: Setup (1 minute)
### Install GECS
1. **Download GECS** and place it in your project's `addons/` folder
2. **Enable the plugin**: Go to `Project > Project Settings > Plugins` and enable "GECS"
3. **Verify setup**: The ECS singleton should be automatically added to AutoLoad
> 💡 **Quick Check**: If you see errors, make sure `ECS` appears in `Project > Project Settings > AutoLoad`
## 🎮 Step 2: Your First Entity (2 minutes)
Entities in GECS extend Godot's `Node` class. You have two options for creating entities:
### **Option A: Scene-based Entities** (For spatial properties)
Use this when you need access to `Node3D` or `Node2D` properties like position, rotation, scale, or want to add visual children (sprites, meshes, etc.).
> ⚠️ **Key Point**: `Entity` extends `Node` (not `Node3D` or `Node2D`), so create a scene with the appropriate spatial node type as the root, then attach your entity script to it.
**Steps:**
1. **Create a new scene** in Godot:
- Click `Scene > New Scene` or press `Ctrl+N`
- Select **"Node3D"** as the root node type (for 3D games) or **"Node2D"** (for 2D games)
- Rename the root node to `Player`
2. **Attach the entity script**:
- With the root node selected, click the "Attach Script" button (📄+ icon)
- Save as `e_player.gd`
3. **Save the scene**:
- Save as `e_player.tscn` in your scenes folder
**File: `e_player.gd`**
```gdscript
# e_player.gd
class_name Player
extends Entity
func on_ready():
# Sync the entity's scene position to the Transform component
if has_component(C_Transform):
var c_trs = get_component(C_Transform) as C_Transform
c_trs.position = self.global_position
```
> 💡 **Use case**: Players, enemies, projectiles, or anything that needs a position in your game world.
### **Option B: Code-based Entities** (Pure data containers)
Use this when you DON'T need spatial properties and just want a pure data container (e.g., game managers, abstract systems, timers).
```gdscript
# Just extend Entity directly
class_name GameManager
extends Entity
# No scene needed - instantiate with GameManager.new()
```
> 💡 **Use case**: Game state managers, quest trackers, inventory systems, or any non-spatial game logic.
---
**For this tutorial**, we'll use **Option A** (scene-based) since we want our player to move around the screen with a position.
## 📦 Step 3: Your First Components (1 minute)
Components hold data. Let's create health and transform components:
**File: `c_health.gd`**
```gdscript
# c_health.gd
class_name C_Health
extends Component
@export var current: float = 100.0
@export var maximum: float = 100.0
func _init(max_health: float = 100.0):
maximum = max_health
current = max_health
```
**File: `c_transform.gd`**
```gdscript
# c_transform.gd
class_name C_Transform
extends Component
@export var position: Vector3 = Vector3.ZERO
func _init(pos: Vector3 = Vector3.ZERO):
position = pos
```
**File: `c_velocity.gd`**
```gdscript
# c_velocity.gd
class_name C_Velocity
extends Component
@export var velocity: Vector3 = Vector3.ZERO
func _init(vel: Vector3 = Vector3.ZERO):
velocity = vel
```
> 💡 **Key Principle**: Components only hold data, never logic. Think of them as data containers.
> ⚠️ **Important Note**: Components `_init` function requires that all arguments have a default value or Godot will crash.
## ⚙️ Step 4: Your First System (1 minute)
Systems contain the logic that operates on entities with specific components. This system moves entities across the screen:
**File: `s_movement.gd`**
```gdscript
# s_movement.gd
class_name MovementSystem
extends System
func query():
# Find all entities that have both transform and velocity
return q.with_all([C_Transform, C_Velocity])
func process(entities: Array[Entity], components: Array, delta: float):
# Process each entity in the array
for entity in entities:
var c_trs = entity.get_component(C_Transform) as C_Transform
var c_velocity = entity.get_component(C_Velocity) as C_Velocity
# Move the entity based on its velocity
c_trs.position += c_velocity.velocity * delta
# Update the actual entity position in the scene
entity.global_position = c_trs.position
# Bounce off screen edges (simple example)
if c_trs.position.x > 10 or c_trs.position.x < -10:
c_velocity.velocity.x *= -1
```
> 💡 **System Logic**: Query finds entities with required components, process() runs the movement logic on each entity every frame.
## 🎬 Step 5: See It Work (1 minute)
Now let's put it all together in a main scene:
### Create Main Scene
1. **Create a new scene** with a `Node` as the root
2. **Add a World node** as a child (Add Child Node > search for "World")
3. **Attach this script** to the root node:
**File: `main.gd`**
```gdscript
# main.gd
extends Node
@onready var world: World = $World
func _ready():
ECS.world = world
# Load and instantiate the player entity scene
var player_scene = preload("res://e_player.tscn") # Adjust path as needed
var e_player = player_scene.instantiate() as Player
# Add components to the entity
e_player.add_components([
C_Health.new(100),
C_Transform.new(),
C_Velocity.new(Vector3(2, 0, 0)) # Move right at 2 units/second
])
add_child(e_player) # Add to scene tree
ECS.world.add_entity(e_player) # Add to ECS world
# Create the movement system
var movement_system = MovementSystem.new()
ECS.world.add_system(movement_system)
func _process(delta):
# Process all systems
if ECS.world:
ECS.process(delta)
```
**Run your project!** 🎉 You now have a working ECS setup where the player entity moves across the screen and bounces off the edges! The MovementSystem updates entity positions based on their velocity components.
> 💡 **Scene-based entities**: Notice we load and instantiate the `e_player.tscn` scene instead of calling `Player.new()`. This is required because we need access to spatial properties (position). For entities that don't need spatial properties, `Entity.new()` works fine.
## 🎯 What You Just Built
Congratulations! You've created your first ECS project with:
- **Entity**: Player - a container for components
- **Components**: C_Health, C_Transform, C_Velocity - pure data containers
- **System**: MovementSystem - logic that moves entities based on velocity
- **World**: Container that manages entities and systems
## 📈 Next Steps
Now that you have the basics working, here's how to level up:
### 1. Create Entity Prefabs (Recommended)
Instead of creating entities in code, use Godot's scene system:
1. **Create a new scene** with your Entity class as the root node
2. **Add visual children** (MeshInstance3D, Sprite3D, etc.)
3. **Add components via define_components()** or `component_resources` array in Inspector
4. **Save as .tscn file** (e.g., `e_player.tscn`)
5. **Load and instantiate** in your main scene
```gdscript
# Improved e_player.gd with define_components()
class_name Player
extends Entity
func define_components() -> Array:
return [
C_Health.new(100),
C_Transform.new(),
C_Velocity.new(Vector3(1, 0, 0)) # Move right slowly
]
func on_ready():
# Sync scene position to component
if has_component(C_Transform):
var c_trs = get_component(C_Transform) as C_Transform
c_trs.position = self.global_position
```
### 2. Organize Your Main Scene
Structure your main scene using the proven scene-based pattern:
```
Main.tscn
├── World (World node)
├── DefaultSystems (instantiated from default_systems.tscn)
│ ├── input (SystemGroup)
│ ├── gameplay (SystemGroup)
│ ├── physics (SystemGroup)
│ └── ui (SystemGroup)
├── Level (Node3D for static environment)
└── Entities (Node3D for spawned entities)
```
**Benefits:**
- **Visual organization** in Godot editor
- **Easy system reordering** between groups
- **Reusable system configurations**
### 3. Learn More Patterns
### 🧠 Understand the Concepts
**→ [Core Concepts Guide](CORE_CONCEPTS.md)** - Deep dive into Entities, Components, Systems, and Relationships
### 🔧 Add More Features
Try adding these to your moving player:
- **Input system** - Add C_Input component and system to control movement with arrow keys
- **Multiple entities** - Create more moving objects with different velocities
- **Collision system** - Add C_Collision component and detect when entities hit each other
- **Gravity system** - Add downward velocity to make entities fall
### 📚 Learn Best Practices
**→ [Best Practices Guide](BEST_PRACTICES.md)** - Write maintainable ECS code
### 🔧 Explore Advanced Features
- **[Component Queries](COMPONENT_QUERIES.md)** - Filter by component property values
- **[Relationships](RELATIONSHIPS.md)** - Link entities together for complex interactions
- **[Observers](OBSERVERS.md)** - Reactive systems that respond to changes
- **[Performance Optimization](PERFORMANCE_OPTIMIZATION.md)** - Make your games run fast
## ❓ Having Issues?
### Player not responding?
- Check that `ECS.process(delta)` is called in `_process()`
- Verify components are added to the entity via `define_components()` or Inspector
- Make sure the system is added to the world
- Ensure transform synchronization is called in entity's `on_ready()`
### Can't access position/rotation properties?
- ⚠️ **Entity extends Node, not Node3D**: To access spatial properties, create a scene with `Node3D` (3D) or `Node2D` (2D) as the root node type
- Attach your entity script (that extends `Entity`) to the Node3D/Node2D root
- Load and instantiate the scene file (don't use `.new()` for spatial entities)
- **If you don't need spatial properties**: Using `Entity.new()` is perfectly fine for pure data containers
- See Step 2 for both entity creation approaches
### Errors in console?
- Check that all classes extend the correct base class
- Verify file names match class names
- Ensure GECS plugin is enabled
**Still stuck?** → [Troubleshooting Guide](TROUBLESHOOTING.md)
## 🏆 What's Next?
You're now ready to build amazing games with GECS! The Entity-Component-System pattern will help you:
- **Scale your game** - Add features without breaking existing code
- **Reuse code** - Components and systems work across different entity types
- **Debug easier** - Clear separation between data and logic
- **Optimize performance** - GECS handles efficient querying for you
**Ready to dive deeper?** Start with [Core Concepts](CORE_CONCEPTS.md) to really understand what makes ECS powerful.
**Need help?** [Join our Discord community](https://discord.gg/eB43XU2tmn) for support and discussions.
---
_"The best way to learn ECS is to build with it. Start simple, then add complexity as you understand the patterns."_

View File

@@ -0,0 +1,351 @@
# Observers in GECS
> **Reactive systems that respond to component changes**
Observers provide a reactive programming model where systems automatically respond to component changes, additions, and removals. This allows for decoupled, event-driven game logic.
## 📋 Prerequisites
- Understanding of [Core Concepts](CORE_CONCEPTS.md)
- Familiarity with [Systems](CORE_CONCEPTS.md#systems)
- Observers must be added to the World to function
## 🎯 What are Observers?
Observers are specialized systems that watch for changes to specific components and react immediately when those changes occur. Instead of processing entities every frame, observers only trigger when something actually changes.
**Benefits:**
- **Performance** - Only runs when changes occur, not every frame
- **Decoupling** - Components don't need to know what systems depend on them
- **Reactivity** - Immediate response to state changes
- **Clean Logic** - Separate change-handling logic from regular processing
## 🔧 Observer Structure
Observers extend the `Observer` class and implement key methods:
1. **`watch()`** - Specifies which component to monitor for events (**required** - will crash if not overridden)
2. **`match()`** - Defines a query to filter which entities trigger events (optional - defaults to all entities)
3. **Event Handlers** - Handle specific types of changes
```gdscript
# o_transform.gd
class_name TransformObserver
extends Observer
func watch() -> Resource:
return C_Transform # Watch for transform component changes (REQUIRED)
func on_component_added(entity: Entity, component: Resource):
# Sync component transform to entity when added
var transform_comp = component as C_Transform
entity.global_transform = transform_comp.transform
func on_component_changed(entity: Entity, component: Resource, property: String, new_value: Variant, old_value: Variant):
# Sync component transform to entity when changed
var transform_comp = component as C_Transform
entity.global_transform = transform_comp.transform
```
## 🎮 Observer Event Types
### on_component_added()
Triggered when a watched component is added to an entity:
```gdscript
class_name HealthUIObserver
extends Observer
func watch() -> Resource:
return C_Health
func match():
return q.with_all([C_Health]).with_group("player")
func on_component_added(entity: Entity, component: Resource):
# Create health bar when player gains health component
var health = component as C_Health
# Use call_deferred to avoid timing issues during component changes
call_deferred("create_health_bar", entity, health.maximum)
```
### on_component_changed()
Triggered when a watched component's property changes:
```gdscript
class_name HealthBarObserver
extends Observer
func watch() -> Resource:
return C_Health
func match():
return q.with_all([C_Health]).with_group("player")
func on_component_changed(entity: Entity, component: Resource, property: String, new_value: Variant, old_value: Variant):
if property == "current":
var health = component as C_Health
# Update health bar display
call_deferred("update_health_bar", entity, health.current, health.maximum)
```
### on_component_removed()
Triggered when a watched component is removed from an entity:
```gdscript
class_name HealthUIObserver
extends Observer
func watch() -> Resource:
return C_Health
func on_component_removed(entity: Entity, component: Resource):
# Clean up health bar when health component is removed
call_deferred("remove_health_bar", entity)
```
## 💡 Common Observer Patterns
### Transform Synchronization
Keep entity scene transforms in sync with Transform components:
```gdscript
# o_transform.gd
class_name TransformObserver
extends Observer
func watch() -> Resource:
return C_Transform
func on_component_added(entity: Entity, component: Resource):
var transform_comp = component as C_Transform
entity.global_transform = transform_comp.transform
func on_component_changed(entity: Entity, component: Resource, property: String, new_value: Variant, old_value: Variant):
var transform_comp = component as C_Transform
entity.global_transform = transform_comp.transform
```
### Status Effect Visuals
Show visual feedback for status effects:
```gdscript
# o_status_effects.gd
class_name StatusEffectObserver
extends Observer
func watch() -> Resource:
return C_StatusEffect
func on_component_added(entity: Entity, component: Resource):
var status = component as C_StatusEffect
call_deferred("add_status_visual", entity, status.effect_type)
func on_component_removed(entity: Entity, component: Resource):
var status = component as C_StatusEffect
call_deferred("remove_status_visual", entity, status.effect_type)
func add_status_visual(entity: Entity, effect_type: String):
match effect_type:
"poison":
# Add poison particle effect
pass
"shield":
# Add shield visual overlay
pass
func remove_status_visual(entity: Entity, effect_type: String):
# Remove corresponding visual effect
pass
```
### Audio Feedback
Trigger sound effects on component changes:
```gdscript
# o_audio_feedback.gd
class_name AudioFeedbackObserver
extends Observer
func watch() -> Resource:
return C_Health
func on_component_changed(entity: Entity, component: Resource, property: String, new_value: Variant, old_value: Variant):
if property == "current":
var health_change = new_value - old_value
if health_change < 0:
# Health decreased - play damage sound
call_deferred("play_damage_sound", entity.global_position)
elif health_change > 0:
# Health increased - play heal sound
call_deferred("play_heal_sound", entity.global_position)
```
## 🏗️ Observer Best Practices
### Naming Conventions
**Observer files and classes:**
- **Class names**: `DescriptiveNameObserver` (TransformObserver, HealthUIObserver)
- **File names**: `o_descriptive_name.gd` (o_transform.gd, o_health_ui.gd)
### Use Deferred Calls
Always use `call_deferred()` to defer work and avoid immediate execution during component updates:
```gdscript
# ✅ Good - Defer work for later execution
func on_component_changed(entity: Entity, component: Resource, property: String, new_value: Variant, old_value: Variant):
call_deferred("update_ui_element", entity, new_value)
# ❌ Avoid - Immediate execution can cause issues
func on_component_changed(entity: Entity, component: Resource, property: String, new_value: Variant, old_value: Variant):
update_ui_element(entity, new_value) # May cause timing issues
```
### Keep Observer Logic Simple
Focus observers on single responsibilities:
```gdscript
# ✅ Good - Single purpose observer
class_name HealthUIObserver
extends Observer
func watch() -> Resource:
return C_Health
func on_component_changed(entity: Entity, component: Resource, property: String, new_value: Variant, old_value: Variant):
if property == "current":
call_deferred("update_health_display", entity, new_value)
# ❌ Avoid - Observer doing too much
class_name HealthObserver
extends Observer
func on_component_changed(entity: Entity, component: Resource, property: String, new_value: Variant, old_value: Variant):
# Too many responsibilities in one observer
update_health_display(entity, new_value)
play_damage_sound(entity)
check_achievements(entity)
save_game_state()
```
### Use Specific Queries
Filter which entities trigger observers with `match()`:
```gdscript
# ✅ Good - Specific query
func match():
return q.with_all([C_Health]).with_group("player") # Only player health
# ❌ Avoid - Too broad
func match():
return q.with_all([C_Health]) # ALL entities with health
```
## 🎯 When to Use Observers
**Use Observers for:**
- UI updates based on game state changes
- Audio/visual effects triggered by state changes
- Immediate response to critical state changes (death, level up)
- Synchronization between components and scene nodes
- Event logging and analytics
**Use Regular Systems for:**
- Continuous processing (movement, physics)
- Frame-by-frame updates
- Complex logic that depends on multiple entities
- Performance-critical processing loops
## 🚀 Adding Observers to the World
Observers must be registered with the World to function. There are several ways to do this:
### Manual Registration
```gdscript
# In your scene or main script
func _ready():
var health_observer = HealthUIObserver.new()
ECS.world.add_observer(health_observer)
# Or add multiple observers at once
ECS.world.add_observers([health_observer, transform_observer, audio_observer])
```
### Automatic Scene Tree Registration
Place Observer nodes in your scene under the systems root (default: "Systems" node), and they'll be automatically registered:
```
Main
├── World
├── Systems/ # Observers placed here are auto-registered
│ ├── HealthUIObserver
│ ├── TransformObserver
│ └── AudioFeedbackObserver
└── Entities/
└── Player
```
### Important Notes:
- Observers are initialized with their own QueryBuilder (`observer.q`)
- The `watch()` method is called during registration to validate the component
- Observers must return a valid Component class from `watch()` or they'll crash
## ⚠️ Common Issues & Troubleshooting
### Observer Not Triggering
**Problem**: Observer events never fire
**Solutions**:
- Ensure the observer is added to the World with `add_observer()`
- Check that `watch()` returns the correct component class
- Verify entities match the `match()` query (if defined)
- Component changes must be on properties, not just internal state
### Crash: "You must override the watch() method"
**Problem**: Observer crashes on registration
**Solution**: Override `watch()` method and return a Component class:
```gdscript
func watch() -> Resource:
return C_Health # Must return actual component class
```
### Events Fire for Wrong Entities
**Problem**: Observer triggers for entities you don't want
**Solution**: Use `match()` to filter entities:
```gdscript
func match():
return q.with_all([C_Health]).with_group("player") # Only players
```
### Property Changes Not Detected
**Problem**: Observer doesn't detect component property changes
**Causes**:
- Direct assignment to properties should work automatically
- Internal object modifications (like Array.append()) may not trigger signals
- Manual signal emission required for complex property changes
## 📚 Related Documentation
- **[Core Concepts](CORE_CONCEPTS.md)** - Understanding the ECS fundamentals
- **[Systems](CORE_CONCEPTS.md#systems)** - Regular processing systems
- **[Best Practices](BEST_PRACTICES.md)** - Write maintainable ECS code
---
_"Observers turn your ECS from a polling system into a reactive system, making your game respond intelligently to state changes rather than constantly checking for them."_

View File

@@ -0,0 +1,418 @@
# GECS Performance Optimization Guide
> **Make your ECS games run fast and smooth**
This guide shows you how to optimize your GECS-based games for maximum performance. Learn to identify bottlenecks, optimize queries, and design systems that scale.
## 📋 Prerequisites
- Understanding of [Core Concepts](CORE_CONCEPTS.md)
- Familiarity with [Best Practices](BEST_PRACTICES.md)
- A working GECS project to optimize
## 🎯 Performance Fundamentals
### The ECS Performance Model
GECS performance depends on three key factors:
1. **Query Efficiency** - How fast you find entities
2. **Component Access** - How quickly you read/write data
3. **System Design** - How well your logic is organized
Most performance gains come from optimizing these in order of impact.
## 🔍 Profiling Your Game
### Monitor Query Cache Performance
Always profile before optimizing. GECS provides query cache statistics for performance monitoring:
```gdscript
# Main.gd
func _process(delta):
ECS.process(delta)
# Print cache performance stats every second
if Engine.get_process_frames() % 60 == 0:
var cache_stats = ECS.world.get_cache_stats()
print("ECS Performance:")
print(" Query cache hits: ", cache_stats.get("hits", 0))
print(" Query cache misses: ", cache_stats.get("misses", 0))
print(" Total entities: ", ECS.world.entities.size())
# Reset stats for next measurement period
ECS.world.reset_cache_stats()
```
### Use Godot's Built-in Profiler
Monitor your game's performance in the Godot editor:
1. **Run your project** in debug mode
2. **Open the Profiler** (Debug → Profiler)
3. **Look for ECS-related spikes** in the frame time
4. **Identify the slowest systems** in your processing groups
## ⚡ Query Optimization
### 1. Choose the Right Query Method ⭐ NEW!
**As of v5.0.0-rc4**, query performance ranking (10,000 entities):
1. **`.enabled(true/false)` queries**: **~0.05ms** 🏆 **(Fastest - Use when possible!)**
2. **`.with_all([Components])` queries**: **~0.6ms** 🥈 **(Excellent for most use cases)**
3. **`.with_any([Components])` queries**: **~5.6ms** 🥉 **(Good for OR-style queries)**
4. **`.with_group("name")` queries**: **~16ms** 🐌 **(Avoid for performance-critical code)**
**Performance Recommendations:**
```gdscript
# 🏆 FASTEST - Use enabled/disabled queries when you only need active entities
class_name ActiveSystemsOnly extends System
func query():
return q.enabled(true) # Constant-time O(1) performance!
# 🥈 EXCELLENT - Component-based queries (heavily optimized cache)
class_name MovementSystem extends System
func query():
return q.with_all([C_Position, C_Velocity]) # ~0.6ms for 10K entities
# 🥉 GOOD - Use with_any sparingly, split into multiple systems when possible
class_name DamageableSystem extends System
func query():
return q.with_any([C_Player, C_Enemy]).with_all([C_Health])
# 🐌 AVOID - Group queries are the slowest
class_name PlayerSystem extends System
func query():
return q.with_group("player") # Consider using components instead
# Better: q.with_all([C_Player])
```
### 2. Use Proper System Query Pattern
GECS automatically handles query optimization when you follow the standard pattern:
### 2. Use Proper System Query Pattern
GECS automatically handles query optimization when you follow the standard pattern:
```gdscript
# ✅ Good - Standard GECS pattern (automatically optimized)
class_name MovementSystem extends System
func query():
return q.with_all([C_Position, C_Velocity]).with_none([C_Frozen])
func process(entities: Array[Entity], components: Array, delta: float):
# Process each entity
for entity in entities:
var pos = entity.get_component(C_Position)
var vel = entity.get_component(C_Velocity)
pos.value += vel.value * delta
```
```gdscript
# ❌ Avoid - Manual query building in process methods
func process(entities: Array[Entity], components: Array, delta: float):
# Don't do this - bypasses automatic query optimization
var custom_entities = ECS.world.query.with_all([C_Position]).execute()
# Process custom_entities...
```
### 3. Optimize Query Specificity
More specific queries run faster:
```gdscript
# ✅ Fast - Use enabled filter for active entities only
class_name PlayerInputSystem extends System
func query():
return q.with_all([C_Input, C_Movement]).enabled(true)
# Super fast enabled filtering + component matching
# ✅ Fast - Specific component query
class_name ProjectileSystem extends System
func query():
return q.with_all([C_Projectile, C_Velocity])
# Only matches projectiles - very specific
```
```gdscript
# ❌ Slow - Overly broad query
class_name UniversalSystem extends System
func query():
return q.with_all([C_Position])
# Matches almost everything in the game!
func process(entities: Array[Entity], components: Array, delta: float):
# Now we need expensive type checking in a loop
for entity in entities:
if entity.has_component(C_Player):
# Handle player...
elif entity.has_component(C_Enemy):
# Handle enemy...
# This defeats the purpose of ECS!
```
### 4. Smart Use of with_any Queries
`with_any` queries are **much faster than before** but still slower than `with_all`. Use strategically:
```gdscript
# ✅ Good - with_any for legitimate OR scenarios
class_name DamageSystem extends System
func query():
return q.with_any([C_Player, C_Enemy, C_NPC]).with_all([C_Health])
# When you truly need "any of these types with health"
# ✅ Better - Split when entities have different behavior
class_name PlayerMovementSystem extends System
func query(): return q.with_all([C_Player, C_Movement])
class_name EnemyMovementSystem extends System
func query(): return q.with_all([C_Enemy, C_Movement])
# Split systems = simpler logic + better performance
```
### 5. Avoid Group Queries for Performance-Critical Code
Group queries are now the slowest option. Use component-based queries instead:
```gdscript
# ❌ Slow - Group-based query (~16ms for 10K entities)
class_name PlayerSystem extends System
func query():
return q.with_group("player")
# ✅ Fast - Component-based query (~0.6ms for 10K entities)
class_name PlayerSystem extends System
func query():
return q.with_all([C_Player])
```
## 🧱 Component Design for Performance
### Keep Components Lightweight
Smaller components = faster memory access:
```gdscript
# ✅ Good - Lightweight components
class_name C_Position extends Component
@export var position: Vector2
class_name C_Velocity extends Component
@export var velocity: Vector2
class_name C_Health extends Component
@export var current: float
@export var maximum: float
```
```gdscript
# ❌ Heavy - Bloated component
class_name MegaComponent extends Component
@export var position: Vector2
@export var velocity: Vector2
@export var health: float
@export var mana: float
@export var inventory: Array[Item] = []
@export var abilities: Array[Ability] = []
@export var dialogue_history: Array[String] = []
# Too much data in one place!
```
### Minimize Component Additions/Removals
Adding and removing components requires index updates. Batch component operations when possible:
```gdscript
# ✅ Good - Batch component operations
func setup_new_enemy(entity: Entity):
# Add multiple components in one batch
entity.add_components([
C_Health.new(),
C_Position.new(),
C_Velocity.new(),
C_Enemy.new()
])
# ✅ Good - Single component change when needed
func apply_damage(entity: Entity, damage: float):
var health = entity.get_component(C_Health)
health.current = clamp(health.current - damage, 0, health.maximum)
if health.current <= 0:
entity.add_component(C_Dead.new()) # Single component addition
```
### Choose Between Boolean Properties vs Components Based on Usage
The choice between boolean properties and separate components depends on how frequently states change and how many entities need them.
#### Use Boolean Properties for Frequently-Changing States
When states change often, boolean properties avoid expensive index updates:
```gdscript
# ✅ Good for frequently-changing states (buffs, status effects, etc.)
class_name C_EntityState extends Component
@export var is_stunned: bool = false
@export var is_invisible: bool = false
@export var is_invulnerable: bool = false
class_name MovementSystem extends System
func query():
return q.with_all([C_Position, C_Velocity, C_EntityState])
# All entities that might need states must have this component
func process(entity: Entity, delta: float):
var state = entity.get_component(C_EntityState)
if state.is_stunned:
return # Just a property check - no index updates
# Process movement...
```
**Tradeoffs:**
- ✅ Fast state changes (no index rebuilds)
- ✅ Simple property checks in systems
- ❌ All entities need the state component (memory overhead)
- ❌ Less precise queries (can't easily find "only stunned entities")
#### Use Separate Components for Rare or Permanent States
When states are long-lasting or infrequent, separate components provide precise queries:
```gdscript
# ✅ Good for rare/permanent states (player vs enemy, permanent abilities)
class_name MovementSystem extends System
func query():
return q.with_all([C_Position, C_Velocity]).with_none([C_Paralyzed])
# Precise query - only entities that can move
# Separate systems can target specific states precisely
class_name ParalyzedSystem extends System
func query():
return q.with_all([C_Paralyzed]) # Only paralyzed entities
```
**Tradeoffs:**
- ✅ Memory efficient (only entities with states have components)
- ✅ Precise queries for specific states
- ❌ State changes trigger expensive index updates
- ❌ Complex queries with multiple exclusions
#### Guidelines:
- **High-frequency changes** (every few frames): Use boolean properties
- **Low-frequency changes** (minutes apart): Use separate components
- **Related states** (buffs/debuffs): Group into property components
- **Distinct entity types** (player/enemy): Use separate components
## ⚙️ System Performance Patterns
### Early Exit Strategies
Return early when no processing is needed:
```gdscript
class_name HealthRegenerationSystem extends System
func process(entities: Array[Entity], components: Array, delta: float):
for entity in entities:
var health = entity.get_component(C_Health)
# Early exits for common cases
if health.current >= health.maximum:
continue # Already at full health
if health.regeneration_rate <= 0:
continue # No regeneration configured
# Only do expensive work when needed
health.current = min(health.current + health.regeneration_rate * delta, health.maximum)
```
### Batch Entity Operations
Group entity operations together:
```gdscript
# ✅ Good - Batch creation
func spawn_enemy_wave():
var enemies: Array[Entity] = []
# Create all entities using entity pooling
for i in range(50):
var enemy = ECS.world.create_entity() # Uses entity pool for performance
setup_enemy_components(enemy)
enemies.append(enemy)
# Add all to world at once
ECS.world.add_entities(enemies)
# ✅ Good - Individual removal (batch removal not available)
func cleanup_dead_entities():
var dead_entities = ECS.world.query.with_all([C_Dead]).execute()
for entity in dead_entities:
ECS.world.remove_entity(entity) # Remove individually
```
## 📊 Performance Targets
### Frame Rate Targets
Aim for these processing times per frame:
- **60 FPS target**: ECS processing < 16ms per frame
- **30 FPS target**: ECS processing < 33ms per frame
- **Mobile target**: ECS processing < 8ms per frame
### Entity Scale Guidelines
GECS handles these entity counts well with proper optimization:
- **Small games**: 100-500 entities
- **Medium games**: 500-2000 entities
- **Large games**: 2000-10000 entities
- **Massive games**: 10000+ entities (requires advanced optimization)
## 🎯 Next Steps
1. **Profile your current game** to establish baseline performance
2. **Apply query optimizations** from this guide
3. **Redesign heavy components** into lighter, focused ones
4. **Implement system improvements** like early exits and batching
5. **Consider advanced techniques** like pooling and spatial partitioning for demanding scenarios
## 🔍 Additional Performance Features
### Entity Pooling
GECS includes built-in entity pooling for optimal performance:
```gdscript
# Use the entity pool for frequent entity creation/destruction
var new_entity = ECS.world.create_entity() # Gets from pool when available
```
### Query Cache Statistics
Monitor query performance with built-in cache tracking:
```gdscript
# Get detailed cache performance data
var stats = ECS.world.get_cache_stats()
print("Cache hit rate: ", stats.get("hits", 0) / (stats.get("hits", 0) + stats.get("misses", 1)))
```
**Need more help?** Check the [Troubleshooting Guide](TROUBLESHOOTING.md) for specific performance issues.
---
_"Fast ECS code isn't about clever tricks - it's about designing systems that naturally align with how the framework works best."_

View File

@@ -0,0 +1,216 @@
# GECS Performance Testing Guide
> **Framework-level performance testing for GECS developers**
This document explains how to run and interpret the GECS performance tests. This is primarily for framework developers and contributors who need to ensure GECS maintains high performance.
**For game developers:** See [Performance Optimization Guide](PERFORMANCE_OPTIMIZATION.md) for optimizing your games.
## 📋 Prerequisites
- GECS framework development environment
- gdUnit4 testing framework
- Godot 4.x
- Test system dependencies: `s_performance_test.gd` and `s_complex_performance_test.gd` in tests/systems/
## 🎯 Overview
The GECS performance test suite provides comprehensive benchmarking for all critical ECS operations:
- **Entity Operations**: Creation, destruction, world management
- **Component Operations**: Addition, removal, lookup, indexing
- **Query Performance**: All query types, caching, complex scenarios
- **System Processing**: Single/multiple systems, different scales
- **Array Operations**: Optimized set operations (intersect, union, difference)
- **Integration Tests**: Realistic game scenarios and stress tests
## 🚀 Running Performance Tests
### Prerequisites
Set the `GODOT_BIN` environment variable to your Godot executable:
```bash
# Windows
setx GODOT_BIN "C:\path\to\godot.exe"
# Linux/Mac
export GODOT_BIN="/path/to/godot"
```
### Running Individual Test Suites
```bash
# Entity performance tests
addons/gdUnit4/runtest.cmd -a res://addons/gecs/tests/performance/performance_test_entities.gd
# Component performance tests
addons/gdUnit4/runtest.cmd -a res://addons/gecs/tests/performance/performance_test_components.gd
# Query performance tests
addons/gdUnit4/runtest.cmd -a res://addons/gecs/tests/performance/performance_test_queries.gd
# System performance tests
addons/gdUnit4/runtest.cmd -a res://addons/gecs/tests/performance/performance_test_systems.gd
# Array operations performance tests
addons/gdUnit4/runtest.cmd -a res://addons/gecs/tests/performance/performance_test_arrays.gd
# Integration performance tests
addons/gdUnit4/runtest.cmd -a res://addons/gecs/tests/performance/performance_test_integration.gd
```
### Running Complete Performance Suite
```bash
# Run all performance tests with comprehensive reporting
addons/gdUnit4/runtest.cmd -a res://addons/gecs/tests/performance/performance_test_master.gd
# Quick smoke test to verify basic performance
addons/gdUnit4/runtest.cmd -a res://addons/gecs/tests/performance/performance_test_master.gd::test_performance_smoke_test
```
## 📊 Test Scales
The performance tests use three different scales:
- **SMALL_SCALE**: 100 entities (for fine-grained testing)
- **MEDIUM_SCALE**: 1,000 entities (for typical game scenarios)
- **LARGE_SCALE**: 10,000 entities (for stress testing)
## ⏱️ Performance Thresholds
The tests include automatic performance threshold checking:
### Entity Operations
- Create 100 entities: < 10ms
- Create 1,000 entities: < 50ms
- Add 1,000 entities to world: < 100ms
### Component Operations
- Add components to 100 entities: < 10ms
- Add components to 1,000 entities: < 75ms
- Component lookup in 1,000 entities: < 30ms
### Query Performance
- Simple query on 100 entities: < 5ms
- Simple query on 1,000 entities: < 20ms
- Simple query on 10,000 entities: < 100ms
- Complex queries: < 50ms
### System Processing
- Process 100 entities: < 5ms
- Process 1,000 entities: < 30ms
- Process 10,000 entities: < 150ms
### Game Loop Performance
- Realistic game frame (1,000 entities): < 16ms (60 FPS target)
## 📈 Understanding Results
### Performance Metrics
Each test provides:
- **Average Time**: Mean execution time across multiple runs
- **Min/Max Time**: Best and worst execution times
- **Standard Deviation**: Consistency of performance
- **Operations/Second**: Throughput measurement
- **Time/Operation**: Per-item processing time
### Result Files
Performance results are saved to `res://reports/` with timestamps:
- `entity_performance_results.json`
- `component_performance_results.json`
- `query_performance_results.json`
- `system_performance_results.json`
- `array_performance_results.json`
- `integration_performance_results.json`
- `complete_performance_results_[timestamp].json`
### Interpreting Results
**Good Performance Indicators:**
- High operations/second (>10,000 for simple operations)
- ✅ Low standard deviation (consistent performance)
- ✅ Linear scaling with entity count
- ✅ Query cache hit rates >80%
**Performance Warning Signs:**
- ⚠️ Tests taking >50ms consistently
- ⚠️ Exponential time scaling with entity count
- ⚠️ High standard deviation (inconsistent performance)
- ⚠️ Cache hit rates <50%
## 🔄 Regression Testing
To monitor performance over time:
1. **Establish Baseline**: Run the complete test suite and save results
2. **Regular Testing**: Run tests after significant changes
3. **Compare Results**: Use the master test suite's regression checking
4. **Set Alerts**: Monitor for >20% performance degradation
## 🎯 Optimization Areas
Based on test results, focus optimization efforts on:
1. **Query Performance**: Most critical for gameplay
2. **Component Operations**: High frequency operations
3. **Array Operations**: Core performance building blocks
4. **System Processing**: Frame-rate critical
5. **Memory Usage**: Large-scale scenarios
## ⚠️ Common Issues
### Missing Dependencies
If tests fail with missing class errors, ensure these files exist:
- `addons/gecs/tests/systems/s_performance_test.gd`
- `addons/gecs/tests/systems/s_complex_performance_test.gd`
### gdUnit4 Setup
Beyond setting `GODOT_BIN`, ensure:
- gdUnit4 plugin is enabled in project settings
- All test component classes are properly defined
## 🔧 Custom Performance Tests
To create custom performance tests:
1. Extend `PerformanceTestBase`
2. Use the `benchmark()` method for timing
3. Set appropriate performance thresholds
4. Include in the master test suite
Example:
```gdscript
extends PerformanceTestBase
func test_my_custom_operation():
var my_test = func():
# Your operation here
pass
benchmark("My_Custom_Test", my_test)
assert_performance_threshold("My_Custom_Test", 10.0, "Custom operation too slow")
```
## 📚 Related Documentation
- **[Performance Optimization](PERFORMANCE_OPTIMIZATION.md)** - User-focused optimization guide
- **[Best Practices](BEST_PRACTICES.md)** - Write performant ECS code
- **[Core Concepts](CORE_CONCEPTS.md)** - Understanding the ECS architecture
---
_This performance testing framework ensures GECS maintains high performance as the codebase evolves. It's a critical tool for framework development and optimization efforts._

View File

@@ -0,0 +1,893 @@
# Relationships in GECS
> **Link entities together for complex game interactions**
Relationships allow you to connect entities in meaningful ways, creating dynamic associations that go beyond simple component data. This guide shows you how to use GECS's relationship system to build complex game mechanics.
## 📋 Prerequisites
- Understanding of [Core Concepts](CORE_CONCEPTS.md)
- Familiarity with [Query System](CORE_CONCEPTS.md#query-system)
## 🔗 What are Relationships?
Think of **components** as the data that makes up an entity's state, and **relationships** as the links that connect entities to other entities, components, or types. Relationships can be simple links or carry data about the connection itself.
In GECS, relationships consist of three parts:
- **Source** - Entity that has the relationship (e.g., Bob)
- **Relation** - Component defining the relationship type (e.g., "Likes", "Damaged")
- **Target** - What is being related to: Entity, Component instance, or archetype (e.g., Alice, FireDamage component, Enemy class)
## 🎯 Relationship Types
GECS supports three powerful relationship patterns:
### 1. **Entity Relationships**
Link entities to other entities:
```gdscript
# Bob likes Alice (entity to entity)
e_bob.add_relationship(Relationship.new(C_Likes.new(), e_alice))
```
### 2. **Component Relationships**
Link entities to component instances for type hierarchies:
```gdscript
# Entity has fire damage (entity to component)
entity.add_relationship(Relationship.new(C_Damaged.new(), C_FireDamage.new(50)))
```
### 3. **Archetype Relationships**
Link entities to classes/types:
```gdscript
# Heather likes all food (entity to type)
e_heather.add_relationship(Relationship.new(C_Likes.new(), Food))
```
This creates powerful queries like "find all entities that like Alice", "find all entities with fire damage", or "find all entities damaged by anything".
## 🎯 Core Relationship Concepts
### Relationship Components
Relationships use components to define their type and can carry data:
```gdscript
# c_likes.gd - Simple relationship
class_name C_Likes
extends Component
# c_loves.gd - Another simple relationship
class_name C_Loves
extends Component
# c_eats.gd - Relationship with data
class_name C_Eats
extends Component
@export var quantity: int = 1
func _init(qty: int = 1):
quantity = qty
```
### Creating Relationships
```gdscript
# Create entities
var e_bob = Entity.new()
var e_alice = Entity.new()
var e_heather = Entity.new()
var e_apple = Food.new()
# Add to world
ECS.world.add_entity(e_bob)
ECS.world.add_entity(e_alice)
ECS.world.add_entity(e_heather)
ECS.world.add_entity(e_apple)
# Create relationships
e_bob.add_relationship(Relationship.new(C_Likes.new(), e_alice)) # bob likes alice
e_alice.add_relationship(Relationship.new(C_Loves.new(), e_heather)) # alice loves heather
e_heather.add_relationship(Relationship.new(C_Likes.new(), Food)) # heather likes food (type)
e_heather.add_relationship(Relationship.new(C_Eats.new(5), e_apple)) # heather eats 5 apples
# Remove relationships
e_alice.remove_relationship(Relationship.new(C_Loves.new(), e_heather)) # alice no longer loves heather
# Remove with limits (NEW)
e_player.remove_relationship(Relationship.new(C_Poison.new(), null), 1) # Remove only 1 poison stack
e_enemy.remove_relationship(Relationship.new(C_Buff.new(), null), 3) # Remove up to 3 buffs
e_hero.remove_relationship(Relationship.new(C_Damage.new(), null), -1) # Remove all damage (default)
```
## 🔍 Relationship Queries
### Basic Relationship Queries
**Query for Specific Relationships:**
```gdscript
# Any entity that likes alice (type matching)
ECS.world.query.with_relationship([Relationship.new(C_Likes.new(), e_alice)])
# Any entity that eats apples (type matching)
ECS.world.query.with_relationship([Relationship.new(C_Eats.new(), e_apple)])
# Any entity that eats 5 or more apples (component query)
ECS.world.query.with_relationship([
Relationship.new({C_Eats: {'quantity': {"_gte": 5}}}, e_apple)
])
# Any entity that likes the Food entity type
ECS.world.query.with_relationship([Relationship.new(C_Likes.new(), Food)])
```
**Exclude Relationships:**
```gdscript
# Entities with any relation toward heather that don't like bob
ECS.world.query
.with_relationship([Relationship.new(ECS.wildcard, e_heather)])
.without_relationship([Relationship.new(C_Likes.new(), e_bob)])
```
### Wildcard Relationships
Use `ECS.wildcard` (or `null`) to query for any relation or target:
```gdscript
# Any entity with any relation toward heather
ECS.world.query.with_relationship([Relationship.new(ECS.wildcard, e_heather)])
# Any entity that likes anything
ECS.world.query.with_relationship([Relationship.new(C_Likes.new(), ECS.wildcard)])
ECS.world.query.with_relationship([Relationship.new(C_Likes.new())]) # Omitting target = wildcard
# Any entity with any relation to Enemy entity type
ECS.world.query.with_relationship([Relationship.new(ECS.wildcard, Enemy)])
```
### Component-Based Relationships
Link entities to **component instances** for powerful type hierarchies and data systems:
```gdscript
# Damage system using component targets
class_name C_Damaged extends Component
class_name C_FireDamage extends Component
@export var amount: int = 0
func _init(dmg: int = 0): amount = dmg
class_name C_PoisonDamage extends Component
@export var amount: int = 0
func _init(dmg: int = 0): amount = dmg
# Entity has multiple damage types
entity.add_relationship(Relationship.new(C_Damaged.new(), C_FireDamage.new(50)))
entity.add_relationship(Relationship.new(C_Damaged.new(), C_PoisonDamage.new(25)))
# Query for entities with any damage type (wildcard)
var damaged_entities = ECS.world.query.with_relationship([
Relationship.new(C_Damaged.new(), null)
]).execute()
# Query for entities with fire damage >= 50 using component query
var high_fire_damaged = ECS.world.query.with_relationship([
Relationship.new(C_Damaged.new(), {C_FireDamage: {"amount": {"_gte": 50}}})
]).execute()
# Query for entities with any fire damage (type matching)
var any_fire_damaged = ECS.world.query.with_relationship([
Relationship.new(C_Damaged.new(), C_FireDamage)
]).execute()
```
### Matching Modes
GECS relationships support two matching modes:
#### Type Matching (Default)
Matches relationships by component type, ignoring property values:
```gdscript
# Matches any C_Damaged relationship regardless of amount
entity.has_relationship(Relationship.new(C_Damaged.new(), target))
# Matches any fire damage effect by type
entity.has_relationship(Relationship.new(C_Damaged.new(), C_FireDamage.new()))
# Query for any entities with fire damage (type matching)
var any_fire_damaged = ECS.world.query.with_relationship([
Relationship.new(C_Damaged.new(), C_FireDamage)
]).execute()
```
#### Component Query Matching
Match relationships by specific property criteria using dictionaries:
```gdscript
# Match C_Damaged relationships where amount >= 50
var high_damage = ECS.world.query.with_relationship([
Relationship.new({C_Damaged: {'amount': {"_gte": 50}}}, target)
]).execute()
# Match fire damage with specific duration
var lasting_fire = ECS.world.query.with_relationship([
Relationship.new(
C_Damaged.new(),
{C_FireDamage: {'duration': {"_gt": 5.0}}}
)
]).execute()
# Match both relation AND target with queries
var strong_buffs = ECS.world.query.with_relationship([
Relationship.new(
{C_Buff: {'duration': {"_gt": 10}}},
{C_Player: {'level': {"_gte": 5}}}
)
]).execute()
```
**When to Use Each:**
- **Type Matching**: Find entities with "any fire damage", "any buff of this type"
- **Component Queries**: Find entities with exact damage amounts, specific buff durations, or property criteria
### Component Queries in Relationships
Query relationships by specific property values using dictionaries:
```gdscript
# Query by relation property
var heavy_eaters = ECS.world.query.with_relationship([
Relationship.new({C_Eats: {'amount': {"_gte": 5}}}, e_apple)
]).execute()
# Query by target component property
var high_hp_targets = ECS.world.query.with_relationship([
Relationship.new(C_Targeting.new(), {C_Health: {'hp': {"_gte": 100}}})
]).execute()
# Query operators: _eq, _ne, _gt, _lt, _gte, _lte, _in, _nin, func
var special_damage = ECS.world.query.with_relationship([
Relationship.new(
{C_Damage: {'type': {"_in": ["fire", "ice"]}}},
null
)
]).execute()
# Complex multi-property queries
var critical_effects = ECS.world.query.with_relationship([
Relationship.new(
{C_Effect: {
'damage': {"_gt": 20},
'duration': {"_gte": 10.0},
'type': {"_eq": "critical"}
}},
null
)
]).execute()
```
### Reverse Relationships
Find entities that are the **target** of relationships:
```gdscript
# Find entities that are being liked by someone
ECS.world.query.with_reverse_relationship([Relationship.new(C_Likes.new(), ECS.wildcard)])
# Find entities being attacked
ECS.world.query.with_reverse_relationship([Relationship.new(C_IsAttacking.new())])
# Find food being eaten
ECS.world.query.with_reverse_relationship([Relationship.new(C_Eats.new(), ECS.wildcard)])
```
## 🎛️ Limited Relationship Removal
> **Control exactly how many relationships to remove for fine-grained management**
The `remove_relationship()` method now supports a **limit parameter** that allows you to control exactly how many matching relationships to remove. This is essential for stack-based systems, partial healing, inventory management, and fine-grained effect control.
### Basic Syntax
```gdscript
entity.remove_relationship(relationship, limit)
```
**Limit Values:**
- `limit = -1` (default): Remove **all** matching relationships
- `limit = 0`: Remove **no** relationships (useful for testing/validation)
- `limit = 1`: Remove **one** matching relationship
- `limit > 1`: Remove **up to that many** matching relationships
### Core Use Cases
#### 1. **Stack-Based Systems**
Perfect for buff/debuff stacks, damage over time effects, or any system where effects can stack:
```gdscript
# Poison stack system
class_name C_PoisonStack extends Component
@export var damage_per_tick: float = 5.0
# Apply poison stacks
entity.add_relationship(Relationship.new(C_PoisonStack.new(3.0), null))
entity.add_relationship(Relationship.new(C_PoisonStack.new(3.0), null))
entity.add_relationship(Relationship.new(C_PoisonStack.new(3.0), null))
entity.add_relationship(Relationship.new(C_PoisonStack.new(3.0), null)) # 4 poison stacks
# Antidote removes 2 poison stacks
entity.remove_relationship(Relationship.new(C_PoisonStack.new(), null), 2)
# Entity now has 2 poison stacks remaining
# Strong antidote removes all poison
entity.remove_relationship(Relationship.new(C_PoisonStack.new(), null)) # Default: remove all
```
#### 2. **Partial Healing Systems**
Control damage removal for gradual healing or partial repair:
```gdscript
# Multiple damage sources on entity
entity.add_relationship(Relationship.new(C_Damage.new(), C_FireDamage.new(25)))
entity.add_relationship(Relationship.new(C_Damage.new(), C_FireDamage.new(15)))
entity.add_relationship(Relationship.new(C_Damage.new(), C_SlashDamage.new(30)))
entity.add_relationship(Relationship.new(C_Damage.new(), C_PoisonDamage.new(10)))
# Healing potion removes one damage source
entity.remove_relationship(Relationship.new(C_Damage.new(), null), 1)
# Fire resistance removes only fire damage (up to 2 sources)
entity.remove_relationship(Relationship.new(C_Damage.new(), C_FireDamage), 2)
# Full heal removes all damage
entity.remove_relationship(Relationship.new(C_Damage.new(), null)) # All damage gone
```
#### 3. **Inventory and Resource Management**
Handle item stacks, resource consumption, and crafting materials:
```gdscript
# Item stack system
class_name C_HasItem extends Component
class_name C_HealthPotion extends Component
@export var healing_amount: int = 50
# Player has multiple health potions
entity.add_relationship(Relationship.new(C_HasItem.new(), C_HealthPotion.new(50)))
entity.add_relationship(Relationship.new(C_HasItem.new(), C_HealthPotion.new(50)))
entity.add_relationship(Relationship.new(C_HasItem.new(), C_HealthPotion.new(50)))
entity.add_relationship(Relationship.new(C_HasItem.new(), C_HealthPotion.new(50)))
# Use one health potion
entity.remove_relationship(Relationship.new(C_HasItem.new(), C_HealthPotion), 1)
# Vendor buys 2 health potions
entity.remove_relationship(Relationship.new(C_HasItem.new(), C_HealthPotion), 2)
# Drop all potions
entity.remove_relationship(Relationship.new(C_HasItem.new(), C_HealthPotion))
```
#### 4. **Buff/Debuff Management**
Fine-grained control over temporary effects:
```gdscript
# Multiple speed buffs from different sources
entity.add_relationship(Relationship.new(C_Buff.new(), C_SpeedBuff.new(1.2, 10.0))) # Boots
entity.add_relationship(Relationship.new(C_Buff.new(), C_SpeedBuff.new(1.5, 5.0))) # Spell
entity.add_relationship(Relationship.new(C_Buff.new(), C_SpeedBuff.new(1.1, 30.0))) # Passive
# Dispel magic removes one buff
entity.remove_relationship(Relationship.new(C_Buff.new(), null), 1)
# Mass dispel removes up to 3 buffs
entity.remove_relationship(Relationship.new(C_Buff.new(), null), 3)
# Purge removes all buffs
entity.remove_relationship(Relationship.new(C_Buff.new(), null))
```
### Advanced Examples
#### Component Query + Limit Combination
Combine component queries with limits for precise control:
```gdscript
# Remove only high-damage effects (damage > 20), up to 2 of them
entity.remove_relationship(
Relationship.new({C_Damage: {"amount": {"_gt": 20}}}, null),
2
)
# Remove poison effects with duration < 5 seconds, limit to 1
entity.remove_relationship(
Relationship.new({C_PoisonEffect: {"duration": {"_lt": 5.0}}}, null),
1
)
# Remove fire damage with specific amount range, up to 3 instances
entity.remove_relationship(
Relationship.new(
C_Damage.new(),
{C_FireDamage: {"amount": {"_gte": 10, "_lte": 50}}}
),
3
)
# Remove all fire damage regardless of amount (no limit, type matching)
entity.remove_relationship(
Relationship.new(C_Damage.new(), C_FireDamage),
-1
)
# Remove buffs with specific multiplier, limit to 2
entity.remove_relationship(
Relationship.new({C_Buff: {"multiplier": {"_gte": 1.5}}}, null),
2
)
```
#### System Integration
Integrate limited removal into your game systems:
```gdscript
class_name HealingSystem extends System
func heal_entity(entity: Entity, healing_power: int):
"""Remove damage based on healing power"""
if healing_power <= 0:
return
# Partial healing - remove damage effects based on healing power
var damage_to_remove = min(healing_power, get_damage_count(entity))
entity.remove_relationship(Relationship.new(C_Damage.new(), null), damage_to_remove)
print("Healed ", damage_to_remove, " damage effects")
func get_damage_count(entity: Entity) -> int:
return entity.get_relationships(Relationship.new(C_Damage.new(), null)).size()
class_name CleanseSystem extends System
func cleanse_entity(entity: Entity, cleanse_strength: int):
"""Remove debuffs based on cleanse strength"""
match cleanse_strength:
1: # Weak cleanse
entity.remove_relationship(Relationship.new(C_Debuff.new(), null), 1)
2: # Medium cleanse
entity.remove_relationship(Relationship.new(C_Debuff.new(), null), 3)
3: # Strong cleanse
entity.remove_relationship(Relationship.new(C_Debuff.new(), null)) # All debuffs
class_name CraftingSystem extends System
func consume_materials(entity: Entity, recipe: Dictionary):
"""Consume specific amounts of crafting materials"""
for material_type in recipe:
var amount_needed = recipe[material_type]
entity.remove_relationship(
Relationship.new(C_HasMaterial.new(), material_type),
amount_needed
)
```
### Error Handling and Validation
The limit parameter provides built-in safeguards:
```gdscript
# Safe operations - won't crash if fewer relationships exist than requested
entity.remove_relationship(Relationship.new(C_Buff.new(), null), 100) # Removes all available, won't error
# Validation operations
entity.remove_relationship(Relationship.new(C_Damage.new(), null), 0) # Removes nothing - useful for testing
# Check before removal
var damage_count = entity.get_relationships(Relationship.new(C_Damage.new(), null)).size()
if damage_count > 0:
entity.remove_relationship(Relationship.new(C_Damage.new(), null), min(3, damage_count))
```
### Performance Considerations
Limited removal is optimized for efficiency:
```gdscript
# ✅ Efficient - stops searching after finding enough matches
entity.remove_relationship(Relationship.new(C_Effect.new(), null), 5)
# ✅ Still efficient - reuses the same removal logic
entity.remove_relationship(Relationship.new(C_Effect.new(), null), -1) # Remove all
# ✅ Most efficient for single removals
entity.remove_relationship(Relationship.new(C_SpecificEffect.new(exact_data), target), 1)
```
### Integration with Multiple Relationships
Works seamlessly with `remove_relationships()` for batch operations:
```gdscript
# Apply limit to multiple relationship types
var relationships_to_remove = [
Relationship.new(C_Buff.new(), null),
Relationship.new(C_Debuff.new(), null),
Relationship.new(C_TemporaryEffect.new(), null)
]
# Remove up to 2 of each type
entity.remove_relationships(relationships_to_remove, 2)
```
## 🎮 Game Examples
### Status Effect System with Component Relationships
This example shows how to build a flexible status effect system using component-based relationships:
```gdscript
# Status effect marker
class_name C_HasEffect extends Component
# Damage type components
class_name C_FireDamage extends Component
@export var damage_per_second: float = 10.0
@export var duration: float = 5.0
func _init(dps: float = 10.0, dur: float = 5.0):
damage_per_second = dps
duration = dur
class_name C_PoisonDamage extends Component
@export var damage_per_tick: float = 5.0
@export var ticks_remaining: int = 10
func _init(dpt: float = 5.0, ticks: int = 10):
damage_per_tick = dpt
ticks_remaining = ticks
# Buff type components
class_name C_SpeedBuff extends Component
@export var multiplier: float = 1.5
@export var duration: float = 10.0
func _init(mult: float = 1.5, dur: float = 10.0):
multiplier = mult
duration = dur
class_name C_StrengthBuff extends Component
@export var bonus_damage: float = 25.0
@export var duration: float = 8.0
func _init(bonus: float = 25.0, dur: float = 8.0):
bonus_damage = bonus
duration = dur
# Apply various effects to entities
func apply_status_effects():
# Player gets fire damage and speed buff
player.add_relationship(Relationship.new(C_HasEffect.new(), C_FireDamage.new(15.0, 8.0)))
player.add_relationship(Relationship.new(C_HasEffect.new(), C_SpeedBuff.new(2.0, 12.0)))
# Enemy gets poison and strength buff
enemy.add_relationship(Relationship.new(C_HasEffect.new(), C_PoisonDamage.new(8.0, 15)))
enemy.add_relationship(Relationship.new(C_HasEffect.new(), C_StrengthBuff.new(30.0, 10.0)))
# Status effect processing system
class_name StatusEffectSystem extends System
func query():
# Get all entities with any status effects
return ECS.world.query.with_relationship([Relationship.new(C_HasEffect.new(), null)])
func process_fire_damage():
# Find entities with any fire damage effect (type matching)
var fire_damaged = ECS.world.query.with_relationship([
Relationship.new(C_HasEffect.new(), C_FireDamage)
]).execute()
for entity in fire_damaged:
# Get the actual fire damage data using type matching
var fire_rel = entity.get_relationship(
Relationship.new(C_HasEffect.new(), C_FireDamage.new())
)
var fire_damage = fire_rel.target as C_FireDamage
# Apply damage
apply_damage(entity, fire_damage.damage_per_second * delta)
# Reduce duration
fire_damage.duration -= delta
if fire_damage.duration <= 0:
entity.remove_relationship(fire_rel)
func process_speed_buffs():
# Find entities with speed buffs using type matching
var speed_buffed = ECS.world.query.with_relationship([
Relationship.new(C_HasEffect.new(), C_SpeedBuff)
]).execute()
for entity in speed_buffed:
# Get actual speed buff data using type matching
var speed_rel = entity.get_relationship(
Relationship.new(C_HasEffect.new(), C_SpeedBuff.new())
)
var speed_buff = speed_rel.target as C_SpeedBuff
# Apply speed modification
apply_speed_modifier(entity, speed_buff.multiplier)
# Handle duration
speed_buff.duration -= delta
if speed_buff.duration <= 0:
entity.remove_relationship(speed_rel)
func remove_all_effects_from_entity(entity: Entity):
# Remove all status effects using wildcard
entity.remove_relationship(Relationship.new(C_HasEffect.new(), null))
func remove_some_effects_from_entity(entity: Entity, count: int):
# Remove a specific number of status effects using limit parameter
entity.remove_relationship(Relationship.new(C_HasEffect.new(), null), count)
func cleanse_one_debuff(entity: Entity):
# Remove just one debuff (useful for cleanse spells)
entity.remove_relationship(Relationship.new(C_Debuff.new(), null), 1)
func dispel_magic(entity: Entity, power: int):
# Dispel magic spell removes buffs based on power level
match power:
1: entity.remove_relationship(Relationship.new(C_HasEffect.new(), C_SpeedBuff), 1) # Weak dispel - 1 speed buff
2: entity.remove_relationship(Relationship.new(C_HasEffect.new(), null), 2) # Medium dispel - 2 any effects
3: entity.remove_relationship(Relationship.new(C_HasEffect.new(), null)) # Strong dispel - all effects
func antidote_healing(entity: Entity, antidote_strength: int):
# Antidote removes poison effects based on strength
entity.remove_relationship(Relationship.new(C_HasEffect.new(), C_PoisonDamage), antidote_strength)
func partial_fire_immunity(entity: Entity):
# Fire immunity spell removes up to 3 fire damage effects
entity.remove_relationship(Relationship.new(C_HasEffect.new(), C_FireDamage), 3)
func get_entities_with_damage_effects():
# Get entities with any damage type effect (fire or poison)
var fire_damaged = ECS.world.query.with_relationship([
Relationship.new(C_HasEffect.new(), C_FireDamage)
]).execute()
var poison_damaged = ECS.world.query.with_relationship([
Relationship.new(C_HasEffect.new(), C_PoisonDamage)
]).execute()
# Combine results
var all_damaged = {}
for entity in fire_damaged:
all_damaged[entity] = true
for entity in poison_damaged:
all_damaged[entity] = true
return all_damaged.keys()
```
### Combat System with Relationships
```gdscript
# Combat relationship components
class_name C_IsAttacking extends Component
@export var damage: float = 10.0
class_name C_IsTargeting extends Component
class_name C_IsAlliedWith extends Component
# Create combat entities
var player = Player.new()
var enemy1 = Enemy.new()
var enemy2 = Enemy.new()
var ally = Ally.new()
# Setup relationships
enemy1.add_relationship(Relationship.new(C_IsAttacking.new(25.0), player))
enemy2.add_relationship(Relationship.new(C_IsTargeting.new(), player))
player.add_relationship(Relationship.new(C_IsAlliedWith.new(), ally))
# Combat system queries
class_name CombatSystem extends System
func get_entities_attacking_player():
var player = get_player_entity()
return ECS.world.query.with_relationship([
Relationship.new(C_IsAttacking.new(), player)
]).execute()
func get_high_damage_attackers():
var player = get_player_entity()
# Find entities attacking player with damage >= 20
return ECS.world.query.with_relationship([
Relationship.new({C_IsAttacking: {'damage': {"_gte": 20.0}}}, player)
]).execute()
func get_player_allies():
var player = get_player_entity()
return ECS.world.query.with_reverse_relationship([
Relationship.new(C_IsAlliedWith.new(), player)
]).execute()
```
### Hierarchical Entity System
```gdscript
# Hierarchy relationship components
class_name C_ParentOf extends Component
class_name C_ChildOf extends Component
class_name C_OwnerOf extends Component
# Create hierarchy
var parent = Entity.new()
var child1 = Entity.new()
var child2 = Entity.new()
var weapon = Weapon.new()
# Setup parent-child relationships
parent.add_relationship(Relationship.new(C_ParentOf.new(), child1))
parent.add_relationship(Relationship.new(C_ParentOf.new(), child2))
child1.add_relationship(Relationship.new(C_ChildOf.new(), parent))
child2.add_relationship(Relationship.new(C_ChildOf.new(), parent))
# Setup ownership
child1.add_relationship(Relationship.new(C_OwnerOf.new(), weapon))
# Hierarchy system queries
class_name HierarchySystem extends System
func get_children_of_entity(entity: Entity):
return ECS.world.query.with_relationship([
Relationship.new(C_ParentOf.new(), entity)
]).execute()
func get_parent_of_entity(entity: Entity):
return ECS.world.query.with_reverse_relationship([
Relationship.new(C_ParentOf.new(), entity)
]).execute()
```
## 🏗️ Relationship Best Practices
### Performance Optimization
**Reuse Relationship Objects:**
```gdscript
# ✅ Good - Reuse for performance
var r_likes_apples = Relationship.new(C_Likes.new(), e_apple)
var r_attacking_players = Relationship.new(C_IsAttacking.new(), Player)
# Use the same relationship object multiple times
entity1.add_relationship(r_attacking_players)
entity2.add_relationship(r_attacking_players)
```
**Static Relationship Factory (Recommended):**
```gdscript
# ✅ Excellent - Organized relationship management
class_name Relationships
static func attacking_players():
return Relationship.new(C_IsAttacking.new(), Player)
static func attacking_anything():
return Relationship.new(C_IsAttacking.new(), ECS.wildcard)
static func chasing_players():
return Relationship.new(C_IsChasing.new(), Player)
static func interacting_with_anything():
return Relationship.new(C_Interacting.new(), ECS.wildcard)
static func equipped_on_anything():
return Relationship.new(C_EquippedOn.new(), ECS.wildcard)
static func any_status_effect():
return Relationship.new(C_HasEffect.new(), null)
static func any_damage_effect():
return Relationship.new(C_Damage.new(), null)
static func any_buff():
return Relationship.new(C_Buff.new(), null)
# Usage in systems:
var attackers = ECS.world.query.with_relationship([Relationships.attacking_players()]).execute()
var chasers = ECS.world.query.with_relationship([Relationships.chasing_anything()]).execute()
# Usage with limits:
entity.remove_relationship(Relationships.any_status_effect(), 1) # Remove one effect
entity.remove_relationship(Relationships.any_damage_effect(), 3) # Remove up to 3 damage effects
entity.remove_relationship(Relationships.any_buff()) # Remove all buffs
```
**Limited Removal Best Practices:**
```gdscript
# ✅ Good - Clear intent with descriptive variables
var WEAK_CLEANSE = 1
var MEDIUM_CLEANSE = 3
var STRONG_CLEANSE = -1 # All
entity.remove_relationship(Relationships.any_debuff(), WEAK_CLEANSE)
# ✅ Good - Helper functions for common operations
func remove_one_poison(entity: Entity):
entity.remove_relationship(Relationship.new(C_Poison.new(), null), 1)
func remove_all_fire_damage(entity: Entity):
entity.remove_relationship(Relationship.new(C_Damage.new(), C_FireDamage))
func partial_heal(entity: Entity, healing_power: int):
entity.remove_relationship(Relationship.new(C_Damage.new(), null), healing_power)
# ✅ Excellent - Validation before removal
func safe_remove_effects(entity: Entity, count: int):
var current_effects = entity.get_relationships(Relationship.new(C_Effect.new(), null)).size()
var to_remove = min(count, current_effects)
if to_remove > 0:
entity.remove_relationship(Relationship.new(C_Effect.new(), null), to_remove)
print("Removed ", to_remove, " effects")
```
### Naming Conventions
**Relationship Components:**
- Use descriptive names that clearly indicate the relationship
- Follow the `C_VerbNoun` pattern when possible
- Examples: `C_Likes`, `C_IsAttacking`, `C_OwnerOf`, `C_MemberOf`
**Relationship Variables:**
- Use `r_` prefix for relationship instances
- Examples: `r_likes_alice`, `r_attacking_player`, `r_parent_of_child`
## 🎯 Next Steps
Now that you understand relationships, component queries, and limited removal:
1. **Design relationship schemas** for your game's entities
2. **Experiment with wildcard queries** for dynamic systems
3. **Use component queries** to filter relationships by property criteria
4. **Implement limited removal** for stack-based and graduated systems
5. **Combine type matching with component queries** for flexible filtering
6. **Optimize with static relationship factories** for better performance
7. **Use limit parameters** for fine-grained control in healing, crafting, and effect systems
8. **Learn advanced patterns** in [Best Practices Guide](BEST_PRACTICES.md)
**Quick Start Checklist for Component Queries:**
- ✅ Try basic component query: `Relationship.new({C_Damage: {'amount': {"_gt": 10}}}, null)`
- ✅ Use query operators: `_eq`, `_ne`, `_gt`, `_lt`, `_gte`, `_lte`, `_in`, `_nin`
- ✅ Query both relation and target properties
- ✅ Combine queries with wildcards for flexible filtering
- ✅ Use type matching for "any component of this type" queries
**Quick Start Checklist for Limited Removal:**
- ✅ Try basic limit syntax: `entity.remove_relationship(rel, 1)`
- ✅ Build a simple stack system (buffs, debuffs, or damage)
- ✅ Create helper functions for common removal patterns
- ✅ Integrate limits into your game systems (healing, cleansing, etc.)
- ✅ Test edge cases (limit > available relationships)
- ✅ Combine component queries with limits for precise control
## 📚 Related Documentation
- **[Core Concepts](CORE_CONCEPTS.md)** - Understanding the ECS fundamentals
- **[Component Queries](COMPONENT_QUERIES.md)** - Advanced property-based filtering
- **[Best Practices](BEST_PRACTICES.md)** - Write maintainable ECS code
- **[Performance Optimization](PERFORMANCE_OPTIMIZATION.md)** - Optimize relationship queries
---
_"Relationships turn a collection of entities into a living, interconnected game world where entities can react to each other in meaningful ways."_

View File

@@ -0,0 +1,218 @@
# GECS Serialization
The GECS framework provides a robust serialization system using Godot's native resource format, enabling persistent game states, save systems, and level data management.
## Quick Start
### Basic Save/Load
```gdscript
# Save entities with persistent components
var query = ECS.world.query.with_all([C_Persistent])
var data = ECS.serialize(query)
ECS.save(data, "user://savegame.tres")
# Load entities back
var entities = ECS.deserialize("user://savegame.tres")
for entity in entities:
ECS.world.add_entity(entity)
```
### Binary Format
```gdscript
# Save as binary for production (smaller files)
ECS.save(data, "user://savegame.tres", true) # Creates .res file
# Load auto-detects format (tries .res first, then .tres)
var entities = ECS.deserialize("user://savegame.tres")
```
## API Reference
### ECS.serialize(query: QueryBuilder) -> GecsData
Converts entities matching a query into serializable data.
**Example:**
```gdscript
# Serialize specific entities
var player_query = ECS.world.query.with_all([C_Player, C_Health])
var save_data = ECS.serialize(player_query)
```
### ECS.save(data: GecsData, filepath: String, binary: bool = false) -> bool
Saves data to disk. Returns `true` on success.
**Parameters:**
- `data`: Serialized entity data
- `filepath`: Save location (use `.tres` extension)
- `binary`: If `true`, saves as `.res` (smaller, faster loading)
### ECS.deserialize(filepath: String) -> Array[Entity]
Loads entities from file. Returns empty array if file doesn't exist.
**Auto-detection:** Tries binary `.res` first, falls back to text `.tres`.
## Component Serialization
Only `@export` variables are serialized:
```gdscript
class_name C_PlayerData
extends Component
@export var health: float = 100.0 # ✅ Saved
@export var inventory: Array[String] # ✅ Saved
@export var position: Vector2 # ✅ Saved
var _cache: Dictionary = {} # ❌ Not saved
```
**Supported types:** All Godot built-ins (int, float, String, Vector2/3, Color, Array, Dictionary, etc.)
## Use Cases
### Save Game System
```gdscript
func save_game(slot: String):
var query = ECS.world.query.with_all([C_Persistent])
var data = ECS.serialize(query)
if ECS.save(data, "user://saves/slot_%s.tres" % slot, true):
print("Game saved!")
func load_game(slot: String):
ECS.world.purge() # Clear current state
var entities = ECS.deserialize("user://saves/slot_%s.tres" % slot)
for entity in entities:
ECS.world.add_entity(entity)
```
### Level Export/Import
```gdscript
func export_level():
var query = ECS.world.query.with_all([C_LevelObject])
var data = ECS.serialize(query)
ECS.save(data, "res://levels/level_01.tres")
func load_level(path: String):
var entities = ECS.deserialize(path)
ECS.world.add_entities(entities)
```
### Selective Serialization
```gdscript
# Save only player data
var player_query = ECS.world.query.with_all([C_Player])
# Save entities in specific area
var area_query = ECS.world.query.with_group("area_1")
# Save entities with specific components
var combat_query = ECS.world.query.with_all([C_Health, C_Weapon])
```
## Data Structure
The system uses two main resource classes:
### GecsData
```gdscript
class_name GecsData
extends Resource
@export var version: String = "0.1"
@export var entities: Array[GecsEntityData] = []
```
### GecsEntityData
```gdscript
class_name GecsEntityData
extends Resource
@export var entity_name: String = ""
@export var scene_path: String = "" # For prefab entities
@export var components: Array[Component] = []
```
## Error Handling
```gdscript
# Serialize never fails (returns empty data if no matches)
var data = ECS.serialize(query)
# Check save success
if not ECS.save(data, filepath):
print("Save failed - check permissions")
# Handle missing files
var entities = ECS.deserialize(filepath)
if entities.is_empty():
print("No data loaded")
```
## Performance
- **Memory:** Creates component copies during serialization
- **Speed:** Binary format ~60% smaller, faster loading than text
- **Scale:** Tested with 100+ entities, sub-second performance
## Binary vs Text Format
**Text (.tres):**
- Human readable
- Editor inspectable
- Version control friendly
- Development debugging
**Binary (.res):**
- Smaller file size
- Faster loading
- Production builds
- Auto-detection on load
## File Structure Example
```tres
[gd_resource type="GecsData" format=3]
[sub_resource type="C_Health" id="1"]
current = 85.0
maximum = 100.0
[sub_resource type="GecsEntityData" id="2"]
entity_name = "Player"
components = [SubResource("1")]
[resource]
version = "0.1"
entities = [SubResource("2")]
```
## Best Practices
1. **Use meaningful filenames:** `player_save.tres`, `level_boss.tres`
2. **Organize by purpose:** `user://saves/`, `res://levels/`
3. **Handle missing components gracefully**
4. **Use binary format for production**
5. **Version your save data for compatibility**
6. **Test with empty query results**
## Limitations
- No entity relationships (planned feature)
- Prefab entities need scene files present
- External resource references need manual handling

View File

@@ -0,0 +1,434 @@
# GECS Troubleshooting Guide
> **Quickly solve common GECS issues**
This guide helps you diagnose and fix the most common problems when working with GECS. Find your issue, apply the solution, and learn how to prevent it.
## 📋 Quick Diagnosis
### My Game Isn't Working At All
**Symptoms**: No entities moving, systems not running, nothing happening
**Quick Check**:
```gdscript
# In your _process() method, ensure you have:
func _process(delta):
if ECS.world:
ECS.world.process(delta) # This line is critical!
```
**Missing this?** → [Systems Not Running](#systems-not-running)
### Entities Aren't Moving/Updating
**Symptoms**: Entities exist but don't respond to systems
**Quick Check**:
1. Are your entities added to the world? `ECS.world.add_entity(entity)`
2. Do your entities have the right components? Check system queries
3. Are your systems properly organized in scene hierarchy? Check default_systems.tscn
**Still broken?** → [Entity Issues](#entity-issues)
### Performance Is Terrible
**Symptoms**: Low FPS, stuttering, slow response
**Quick Check**:
1. Enable profiling: `ECS.world.enable_profiling = true`
2. Check entity count: `print(ECS.world.entity_count)`
3. Look for expensive queries in your systems
**Need optimization?** → [Performance Issues](#performance-issues)
## 🚫 Systems Not Running
### Problem: Systems Never Execute
**Error Messages**:
- No error, but `process()` method never called
- Entities exist but don't change
**Solution**:
```gdscript
# ✅ Ensure this exists in your main scene
func _process(delta):
ECS.process(delta) # This processes all systems
# OR if using system groups:
func _process(delta):
ECS.process(delta, "physics")
ECS.process(delta, "render")
```
**Prevention**: Always call `ECS.process()` in your main game loop.
### Problem: System Query Returns Empty
**Symptoms**: System exists but `process()` never called
**Diagnosis**:
```gdscript
# Add this to your system for debugging
class_name MySystem extends System
func _ready():
print("MySystem query result: ", query().execute().size())
func query():
return q.with_all([C_ComponentA, C_ComponentB])
```
**Common Causes**:
1. **Missing Components**:
```gdscript
# ❌ Problem - Entity missing required component
var entity = Entity.new()
entity.add_component(C_ComponentA.new())
# Missing C_ComponentB!
# ✅ Solution - Add all required components
entity.add_component(C_ComponentA.new())
entity.add_component(C_ComponentB.new())
```
2. **Wrong Component Types**:
```gdscript
# ❌ Problem - Using instance instead of class
func query():
return q.with_all([C_ComponentA.new()]) # Wrong!
# ✅ Solution - Use class reference
func query():
return q.with_all([C_ComponentA]) # Correct!
```
3. **Component Not Added to World**:
```gdscript
# ❌ Problem - Entity not in world
var entity = Entity.new()
entity.add_component(C_ComponentA.new())
# Entity never added to world!
# ✅ Solution - Add entity to world
ECS.world.add_entity(entity)
```
## 🎭 Entity Issues
### Problem: Entity Components Not Found
**Error Messages**:
- `get_component() returned null`
- `Entity does not have component of type...`
**Diagnosis**:
```gdscript
# Debug what components an entity actually has
func debug_entity_components(entity: Entity):
print("Entity components:")
for component_path in entity.components.keys():
print(" ", component_path)
```
**Solution**: Ensure components are added correctly:
```gdscript
# ✅ Correct component addition
var entity = Entity.new()
entity.add_component(C_Health.new(100))
entity.add_component(C_Position.new(Vector2(50, 50)))
# Verify component exists before using
if entity.has_component(C_Health):
var health = entity.get_component(C_Health)
health.current -= 10
```
### Problem: Component Properties Not Updating
**Symptoms**: Setting component properties has no effect
**Common Causes**:
1. **Getting Component Reference Once**:
```gdscript
# ❌ Problem - Stale component reference
var health = entity.get_component(C_Health)
# ... later in code, component gets replaced ...
health.current = 50 # Updates old component!
# ✅ Solution - Get fresh reference each time
entity.get_component(C_Health).current = 50
```
2. **Modifying Wrong Entity**:
```gdscript
# ❌ Problem - Variable confusion
var player = get_player_entity()
var enemy = get_enemy_entity()
# Accidentally modify wrong entity
player.get_component(C_Health).current = 0 # Meant to be enemy!
# ✅ Solution - Use clear variable names
var player_health = player.get_component(C_Health)
var enemy_health = enemy.get_component(C_Health)
enemy_health.current = 0
```
## 💥 Common Errors
### Error: "Cannot access property/method on null instance"
**Full Error**:
```
Invalid get index 'current' (on base: 'null instance')
```
**Cause**: Component doesn't exist on entity
**Solution**:
```gdscript
# ❌ Causes null error
var health = entity.get_component(C_Health)
health.current -= 10 # health is null!
# ✅ Safe component access
if entity.has_component(C_Health):
var health = entity.get_component(C_Health)
health.current -= 10
else:
print("Entity doesn't have C_Health!")
```
### Error: "Class not found"
**Full Error**:
```
Identifier 'ComponentName' not found in current scope
```
**Causes & Solutions**:
1. **Missing class_name**:
```gdscript
# ❌ Problem - No class_name declaration
extends Component
# Script exists but can't be referenced by name
# ✅ Solution - Add class_name
class_name C_Health
extends Component
```
2. **File not saved or loaded**:
- Save your component script files
- Restart Godot if classes still not found
- Check for syntax errors in the component file
3. **Wrong inheritance**:
```gdscript
# ❌ Problem - Wrong base class
class_name C_Health
extends Node # Should be Component!
# ✅ Solution - Correct inheritance
class_name C_Health
extends Component
```
## 🐌 Performance Issues
### Problem: Low FPS / Stuttering
**Diagnosis Steps**:
1. **Enable profiling**:
```gdscript
ECS.world.enable_profiling = true
# Check processing times
func _process(delta):
ECS.process(delta)
print("Frame time: ", get_process_delta_time() * 1000, "ms")
```
2. **Check entity count**:
```gdscript
print("Total entities: ", ECS.world.entity_count)
print("System count: ", ECS.world.get_system_count())
```
**Common Fixes**:
1. **Too Many Entities in Broad Queries**:
```gdscript
# ❌ Problem - Overly broad query
func query():
return q.with_all([C_Position]) # Matches everything!
# ✅ Solution - More specific query
func query():
return q.with_all([C_Position, C_Movable])
```
2. **Expensive Queries Rebuilt Every Frame**:
```gdscript
# ❌ Problem - Rebuilding queries in process
func process(entities: Array[Entity], components: Array, delta: float):
var custom_entities = ECS.world.query.with_all([C_ComponentA]).execute()
# ✅ Solution - Use the system's query() method (automatically cached)
func query():
return q.with_all([C_ComponentA]) # Automatically cached by GECS
func process(entities: Array[Entity], components: Array, delta: float):
# Just process the entities passed in - already filtered by query
for entity in entities:
# Process entity...
```
## 🔧 Integration Issues
### Problem: GECS Conflicts with Godot Features
**Issue**: Using GECS entities with Godot nodes causes problems
**Solution**: Choose your approach consistently:
```gdscript
# ✅ Approach 1 - Pure ECS (recommended for complex games)
# Entities are not nodes, use ECS for everything
var entity = Entity.new() # Not added to scene tree
entity.add_component(C_Position.new())
ECS.world.add_entity(entity)
# ✅ Approach 2 - Hybrid (good for simpler games)
# Entities are nodes, use ECS for specific systems
var entity = Entity.new()
add_child(entity) # Entity is in scene tree
entity.add_component(C_Health.new())
ECS.world.add_entity(entity)
```
**Avoid**: Mixing approaches inconsistently in the same project.
### Problem: GECS Not Working After Scene Changes
**Symptoms**: Systems stop working when changing scenes
**Solution**: Properly reinitialize ECS in new scenes:
```gdscript
# In each main scene script
func _ready():
# Create new world for this scene
var world = World.new()
add_child(world)
ECS.world = world
# Systems are usually managed via scene composition
# See default_systems.tscn for organization
# Create your entities
setup_entities()
```
**Prevention**: Always initialize ECS properly in each scene that uses it.
## 🛠️ Debugging Tools
### Enable Debug Logging
Add to your project settings or main script:
```gdscript
# Enable GECS debug output
ECS.set_debug_level(ECS.DEBUG_VERBOSE)
# This will show:
# - Entity creation/destruction
# - Component additions/removals
# - System processing information
# - Query execution details
```
### Entity Inspector Tool
Create a debug tool to inspect entities at runtime:
```gdscript
# DebugPanel.gd
extends Control
func _on_inspect_button_pressed():
var entities = ECS.world.get_all_entities()
print("=== ENTITY INSPECTOR ===")
for i in range(min(10, entities.size())): # Show first 10
var entity = entities[i]
print("Entity ", i, ":")
print(" Components: ", entity.components.keys())
print(" Groups: ", entity.get_groups())
# Show component values
for comp_path in entity.components.keys():
var comp = entity.components[comp_path]
print(" ", comp_path, ": ", comp)
```
## 📚 Getting More Help
### Community Resources
- **Discord**: [Join our community](https://discord.gg/eB43XU2tmn) for help and discussions
- **GitHub Issues**: [Report bugs](https://github.com/csprance/gecs/issues)
- **Documentation**: [Complete Guide](../DOCUMENTATION.md)
### Before Asking for Help
Include this information in your question:
1. **GECS version** you're using
2. **Godot version** you're using
3. **Minimal code example** that reproduces the issue
4. **Error messages** (full text, not paraphrased)
5. **Expected vs actual behavior**
### Still Stuck?
If this guide doesn't solve your problem:
1. **Check the examples** in [Getting Started](GETTING_STARTED.md)
2. **Review best practices** in [Best Practices](BEST_PRACTICES.md)
3. **Search GitHub issues** for similar problems
4. **Create a minimal reproduction** and ask for help
---
_"Every bug is a learning opportunity. The key is knowing where to look and what questions to ask."_

View File

@@ -0,0 +1,299 @@
## Archetype
##
## Represents a unique combination of component types in the ECS framework.
## Entities with the exact same set of components share an archetype.
##
## Archetypes enable high-performance queries by grouping entities with identical
## component structures together in flat arrays, providing excellent cache locality
## and eliminating the need for set intersections during queries.
##
## [b]Key Concepts:[/b]
## - [b]Signature:[/b] Hash of all component types (determines archetype identity)
## - [b]Entities:[/b] Flat array of entities with this exact component combination
## - [b]Edges:[/b] Fast lookup for when components are added/removed (future optimization)
##
## [b]Example:[/b]
## [codeblock]
## # Archetype for entities with Position + Velocity
## var archetype = Archetype.new(12345, ["Position", "Velocity"])
## archetype.add_entity(player)
## archetype.add_entity(enemy)
## # Now both entities are stored contiguously for fast iteration
## [/codeblock]
##
## [b]Performance:[/b]
## - Add entity: O(1) amortized (array append)
## - Remove entity: O(1) (swap-remove with index tracking)
## - Query match: O(1) (check if archetype signature matches query)
## - Iterate entities: O(n) with excellent cache locality
class_name Archetype
extends RefCounted
## Unique hash identifying this component combination
## Generated by QueryCacheKey.build() from sorted component types
var signature: int = 0
## Sorted array of component resource paths (e.g., ["res://c_position.gd", "res://c_velocity.gd"])
## Used for debugging and archetype matching logic
var component_types: Array = []
## Flat array of entities with this exact component combination
## Provides excellent cache locality when iterating in systems
var entities: Array[Entity] = []
## Fast lookup: Entity -> index in entities array
## Enables O(1) entity removal using swap-remove technique
var entity_to_index: Dictionary = {} # Entity -> int
## OPTIMIZATION: Bitset for enabled/disabled state instead of archetype splitting
## Uses PackedInt64Array where each bit represents whether entity at that index is enabled
## Reduces archetype count by 2x and enables O(1) enabled/disabled filtering
var enabled_bitset: PackedInt64Array = []
## OPTIMIZATION: Structure of Arrays (SoA) column storage for cache-friendly iteration
## Maps component_path -> Array of component instances
## Enables Flecs-style direct array iteration without dictionary lookups
## Example: columns["res://c_velocity.gd"] = [vel1, vel2, vel3, ...]
var columns: Dictionary = {} # String (component_path) -> Array of components
## Archetype edges for fast component add/remove (future optimization)
## Maps: component_path -> Archetype (the archetype you get by adding/removing that component)
var add_edges: Dictionary = {} # String -> Archetype
var remove_edges: Dictionary = {} # String -> Archetype
## Initialize archetype with signature and component types
func _init(p_signature: int, p_component_types: Array):
signature = p_signature
component_types = p_component_types.duplicate()
component_types.sort() # Ensure sorted for consistent matching
# Initialize column arrays for each component type
for comp_type in component_types:
columns[comp_type] = []
## Add an entity to this archetype
## Uses O(1) append and tracks index for fast removal
## OPTIMIZATION: Also populates column arrays for cache-friendly iteration
func add_entity(entity: Entity) -> void:
var index = entities.size()
entities.append(entity)
entity_to_index[entity] = index
# OPTIMIZATION: Update enabled bitset
_ensure_bitset_capacity(index + 1)
_set_enabled_bit(index, entity.enabled)
# OPTIMIZATION: Populate column arrays from entity.components
for comp_path in component_types:
if entity.components.has(comp_path):
(columns[comp_path]
.append(entity.components[comp_path]))
else:
# Entity doesn't have this component yet (might be mid-initialization)
# Push null placeholder, will be fixed when component is added
columns[comp_path].append(null)
## Remove an entity from this archetype using swap-remove
## O(1) operation: swaps with last entity and pops
## OPTIMIZATION: Also maintains column arrays in sync
func remove_entity(entity: Entity) -> bool:
if not entity_to_index.has(entity):
return false
var index = entity_to_index[entity]
var last_index = entities.size() - 1
# Swap with last element in entities array
if index != last_index:
var last_entity = entities[last_index]
entities[index] = last_entity
entity_to_index[last_entity] = index
# OPTIMIZATION: Swap in column arrays too (maintain same ordering)
for comp_path in component_types:
columns[comp_path][index] = columns[comp_path][last_index]
# OPTIMIZATION: Swap enabled bit
var last_enabled = _get_enabled_bit(last_index)
_set_enabled_bit(index, last_enabled)
# Remove last element from entities
entities.pop_back()
entity_to_index.erase(entity)
# OPTIMIZATION: Remove last element from all columns
for comp_path in component_types:
columns[comp_path].pop_back()
# OPTIMIZATION: Update bitset size (no need to clear the bit, just reduce logical size)
# The bit will be overwritten when a new entity is added
return true
## Check if this archetype has a specific entity
func has_entity(entity: Entity) -> bool:
return entity_to_index.has(entity)
## Get entity count in this archetype
func size() -> int:
return entities.size()
## Check if archetype is empty
func is_empty() -> bool:
return entities.is_empty()
## Clear all entities from this archetype
func clear() -> void:
entities.clear()
entity_to_index.clear()
# OPTIMIZATION: Clear column arrays
for comp_path in component_types:
columns[comp_path].clear()
# OPTIMIZATION: Clear bitset
enabled_bitset.clear()
## Check if this archetype matches a query with all/any/exclude components
## [param all_comp_types] Component paths that must all be present
## [param any_comp_types] Component paths where at least one must be present
## [param exclude_comp_types] Component paths that must not be present
func matches_query(all_comp_types: Array, any_comp_types: Array, exclude_comp_types: Array) -> bool:
# Check all_components: must have ALL of these
for comp_type in all_comp_types:
if not component_types.has(comp_type):
return false
# Check any_components: must have AT LEAST ONE of these
if not any_comp_types.is_empty():
var has_any = false
for comp_type in any_comp_types:
if component_types.has(comp_type):
has_any = true
break
if not has_any:
return false
# Check exclude_components: must have NONE of these
for comp_type in exclude_comp_types:
if component_types.has(comp_type):
return false
return true
## Get a debug-friendly string representation
func _to_string() -> String:
var comp_names = []
for comp_type in component_types:
# Extract just the class name from the path
var parts = comp_type.split("/")
var filename = parts[parts.size() - 1].replace(".gd", "")
comp_names.append(filename)
return "Archetype[sig=%d, comps=%s, entities=%d]" % [
signature,
str(comp_names),
entities.size()
]
## Set up an edge to another archetype when a component is added
## Enables O(1) archetype transitions when components change
func set_add_edge(component_path: String, target_archetype: Archetype) -> void:
add_edges[component_path] = target_archetype
## Set up an edge to another archetype when a component is removed
## Enables O(1) archetype transitions when components change
func set_remove_edge(component_path: String, target_archetype: Archetype) -> void:
remove_edges[component_path] = target_archetype
## Get the target archetype when adding a component (if edge exists)
func get_add_edge(component_path: String) -> Archetype:
return add_edges.get(component_path, null)
## Get the target archetype when removing a component (if edge exists)
func get_remove_edge(component_path: String) -> Archetype:
return remove_edges.get(component_path, null)
## OPTIMIZATION: Get component column array for cache-friendly iteration
## Enables Flecs-style direct array access instead of dictionary lookups per entity
## [param component_path] The resource path of the component type (e.g., C_Velocity.resource_path)
## [returns] Array of component instances in entity index order, or empty array if not found
##
## Example:
## [codeblock]
## var velocities = archetype.get_column(C_Velocity.resource_path)
## for i in range(velocities.size()):
## var velocity = velocities[i]
## var entity = archetype.entities[i]
## # Process with cache-friendly sequential access
## [/codeblock]
func get_column(component_path: String) -> Array:
return columns.get(component_path, [])
## OPTIMIZATION: Get entities filtered by enabled state using bitset
## [param enabled_only] If true, return only enabled entities; if false, only disabled
## [returns] Array of entities matching the enabled state
func get_entities_by_enabled_state(enabled_only: bool) -> Array[Entity]:
var result: Array[Entity] = []
for i in range(entities.size()):
if _get_enabled_bit(i) == enabled_only:
result.append(entities[i])
return result
## OPTIMIZATION: Update entity enabled state in bitset
## [param entity] The entity to update
## [param enabled] The new enabled state
func update_entity_enabled_state(entity: Entity, enabled: bool) -> void:
if entity_to_index.has(entity):
var index = entity_to_index[entity]
_set_enabled_bit(index, enabled)
## OPTIMIZATION: Ensure bitset has enough capacity for the given number of entities
func _ensure_bitset_capacity(required_size: int) -> void:
var required_int64s = (required_size + 63) / 64 # Round up to nearest 64-bit boundary
while enabled_bitset.size() < required_int64s:
enabled_bitset.append(0)
## OPTIMIZATION: Set enabled bit for entity at index
func _set_enabled_bit(index: int, enabled: bool) -> void:
var int64_index = index / 64
var bit_index = index % 64
_ensure_bitset_capacity(index + 1)
if enabled:
enabled_bitset[int64_index] |= (1 << bit_index)
else:
enabled_bitset[int64_index] &= ~(1 << bit_index)
## OPTIMIZATION: Get enabled bit for entity at index
func _get_enabled_bit(index: int) -> bool:
if index >= entities.size():
return false
var int64_index = index / 64
var bit_index = index % 64
if int64_index >= enabled_bitset.size():
return false
return (enabled_bitset[int64_index] & (1 << bit_index)) != 0

View File

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

View File

@@ -0,0 +1,48 @@
## A Component serves as a data container within the [_ECS] ([Entity] [Component] [System]) framework.
##
## A [Component] holds specific data related to an [Entity] but does not contain any behavior or logic.[br]
## Components are designed to be lightweight and easily attachable to [Entity]s to define their properties.[br]
##[br]
## [b]Example:[/b]
##[codeblock]
## ## Velocity Component.
## ##
## ## Holds the velocity data for an entity.
## class_name VelocityComponent
## extends Node2D
##
## @export var velocity: Vector2 = Vector2.ZERO
##[/codeblock]
##[br]
## [b]Component Queries:[/b][br]
## Use component query dictionaries to match components by specific property criteria in queries and relationships:[br]
##[codeblock]
## # Query entities with health >= 50
## var entities = ECS.world.query.with_all([{C_Health: {'amount': {"_gte": 50}}}]).execute()
##
## # Query relationships with specific damage values
## var entities = ECS.world.query.with_relationship([
## Relationship.new({C_Damage: {'amount': {"_eq": 100}}}, target)
## ]).execute()
##[/codeblock]
@icon("res://addons/gecs/assets/component.svg")
class_name Component
extends Resource
## Emitted when a property of this component changes. This is slightly different from the property_changed signal
signal property_changed(component: Resource, property_name: String, old_value: Variant, new_value: Variant)
## Reference to the parent entity that owns this component
var parent: Entity
## Used to serialize the component to a dictionary with only the export variables
## This is used for the debugger to send the data to the editor
func serialize() -> Dictionary:
var data: Dictionary = {}
for prop_info in get_script().get_script_property_list():
# Only include properties that are exported (@export variables)
if prop_info.usage & PROPERTY_USAGE_EDITOR:
var prop_name: String = prop_info.name
var prop_val = get(prop_name)
data[prop_name] = prop_val
return data

View File

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

102
addons/gecs/ecs/ecs.gd Normal file
View File

@@ -0,0 +1,102 @@
## ECS ([Entity] [Component] [System]) Singleton[br]
## The ECS class acts as the central manager for the entire ECS framework
##
## The [_ECS] class maintains the current active [World] and provides access to [QueryBuilder] for fetching [Entity]s based on their [Component]s.
##[br]
## This singleton allows any part of the game to interact with the ECS system seamlessly.
## [codeblock]
## var entities = ECS.world.query.with_all([Transform, Velocity]).execute()
## for entity in entities:
## entity.get_component(Transform).position += entity.get_component(Velocity).direction * delta
## [/codeblock]
## This is also where you control the setup of the world and process loop of the ECS system.
##[codeblock]
##
## func _read(delta):
## ECS.world = world
##
## func _process(delta):
## ECS.process(delta)
##[/codeblock]
## or in the physics loop
##[codeblock]
## func _physics_process(delta):
## ECS.process(delta)
##[/codeblock]
class_name _ECS
extends Node
## Emitted when the world is changed with a ref to the new world
signal world_changed(world: World)
## Emitted when the world is exited
signal world_exited
## The Current active [World] Instance[br]
## Holds a reference to the currently active [World], allowing access to the [member World.query] instance and any [Entity]s and [System]s within it.
var world: World:
get:
return world
set(value):
# Add the new world to the scenes
world = value
if world:
if not world.is_inside_tree():
# Add the world to the tree if it is not already
get_tree().root.get_node("./Root").add_child(world)
if not world.is_connected("tree_exited", _on_world_exited):
world.connect("tree_exited", _on_world_exited)
world_changed.emit(world)
assert(GECSEditorDebuggerMessages.set_world(world) if debug else true, 'Debug Data')
## Are we in debug mode? Controlled by project setting gecs/debug_mode
var debug := ProjectSettings.get_setting(GecsSettings.SETTINGS_DEBUG_MODE, false)
## This is an array of functions that get called on the entities when they get added to the world (after they are ready)
var entity_preprocessors: Array[Callable] = []
## This is an array of functions that get called on the entities right before they get removed from the world
var entity_postprocessors: Array[Callable] = []
## A Wildcard for use in relatonship queries. Indicates can be any value for a relation
## or a target in a Relationship Pair ECS.wildcard
var wildcard = null
## This is called to process the current active [World] instance and the [System]s within it.
## You would call this in _process or _physics_process to update the [_ECS] system.[br]
## If you provide a group name it will run just that group otherwise it runs all groups[br]
## Example:
## [codeblock]ECS.world.process(world, 'my-system-group')[/codeblock]
func process(delta: float, group: String = "") -> void:
world.process(delta, group)
## Get all components of a specific type from a list of entities[br]
## If the component does not exist on the entity it will return the default_component if provided or assert
func get_components(entities, component_type, default_component = null) -> Array:
var components = []
for entity in entities:
var component = entity.components.get(component_type.resource_path, null)
if not component and not default_component:
assert(component, "Entity does not have component: " + str(component_type))
if not component and default_component:
component = default_component
components.append(component)
return components
## Called when the world is exited
func _on_world_exited() -> void:
world = null
world_exited.emit()
assert(GECSEditorDebuggerMessages.exit_world() if debug else true, 'Debug Data')
func serialize(query: QueryBuilder, config: GECSSerializeConfig = null) -> GecsData:
return GECSIO.serialize(query, config)
func save(gecs_data: GecsData, filepath: String, binary: bool = false) -> bool:
return GECSIO.save(gecs_data, filepath, binary)
func deserialize(gecs_filepath: String) -> Array[Entity]:
return GECSIO.deserialize(gecs_filepath)

View File

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

509
addons/gecs/ecs/entity.gd Normal file
View File

@@ -0,0 +1,509 @@
## Entity[br]
##
## Represents an entity within the [_ECS] framework.[br]
## An entity is a container that can hold multiple [Component]s.
##
## Entities serve as the fundamental building block for game objects, allowing for flexible and modular design.[br]
##[br]
## Entities can have [Component]s added or removed dynamically, enabling the behavior and properties of game objects to change at runtime.[br]
## Entities can have [Relationship]s added or removed dynamically, allowing for a deep hierarchical query system.[br]
##[br]
## Example:
##[codeblock]
## var entity = Entity.new()
## var transform = Transform.new()
## entity.add_component(transform)
## entity.component_added.connect(_on_component_added)
##
## func _on_component_added(entity: Entity, component_key: String) -> void:
## print("Component added:", component_key)
##[/codeblock]
@icon("res://addons/gecs/assets/entity.svg")
@tool
class_name Entity
extends CharacterBody3D
#region Signals
## Emitted when a [Component] is added to the entity.
signal component_added(entity: Entity, component: Resource)
## Emitted when a [Component] is removed from the entity.
signal component_removed(entity: Entity, component: Resource)
## Emitted when a [Component] property is changed.
signal component_property_changed(
entity: Entity,
component: Resource,
property_name: String,
old_value: Variant,
new_value: Variant
)
## Emit when a [Relationship] is added to the [Entity]
signal relationship_added(entity: Entity, relationship: Relationship)
## Emit when a [Relationship] is removed from the [Entity]
signal relationship_removed(entity: Entity, relationship: Relationship)
#endregion Signals
#region Exported Variables
## The id of the entity either UUID or custom string.
## This must be unique within a [World]. If left blank, a UUID will be generated when the entity is added to a world.
@export var id: String
## Is this entity active? (Will show up in queries)
@export var enabled: bool = true:
set(value):
if enabled != value:
var old_enabled = enabled
enabled = value
# Notify world to move entity between enabled/disabled archetypes
_on_enabled_changed(old_enabled, value)
## [Component]s to be attached to the entity set in the editor. These will be loaded for you and added to the [Entity]
@export var component_resources: Array[Component] = []
## Serialization config override for this specific entity (optional)
@export var serialize_config: GECSSerializeConfig
#endregion Exported Variables
#region Public Variables
## [Component]s attached to the [Entity] in the form of Dict[resource_path:String, Component]
var components: Dictionary = {}
## Relationships attached to the entity
var relationships: Array[Relationship] = []
## Cache for component resource paths to avoid repeated .get_script().resource_path calls
var _component_path_cache: Dictionary = {}
## Logger for entities to only log to a specific domain
var _entityLogger = GECSLogger.new().domain("Entity")
## We can store ephemeral state on the entity
var _state = {}
#endregion Public Variables
#region Built-in Virtual Methods
## Called to initialize the entity and its components.
## This is called automatically by [method World.add_entity][br]
func _initialize(_components: Array = []) -> void:
_entityLogger.trace("Entity Initializing Components: ", self.name)
# because components can be added before the entity is added to the world
# replay adding components here so signals pick them up and the index is updated
var temp_comps = components.values().duplicate_deep()
components.clear()
for comp in temp_comps:
add_component(comp)
# Add components defined in code to comp resources
component_resources.append_array(define_components())
# remove any component_resources that are already defined in components
# This is useful for when you instantiate an entity from a scene and want to overide components
component_resources = component_resources.filter(func(comp): return not has_component(comp.get_script()))
# Add components passed in directly to the _initialize method to override everything else
component_resources.append_array(_components)
# Initialize components
for res in component_resources:
add_component(res.duplicate(true))
# Call the lifecycle method on_ready
on_ready()
#endregion Built-in Virtual Methods
## Get the effective serialization config for this entity
## Returns entity-specific config if set, otherwise falls back to world default
func get_effective_serialize_config() -> GECSSerializeConfig:
if serialize_config != null:
return serialize_config
if ECS.world != null and ECS.world.default_serialize_config != null:
return ECS.world.default_serialize_config
# Fallback if no world or no default config
var fallback = GECSSerializeConfig.new()
return fallback
#region Components
## Adds a single component to the entity.[br]
## [param component] The subclass of [Component] to add.[br]
## [b]Example[/b]:
## [codeblock]entity.add_component(HealthComponent)[/codeblock]
func add_component(component: Resource) -> void:
# Cache the resource path to avoid repeated calls
var resource_path = component.get_script().resource_path
# If a component of this type already exists, remove it first
if components.has(resource_path):
var existing_component = components[resource_path]
remove_component(existing_component)
_component_path_cache[component] = resource_path
components[resource_path] = component
component.parent = self
if not component.property_changed.is_connected(_on_component_property_changed):
component.property_changed.connect(_on_component_property_changed)
## Adding components happens through a signal
component_added.emit(self , component)
_entityLogger.trace("Added Component: ", resource_path)
func _on_component_property_changed(
component: Resource, property_name: String, old_value: Variant, new_value: Variant
) -> void:
# Pass this signal on to the world
component_property_changed.emit(self , component, property_name, old_value, new_value)
## Adds multiple components to the entity.[br]
## [param _components] An [Array] of [Component]s to add.[br]
## [b]Example:[/b]
## [codeblock]entity.add_components([TransformComponent, VelocityComponent])[/codeblock]
func add_components(_components: Array):
# OPTIMIZATION: Batch component additions to avoid multiple archetype transitions
# Instead of moving archetype once per component, calculate the final archetype once
if _components.is_empty():
return
# Add all components to local storage first (no signals yet)
var added_components = []
for component in _components:
if component == null:
continue
var component_path = component.get_script().resource_path
if not components.has(component_path):
components[component_path] = component
added_components.append(component)
# If no new components were actually added, return early
if added_components.is_empty():
return
# OPTIMIZATION: Move to final archetype only once, after all components are added
if ECS.world and ECS.world.entity_to_archetype.has(self ):
var old_archetype = ECS.world.entity_to_archetype[ self ]
var new_signature = ECS.world._calculate_entity_signature(self )
var comp_types = components.keys()
var new_archetype = ECS.world._get_or_create_archetype(new_signature, comp_types)
# Only move if we actually need a different archetype
if old_archetype != new_archetype:
# Remove from old archetype
old_archetype.remove_entity(self )
# Add to new archetype
new_archetype.add_entity(self )
ECS.world.entity_to_archetype[ self ] = new_archetype
# Clean up empty old archetype
if old_archetype.is_empty():
old_archetype.add_edges.clear()
old_archetype.remove_edges.clear()
ECS.world.archetypes.erase(old_archetype.signature)
else:
# Same archetype - just update the column data for new components
for component in added_components:
var comp_path = component.get_script().resource_path
var entity_index = old_archetype.entity_to_index[ self ]
old_archetype.columns[comp_path][entity_index] = component
# Emit signals for all added components
for component in added_components:
component_added.emit(self , component)
## Removes a single component from the entity.[br]
## [param component] The [Component] subclass to remove.[br]
## [b]Example:[/b]
## [codeblock]entity.remove_component(HealthComponent)[/codeblock]
func remove_component(component: Resource) -> void:
# Use cached path if available, otherwise get it from the component class
var resource_path: String
if _component_path_cache.has(component):
resource_path = _component_path_cache[component]
_component_path_cache.erase(component)
else:
# Component parameter should be a class/script, consistent with has_component
resource_path = component.resource_path
if components.has(resource_path):
var component_instance = components[resource_path]
components.erase(resource_path)
# Clean up cache entry for the component instance
_component_path_cache.erase(component_instance)
component_removed.emit(self , component_instance)
# ARCHETYPE: Signal handler (_on_entity_component_removed) handles archetype update
_entityLogger.trace("Removed Component: ", resource_path)
func deferred_remove_component(component: Resource) -> void:
call_deferred_thread_group("remove_component", component)
## Removes multiple components from the entity.[br]
## [param _components] An array of components to remove.[br]
##
## [b]Example:[/b]
## [codeblock]entity.remove_components([transform_component, velocity_component])[/codeblock]
func remove_components(_components: Array):
# OPTIMIZATION: Batch component removals to avoid multiple archetype transitions
# Instead of moving archetype once per component, calculate the final archetype once
if _components.is_empty():
return
# Remove all components from local storage first (no signals yet)
var removed_components = []
for _component in _components:
if _component == null:
continue
var comp_to_remove: Resource = null
# Handle both Scripts and Resource instances
# NOTE: Check Script first since Script inherits from Resource
if _component is Script:
comp_to_remove = get_component(_component)
elif _component is Resource:
comp_to_remove = _component
if comp_to_remove:
var component_path = comp_to_remove.get_script().resource_path
if components.has(component_path):
components.erase(component_path)
removed_components.append(comp_to_remove)
# If no components were actually removed, return early
if removed_components.is_empty():
return
# OPTIMIZATION: Move to final archetype only once, after all components are removed
if ECS.world and ECS.world.entity_to_archetype.has(self ):
var old_archetype = ECS.world.entity_to_archetype[ self ]
var new_signature = ECS.world._calculate_entity_signature(self )
var comp_types = components.keys()
var new_archetype = ECS.world._get_or_create_archetype(new_signature, comp_types)
# Only move if we actually need a different archetype
if old_archetype != new_archetype:
# Remove from old archetype
old_archetype.remove_entity(self )
# Add to new archetype
new_archetype.add_entity(self )
ECS.world.entity_to_archetype[ self ] = new_archetype
# Clean up empty old archetype
if old_archetype.is_empty():
old_archetype.add_edges.clear()
old_archetype.remove_edges.clear()
ECS.world.archetypes.erase(old_archetype.signature)
# Emit signals for all removed components
for component in removed_components:
component_removed.emit(self , component)
## Removes all components from the entity.[br]
## [b]Example:[/b]
## [codeblock]entity.remove_all_components()[/codeblock]
func remove_all_components() -> void:
for component in components.values():
remove_component(component)
## Retrieves a specific [Component] from the entity.[br]
## [param component] The [Component] class to retrieve.[br]
## Returns the requested [Component] if it exists, otherwise `null`.[br]
## [b]Example:[/b]
## [codeblock]var transform = entity.get_component(Transform)[/codeblock]
func get_component(component: Resource) -> Component:
return components.get(component.resource_path, null)
## Check to see if an entity has a specific component on it.[br]
## This is useful when you're checking to see if it has a component and not going to use the component itself.[br]
## If you plan on getting and using the component, use [method get_component] instead.
func has_component(component: Resource) -> bool:
return components.has(component.resource_path)
#endregion Components
#region Relationships
## Adds a relationship to this entity.[br]
## [param relationship] The [Relationship] to add.
func add_relationship(relationship: Relationship) -> void:
assert(
not relationship._is_query_relationship,
"Cannot add query relationships to entities. Query relationships (created with dictionaries) are for matching only, not for storage."
)
relationship.source = self
relationships.append(relationship)
relationship_added.emit(self , relationship)
func add_relationships(_relationships: Array):
for relationship in _relationships:
add_relationship(relationship)
## Removes a relationship from the entity.[br]
## [param relationship] The [Relationship] to remove.[br]
## [param limit] Maximum number of relationships to remove. -1 = all (default), 0 = none, >0 = up to that many.[br]
## [br]
## [b]Examples:[/b]
## [codeblock]
## # Remove all matching relationships (default behavior)
## entity.remove_relationship(Relationship.new(C_Damage.new(), target))
##
## # Remove only one matching relationship
## entity.remove_relationship(Relationship.new(C_Damage.new(), target), 1)
##
## # Remove up to 3 matching relationships
## entity.remove_relationship(Relationship.new(C_Damage.new(), target), 3)
##
## # Remove no relationships (useful for testing/debugging)
## entity.remove_relationship(Relationship.new(C_Damage.new(), target), 0)
## [/codeblock]
func remove_relationship(relationship: Relationship, limit: int = -1) -> void:
if limit == 0:
return
var to_remove = []
var removed_count = 0
var pattern_remove = true
if relationships.has(relationship):
to_remove.append(relationship)
pattern_remove = false
if pattern_remove:
for rel in relationships:
if rel.matches(relationship):
to_remove.append(rel)
removed_count += 1
# If limit is positive and we've reached it, stop collecting
if limit > 0 and removed_count >= limit:
break
for rel in to_remove:
relationships.erase(rel)
relationship_removed.emit(self , rel)
## Removes multiple relationships from the entity.[br]
## [param _relationships] Array of [Relationship]s to remove.[br]
## [param limit] Maximum number of relationships to remove per relationship type. -1 = all (default), 0 = none, >0 = up to that many.
func remove_relationships(_relationships: Array, limit: int = -1):
for relationship in _relationships:
remove_relationship(relationship, limit)
## Removes all relationships from the entity.
func remove_all_relationships() -> void:
var to_remove = relationships.duplicate()
for rel in to_remove:
relationships.erase(rel)
relationship_removed.emit(self , rel)
## Retrieves a specific [Relationship] from the entity.
## [param relationship] The [Relationship] to retrieve.
## [return] The first matching [Relationship] if it exists, otherwise `null`
func get_relationship(relationship: Relationship) -> Relationship:
var to_remove = []
for rel in relationships:
# Check if the relationship is valid
if not rel.valid():
to_remove.append(rel)
continue
if rel.matches(relationship):
# Remove invalid relationships before returning
for invalid_rel in to_remove:
relationships.erase(invalid_rel)
relationship_removed.emit(self , invalid_rel)
return rel
# Remove invalid relationships
for rel in to_remove:
relationships.erase(rel)
relationship_removed.emit(self , rel)
return null
## Retrieves [Relationship]s from the entity.
## [param relationship] The [Relationship]s to retrieve.
## [return] Array of all matching [Relationship]s (empty array if none found).
func get_relationships(relationship: Relationship) -> Array[Relationship]:
var results: Array[Relationship] = []
var to_remove = []
for rel in relationships:
# Check if the relationship is valid
if not rel.valid():
to_remove.append(rel)
continue
if rel.matches(relationship):
results.append(rel)
# Remove invalid relationships
for rel in to_remove:
relationships.erase(rel)
relationship_removed.emit(self , rel)
return results
## Checks if the entity has a specific relationship.[br]
## [param relationship] The [Relationship] to check for.
func has_relationship(relationship: Relationship) -> bool:
return get_relationship(relationship) != null
#endregion Relationships
#region Lifecycle Methods
## Called after the entity is fully initialized and ready.[br]
## Override this method to perform additional setup after all components have been added.
func on_ready() -> void:
pass
## Called right before the entity is freed from memory.[br]
## Override this method to perform any necessary cleanup before the entity is destroyed.
func on_destroy() -> void:
pass
## Called when the entity is disabled.[br]
func on_disable() -> void:
pass
## Called when the entity is enabled.[br]
func on_enable() -> void:
pass
## Define the default components in code to use (Instead of in the editor)[br]
## This should return a list of components to add by default when the entity is created
func define_components() -> Array:
return []
## INTERNAL: Called when entity.enabled changes to move entity between archetypes
func _on_enabled_changed(old_value: bool, new_value: bool) -> void:
# Only handle if entity is already in a world
if not ECS.world or not ECS.world.entity_to_archetype.has(self ):
return
# OPTIMIZATION: Update bitset instead of moving between archetypes
# This eliminates the need for separate enabled/disabled archetypes
var archetype = ECS.world.entity_to_archetype[ self ]
archetype.update_entity_enabled_state(self , new_value)
# Invalidate query cache since archetypes changed
ECS.world.cache_invalidated.emit()
#endregion Lifecycle Methods

View File

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

View File

@@ -0,0 +1,74 @@
## An Observer is like a system that reacts when specific component events happen
## It has a query that filters which entities are monitored for these events
## Observers can respond to component add/remove/change events on specific sets of entities
##
## [b]Important:[/b] For property changes to trigger [method on_component_changed], you must
## manually emit the [signal Component.property_changed] signal from within your component.
## Simply setting properties does not automatically trigger observers.
##
## [b]Example of triggering property changes:[/b]
## [codeblock]
## # In your component class
## class_name MyComponent
## extends Component
##
## @export var health: int = 100 : set = set_health
##
## func set_health(new_value: int):
## var old_value = health
## health = new_value
## # This is required for observers to detect the change
## property_changed.emit(self, "health", old_value, new_value)
## [/codeblock]
@icon("res://addons/gecs/assets/observer.svg")
class_name Observer
extends Node
## The [QueryBuilder] object exposed for conveinence to use in the system and to create the query.
var q: QueryBuilder
## Override this method and return a [QueryBuilder] to define the required [Component]s the entity[br]
## must match for the observer to trigger. If empty this will match all [Entity]s
func match() -> QueryBuilder:
return q
## Override this method and provide a single component to watch for events.[br]
## This means that the observer will only react to events on this component (add/remove/change)[br]
## assuming the entity matches the query defined in the [method match] method
func watch() -> Resource:
assert(false, "You must override the watch() method in your system")
return
## Override this method to define the main processing function for the observer when a component is added to an [Entity].[br]
## [param entity] The [Entity] the component was added to.[br]
## [param component] The [Component] that was added. Guaranteed to be the component defined in [method watch].[br]
func on_component_added(entity: Entity, component: Resource) -> void:
pass
## Override this method to define the main processing function for the observer when a component is removed from an [Entity].[br]
## [param entity] The [Entity] the component was removed from.[br]
## [param component] The [Component] that was removed. Guaranteed to be the component defined in [method watch].[br]
func on_component_removed(entity: Entity, component: Resource) -> void:
pass
## Override this method to define the main processing function for property changes.[br]
## This method is called when a property changes on the watched component.[br]
## [br]
## [b]Note:[/b] This method only triggers when the component explicitly emits its
## [signal Component.property_changed] signal for performance reasons. Setting properties directly will
## [b]not[/b] automatically trigger this method.[br]
## [br]
## [param entity] The [Entity] the component that changed is attached to.
## [param component] The [Component] that changed. Guaranteed to be the component defined in [method watch].
## [param property] The name of the property that changed on the [Component].
## [param old_value] The old value of the property.
## [param new_value] The new value of the property.
func on_component_changed(
entity: Entity, component: Resource, property: String, new_value: Variant, old_value: Variant
) -> void:
pass

View File

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

View File

@@ -0,0 +1,571 @@
## QueryBuilder[br]
## A utility class for constructing and executing queries to retrieve entities based on their components.
##
## The QueryBuilder supports filtering entities that have all, any, or exclude specific components,
## as well as filtering by enabled/disabled status using high-performance group indexing.
## [codeblock]
## var enabled_entities = ECS.world.query
## .with_all([Transform, Velocity])
## .with_any([Health])
## .with_none([Inactive])
## .enabled(true)
## .execute()
##
## var disabled_entities = ECS.world.query.enabled(false).execute()
## var all_entities = ECS.world.query.enabled(null).execute()
##[/codeblock]
## This will efficiently query entities using indexed group lookups rather than
## filtering the entire entity list.
class_name QueryBuilder
extends RefCounted
# The world instance to query against.
var _world: World
# Components that an entity must have all of.
var _all_components: Array = []
# Components that an entity must have at least one of.
var _any_components: Array = []
# Components that an entity must not have.
var _exclude_components: Array = []
# Relationships that entities must have
var _relationships: Array = [] # (Retained for entity-level filtering only; NOT part of cache key)
var _exclude_relationships: Array = []
# Components queries that an entity must match
var _all_components_queries: Array = []
# Components queries that an entity must match for any components
var _any_components_queries: Array = []
# Groups that an entity must be in
var _groups: Array = []
# Groups that an entity must not be in
var _exclude_groups: Array = []
# Enabled/disabled filter: true = enabled only, false = disabled only, null = all
var _enabled_filter = null
# Components to iterate in archetype mode (ordered array of component types)
var _iterate_components: Array = []
# Add fields for query result caching
var _cache_valid: bool = false
var _cached_result: Array = []
# OPTIMIZATION: Cache the query hash key to avoid recalculating FNV-1a hash every frame
var _cache_key: int = -1
var _cache_key_valid: bool = false
## Initializes the QueryBuilder with the specified [param world]
func _init(world: World = null):
_world = world as World
## Allow setting the world after creation for editor time creation
func set_world(world: World):
_world = world
## Clears the query criteria, resetting all filters. Mostly used in testing
## [param returns] - The current instance of the QueryBuilder for chaining.
func clear():
_all_components = []
_any_components = []
_exclude_components = []
_relationships = []
_exclude_relationships = []
_all_components_queries = []
_any_components_queries = []
_groups = []
_exclude_groups = []
_enabled_filter = null
_iterate_components = []
_cache_valid = false
_cache_key_valid = false
return self
## Finds entities with all of the provided components.[br]
## [param components] An [Array] of [Component] classes.[br]
## [param returns]: [QueryBuilder] instance for chaining.
func with_all(components: Array = []) -> QueryBuilder:
var processed = ComponentQueryMatcher.process_component_list(components)
_all_components = processed.components
_all_components_queries = processed.queries
_cache_valid = false
_cache_key_valid = false
return self
## Entities must have at least one of the provided components.[br]
## [param components] An [Array] of [Component] classes.[br]
## [param reutrns] [QueryBuilder] instance for chaining.
func with_any(components: Array = []) -> QueryBuilder:
var processed = ComponentQueryMatcher.process_component_list(components)
_any_components = processed.components
_any_components_queries = processed.queries
_cache_valid = false
_cache_key_valid = false
return self
## Entities must not have any of the provided components.[br]
## Params: [param components] An [Array] of [Component] classes.[br]
## [param reutrns] [QueryBuilder] instance for chaining.
func with_none(components: Array = []) -> QueryBuilder:
# Don't process queries for with_none, just take the components directly
_exclude_components = components.map(
func(comp): return comp if not comp is Dictionary else comp.keys()[0]
)
_cache_valid = false
_cache_key_valid = false
return self
## Finds entities with specific relationships using weak matching by default (component type and queries).
## [br][b]Weak Matching (default):[/b] Components match by type and component queries are evaluated.
## [br]For strong matching (exact component data), use [method Entity.has_relationship] with [code]weak=false[/code].
func with_relationship(relationships: Array = []) -> QueryBuilder:
_relationships = relationships
_cache_valid = false
# Cache key unaffected by relationships (structural only)
return self
## Entities must not have any of the provided relationships using weak matching by default (component type and queries).
## [br][b]Weak Matching (default):[/b] Components match by type and component queries are evaluated.
## [br]For strong matching (exact component data), use [method Entity.has_relationship] with [code]weak=false[/code].
func without_relationship(relationships: Array = []) -> QueryBuilder:
_exclude_relationships = relationships
_cache_valid = false
return self
## Query for entities that are targets of specific relationships
func with_reverse_relationship(relationships: Array = []) -> QueryBuilder:
for rel in relationships:
if rel.relation != null:
var rev_key = "reverse_" + rel.relation.get_script().resource_path
if _world.reverse_relationship_index.has(rev_key):
return self.with_all(_world.reverse_relationship_index[rev_key])
_cache_valid = false
return self
## Finds entities with specific groups.
func with_group(groups: Array[String] = []) -> QueryBuilder:
_groups.append_array(groups)
_cache_valid = false
_cache_key_valid = false
return self
## Entities must not have any of the provided groups.
func without_group(groups: Array[String] = []) -> QueryBuilder:
_exclude_groups.append_array(groups)
_cache_valid = false
_cache_key_valid = false
return self
## Filter to only enabled entities using internal arrays for optimal performance.[br]
## [param returns] [QueryBuilder] instance for chaining.
func enabled() -> QueryBuilder:
_enabled_filter = true
_cache_valid = false
_cache_key_valid = false
return self
## Filter to only disabled entities using internal arrays for optimal performance.[br]
## [param returns] [QueryBuilder] instance for chaining.
func disabled() -> QueryBuilder:
_enabled_filter = false
_cache_valid = false
_cache_key_valid = false
return self
## Specifies the component order for batch processing iteration.[br]
## This determines the order of component arrays passed to System.process_batch()[br]
## [param components] An array of component types in the desired iteration order[br]
## [param returns] [QueryBuilder] instance for chaining.[br][br]
## [b]Example:[/b]
## [codeblock]
## func query() -> QueryBuilder:
## return q.with_all([C_Velocity, C_Timer]).enabled().iterate([C_Velocity, C_Timer])
##
## func process_batch(entities: Array[Entity], components: Array, delta: float) -> void:
## var velocities = components[0] # C_Velocity (first in iterate)
## var timers = components[1] # C_Timer (second in iterate)
## [/codeblock]
func iterate(components: Array) -> QueryBuilder:
_iterate_components = components
return self
func execute_one() -> Entity:
# Execute the query and return the first matching entity
var result = execute()
if result.size() > 0:
return result[0]
return null
## Executes the constructed query and retrieves matching entities.[br]
## [param returns] - An [Array] of [Entity] that match the query criteria.
func execute() -> Array:
# For relationship or group filters we need fresh filtering every call (no stale cached filtered result)
var uses_relationship_filters := (not _relationships.is_empty() or not _exclude_relationships.is_empty())
var uses_group_filters := (not _groups.is_empty() or not _exclude_groups.is_empty())
var structural_result: Array
if _cache_valid and not uses_relationship_filters and not uses_group_filters:
# Safe to reuse full cached result only for purely structural component queries
structural_result = _cached_result
else:
# Recompute base structural/group result (without relationship filtering caching)
structural_result = _internal_execute()
# Only cache if no dynamic relationship/group filters are present
if not uses_relationship_filters and not uses_group_filters:
_cached_result = structural_result
_cache_valid = true
else:
_cache_valid = false # force recompute next call
var result = structural_result
# Apply component property queries (post structural)
if not _all_components_queries.is_empty() and _has_actual_queries(_all_components_queries):
result = _filter_entities_by_queries(result, _all_components, _all_components_queries, true)
if not _any_components_queries.is_empty() and _has_actual_queries(_any_components_queries):
result = _filter_entities_by_queries(result, _any_components, _any_components_queries, false)
return result
func _internal_execute() -> Array:
# If we have groups or exclude groups, gather entities from those groups
if not _groups.is_empty() or not _exclude_groups.is_empty():
var entities_in_group = []
# Use Godot's optimized get_nodes_in_group() instead of filtering
if not _groups.is_empty():
# For multiple groups, use set operations for efficiency
var group_set: Set
for i in range(_groups.size()):
var group_name = _groups[i]
var nodes_in_group = _world.get_tree().get_nodes_in_group(group_name)
# Filter to only Entity nodes
var entities_in_this_group = nodes_in_group.filter(func(n): return n is Entity)
if i == 0:
# First group - start with these entities
group_set = Set.new(entities_in_this_group)
else:
# Subsequent groups - intersect (entity must be in ALL groups)
group_set = group_set.intersect(Set.new(entities_in_this_group))
entities_in_group = group_set.to_array() if group_set else []
else:
# If no required groups but we have exclude_groups, start with ALL entities from component query
# This handles the case of "without_group" queries
entities_in_group = (
_world._query(_all_components, _any_components, _exclude_components, _enabled_filter, get_cache_key()) as Array[Entity]
)
# Filter out entities in excluded groups
if not _exclude_groups.is_empty():
var exclude_set = Set.new()
for group_name in _exclude_groups:
var nodes_in_group = _world.get_tree().get_nodes_in_group(group_name)
var entities_in_excluded = nodes_in_group.filter(func(n): return n is Entity)
exclude_set = exclude_set.union(Set.new(entities_in_excluded))
# Remove excluded entities
var result_set = Set.new(entities_in_group)
entities_in_group = result_set.difference(exclude_set).to_array()
# match the entities in the group with the query
return matches(entities_in_group)
# Otherwise, query the world with enabled filter for optimal performance
# OPTIMIZATION: Pass pre-calculated cache key to avoid rehashing
var result = (
_world._query(_all_components, _any_components, _exclude_components, _enabled_filter, get_cache_key()) as Array[Entity]
)
# Handle relationship filtering
if not _relationships.is_empty() or not _exclude_relationships.is_empty():
var filtered_entities: Array = []
for entity in result:
var matches = true
# Required relationships
for relationship in _relationships:
if not entity.has_relationship(relationship):
matches = false
break
# Excluded relationships
if matches:
for ex_relationship in _exclude_relationships:
if entity.has_relationship(ex_relationship):
matches = false
break
if matches:
filtered_entities.append(entity)
result = filtered_entities
# Return the structural query result (caching handled in execute())
# Note: enabled/disabled filtering is now handled in World._query for optimal performance
return result
## Check if any query in the array has actual property filters (not just empty {})
func _has_actual_queries(queries: Array) -> bool:
for query in queries:
if not query.is_empty():
return true
return false
## Filter entities based on component queries
func _filter_entities_by_queries(
entities: Array, components: Array, queries: Array, require_all: bool
) -> Array:
var filtered = []
for entity in entities:
if entity == null:
continue
if require_all:
# Must match all queries
var matches = true
for i in range(components.size()):
var component = entity.get_component(components[i])
var query = queries[i]
if not ComponentQueryMatcher.matches_query(component, query):
matches = false
break
if matches:
filtered.append(entity)
else:
# Must match any query
for i in range(components.size()):
var component = entity.get_component(components[i])
var query = queries[i]
if component and ComponentQueryMatcher.matches_query(component, query):
filtered.append(entity)
break
return filtered
## Check if entity matches any of the queries
func _entity_matches_any_query(entity: Entity, components: Array, queries: Array) -> bool:
for i in range(components.size()):
var component = entity.get_component(components[i])
if component and ComponentQueryMatcher.matches_query(component, queries[i]):
return true
return false
## Filters a provided list of entities using the current query criteria.[br]
## Unlike execute(), this doesn't query the world but instead filters the provided entities.[br][br]
## [param entities] Array of entities to filter[br]
## [param returns] Array of entities that match the query criteria[br]
func matches(entities: Array) -> Array:
# if the query is empty all entities match
if is_empty():
return entities
var result = []
for entity in entities:
# If it's null skip it
if entity == null:
continue
assert(entity is Entity, "Must be an entity")
var matches = true
# Check all required components
for component in _all_components:
if not entity.has_component(component):
matches = false
break
# If still matching and we have any_components, check those
if matches and not _any_components.is_empty():
matches = false
for component in _any_components:
if entity.has_component(component):
matches = true
break
# Check excluded components
if matches:
for component in _exclude_components:
if entity.has_component(component):
matches = false
break
# Check required relationships
if matches and not _relationships.is_empty():
for relationship in _relationships:
if not entity.has_relationship(relationship):
matches = false
break
# Check excluded relationships
if matches and not _exclude_relationships.is_empty():
for relationship in _exclude_relationships:
if entity.has_relationship(relationship):
matches = false
break
if matches:
result.append(entity)
return result
func combine(other: QueryBuilder) -> QueryBuilder:
_all_components += other._all_components
_all_components_queries += other._all_components_queries
_any_components += other._any_components
_any_components_queries += other._any_components_queries
_exclude_components += other._exclude_components
_relationships += other._relationships
_exclude_relationships += other._exclude_relationships
_groups += other._groups
_exclude_groups += other._exclude_groups
_cache_valid = false
return self
func as_array() -> Array:
return [
_all_components,
_any_components,
_exclude_components,
_relationships,
_exclude_relationships
]
func is_empty() -> bool:
return (
_all_components.is_empty()
and _any_components.is_empty()
and _exclude_components.is_empty()
and _relationships.is_empty()
and _exclude_relationships.is_empty()
)
func _to_string() -> String:
var parts = []
if not _all_components.is_empty():
parts.append("with_all(" + _format_components(_all_components) + ")")
if not _any_components.is_empty():
parts.append("with_any(" + _format_components(_any_components) + ")")
if not _exclude_components.is_empty():
parts.append("with_none(" + _format_components(_exclude_components) + ")")
if not _relationships.is_empty():
parts.append("with_relationship(" + _format_relationships(_relationships) + ")")
if not _exclude_relationships.is_empty():
parts.append("without_relationship(" + _format_relationships(_exclude_relationships) + ")")
if not _groups.is_empty():
parts.append("with_group(" + str(_groups) + ")")
if not _exclude_groups.is_empty():
parts.append("without_group(" + str(_exclude_groups) + ")")
if _enabled_filter != null:
if _enabled_filter:
parts.append("enabled()")
else:
parts.append("disabled()")
if not _all_components_queries.is_empty():
parts.append("component_queries(" + _format_component_queries(_all_components_queries) + ")")
if not _any_components_queries.is_empty():
parts.append("any_component_queries(" + _format_component_queries(_any_components_queries) + ")")
if parts.is_empty():
return "ECS.world.query"
return "ECS.world.query." + ".".join(parts)
func _format_components(components: Array) -> String:
var names = []
for component in components:
if component is Script:
names.append(component.get_global_name())
else:
names.append(str(component))
return "[" + ", ".join(names) + "]"
func _format_relationships(relationships: Array) -> String:
var names = []
for relationship in relationships:
if relationship.has_method("to_string"):
names.append(relationship.to_string())
else:
names.append(str(relationship))
return "[" + ", ".join(names) + "]"
func _format_component_queries(queries: Array) -> String:
var formatted = []
for query in queries:
if query.has_method("to_string"):
formatted.append(query.to_string())
else:
formatted.append(str(query))
return "[" + ", ".join(formatted) + "]"
func compile(query: String) -> QueryBuilder:
return QueryBuilder.new(_world)
func invalidate_cache():
_cache_valid = false
_cache_key_valid = false
## Called when a relationship is added or removed (only for queries using relationships)
## Relationship changes do NOT affect structural cache key; queries only re-filter at execute time
func _on_relationship_changed(_entity: Entity, _relationship: Relationship):
_cache_valid = false # only result cache
## Get the cached query hash key, calculating it only once
## OPTIMIZATION: Avoids recalculating FNV-1a hash every frame in hot path queries
func get_cache_key() -> int:
# Structural cache key excludes relationships/groups (matches 6.0.0 behavior)
if not _cache_key_valid:
if _world:
_cache_key = QueryCacheKey.build(_all_components, _any_components, _exclude_components)
_cache_key_valid = true
else:
return -1
return _cache_key
## Get matching archetypes directly for column-based iteration
## OPTIMIZATION: Skip entity flattening, return archetypes directly for cache-friendly processing
## [br][br]
## [b]Example:[/b]
## [codeblock]
## func process_all(entities: Array, delta: float):
## for archetype in query().archetypes():
## var transforms = archetype.get_column(transform_path)
## for i in range(transforms.size()):
## # Process transform directly from packed array
## [/codeblock]
func archetypes() -> Array[Archetype]:
return _world.get_matching_archetypes(self )

View File

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

View File

@@ -0,0 +1,184 @@
## QueryCacheKey
## ------------------------------------------------------------------------------
## PURPOSE
## Build a structural query signature (cache key) that is:
## * Order-insensitive inside each domain (with_all / with_any / with_none)
## * Order-sensitive ACROSS domains (the same component in different domains => different key)
## * Extremely fast (single allocation + contiguous integer writes)
## * Stable for the lifetime of loaded component scripts (uses script.instance_id)
##
## WHY NOT JUST MERGE & SORT?
## A naive approach merges all component IDs + domain markers then sorts. That destroys
## domain boundaries and lets these collide:
## with_all([A,B]) vs with_any([A,B])
## After a full sort both become the same multiset {1,2,3,A,B}. We prevent that by
## emitting DOMAIN MARKER then COUNT then the sorted IDs for that domain preserving
## domain structure while still being permutation-insensitive within the domain.
##
## LAYOUT (integers in final array):
## [ 1, |count_all|, sorted(all_ids)...,
## 2, |count_any|, sorted(any_ids)...,
## 3, |count_none|, sorted(ex_ids)... ]
## 1/2/3 : domain sentinels (ALL / ANY / NONE)
## count_* : disambiguates empty vs non-empty ( [] vs [X] ) and prevents boundary ambiguity
## sorted(ids) : order-insensitivity; identical sets different order => same run of ints
##
## COMPLEXITY
## Sorting dominates: O(a log a + y log y + n log n). Typical domain sizes are tiny.
## Allocation: exactly one integer array sized to final layout.
## Hash: Godot's native Array.hash() (64-bit) very fast.
##
## COLLISION PROFILE
## 64-bit space (~1.84e19). Even 1,000,000 distinct structural queries => ~2.7e-8 collision probability.
## Practically zero for real ECS usage. See PERFORMANCE_CACHE_KEY_NOTE.md for math.
##
## EXTENSION POINTS
## * Add a leading VERSION marker if the format evolves.
## * Add extra domains (e.g. relationship structure) by appending new marker + count + IDs.
## * Add enabled-state separation by injecting a synthetic domain marker (kept separate currently).
##
## INLINE COMMENT LEGEND
## all_ids / any_ids / ex_ids : per-domain sorted component script instance IDs
## total : exact integer count used for one-shot allocation (prevents incremental reallocation)
## layout[i] = marker/count/id : sequential write building final signature array
##
class_name QueryCacheKey
extends RefCounted
static func build(
all_components: Array,
any_components: Array,
exclude_components: Array,
relationships: Array = [],
exclude_relationships: Array = [],
groups: Array = [],
exclude_groups: Array = []
) -> int:
# Collect & sort per-domain IDs (order-insensitive inside each domain)
var all_ids: Array[int] = []
for c in all_components: all_ids.append(c.get_instance_id())
all_ids.sort()
var any_ids: Array[int] = []
for c in any_components: any_ids.append(c.get_instance_id())
any_ids.sort()
var ex_ids: Array[int] = []
for c in exclude_components: ex_ids.append(c.get_instance_id())
ex_ids.sort()
# Collect & sort relationship IDs
var rel_ids: Array[int] = []
for rel in relationships:
# Use Script instance ID for type matching (consistent with component queries)
# Relationship.new(C_TestB.new()) creates component instance, we want the Script's ID
if rel.relation:
rel_ids.append(rel.relation.get_script().get_instance_id())
else:
rel_ids.append(0)
# Handle target - use Script instance ID for Components (type matching)
if rel.target is Component:
# Component target: use Script instance ID for type matching
rel_ids.append(rel.target.get_script().get_instance_id())
elif rel.target is Entity:
# Entity target: use entity instance ID (entities are specific instances)
rel_ids.append(rel.target.get_instance_id())
elif rel.target is Script:
# Archetype target: use Script instance ID
rel_ids.append(rel.target.get_instance_id())
elif rel.target != null:
# Other types: use generic hash
rel_ids.append(rel.target.hash())
else:
rel_ids.append(0) # null target
rel_ids.sort()
var ex_rel_ids: Array[int] = []
for rel in exclude_relationships:
# Use Script instance ID for type matching (consistent with component queries)
if rel.relation:
ex_rel_ids.append(rel.relation.get_script().get_instance_id())
else:
ex_rel_ids.append(0)
# Handle target - use Script instance ID for Components (type matching)
if rel.target is Component:
ex_rel_ids.append(rel.target.get_script().get_instance_id())
elif rel.target is Entity:
ex_rel_ids.append(rel.target.get_instance_id())
elif rel.target is Script:
ex_rel_ids.append(rel.target.get_instance_id())
elif rel.target != null:
ex_rel_ids.append(rel.target.hash())
else:
ex_rel_ids.append(0)
ex_rel_ids.sort()
# Collect & sort group name hashes
var group_ids: Array[int] = []
for group_name in groups:
group_ids.append(group_name.hash())
group_ids.sort()
var ex_group_ids: Array[int] = []
for group_name in exclude_groups:
ex_group_ids.append(group_name.hash())
ex_group_ids.sort()
# Compute exact total length: (marker + count) per domain + IDs
var total = 1 + 1 + all_ids.size() # ALL marker + count + ids
total += 1 + 1 + any_ids.size() # ANY marker + count + ids
total += 1 + 1 + ex_ids.size() # NONE marker + count + ids
total += 1 + 1 + rel_ids.size() # RELATIONSHIPS marker + count + ids
total += 1 + 1 + ex_rel_ids.size() # EXCLUDE_RELATIONSHIPS marker + count + ids
total += 1 + 1 + group_ids.size() # GROUPS marker + count + ids
total += 1 + 1 + ex_group_ids.size() # EXCLUDE_GROUPS marker + count + ids
# Single allocation for final signature layout
var layout: Array[int] = []
layout.resize(total)
var i := 0
# --- Domain: ALL ---
layout[i] = 1; i += 1 # Marker for ALL domain
layout[i] = all_ids.size(); i += 1 # Count (disambiguates empty vs non-empty)
for id in all_ids:
layout[i] = id; i += 1 # Sorted ALL component IDs
# --- Domain: ANY ---
layout[i] = 2; i += 1 # Marker for ANY domain
layout[i] = any_ids.size(); i += 1 # Count
for id in any_ids:
layout[i] = id; i += 1 # Sorted ANY component IDs
# --- Domain: NONE (exclude) ---
layout[i] = 3; i += 1 # Marker for NONE domain
layout[i] = ex_ids.size(); i += 1 # Count
for id in ex_ids:
layout[i] = id; i += 1 # Sorted EXCLUDE component IDs
# --- Domain: RELATIONSHIPS ---
layout[i] = 4; i += 1 # Marker for RELATIONSHIPS domain
layout[i] = rel_ids.size(); i += 1 # Count
for id in rel_ids:
layout[i] = id; i += 1 # Sorted relationship IDs
# --- Domain: EXCLUDE_RELATIONSHIPS ---
layout[i] = 5; i += 1 # Marker for EXCLUDE_RELATIONSHIPS domain
layout[i] = ex_rel_ids.size(); i += 1 # Count
for id in ex_rel_ids:
layout[i] = id; i += 1 # Sorted exclude relationship IDs
# --- Domain: GROUPS ---
layout[i] = 6; i += 1 # Marker for GROUPS domain
layout[i] = group_ids.size(); i += 1 # Count
for id in group_ids:
layout[i] = id; i += 1 # Sorted group name hashes
# --- Domain: EXCLUDE_GROUPS ---
layout[i] = 7; i += 1 # Marker for EXCLUDE_GROUPS domain
layout[i] = ex_group_ids.size(); i += 1 # Count
for id in ex_group_ids:
layout[i] = id; i += 1 # Sorted exclude group name hashes
# Hash the structural layout -> 64-bit key
return layout.hash()

View File

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

View File

@@ -0,0 +1,294 @@
## Relationship
## Represents a relationship between entities in the ECS framework.
## A relationship consists of a [Component] relation and a target, which can be an [Entity], a [Component], or an archetype.
##
## Relationships are used to link entities together, allowing for complex queries and interactions.
## They enable entities to have dynamic associations that can be queried and manipulated at runtime.
## The powerful relationship system supports component-based targets for hierarchical type systems.
##
## [b]Relationship Types:[/b]
## [br]• [b]Entity Relationships:[/b] Link entities to other entities
## [br]• [b]Component Relationships:[/b] Link entities to component instances for type hierarchies
## [br]• [b]Archetype Relationships:[/b] Link entities to component/entity classes
##
## [b]Query Features:[/b]
## [br]• [b]Type Matching:[/b] Find entities by relationship component type (default)
## [br]• [b]Query Matching:[/b] Use dictionaries to match by specific property criteria
## [br]• [b]Wildcard Queries:[/b] Use [code]null[/code] targets to find any relationship of a type
##
## [b]Basic Entity Relationship Example:[/b]
## [codeblock]
## # Create a 'likes' relationship where e_bob likes e_alice
## var likes_relationship = Relationship.new(C_Likes.new(), e_alice)
## e_bob.add_relationship(likes_relationship)
##
## # Check if e_bob has a 'likes' relationship with e_alice
## if e_bob.has_relationship(Relationship.new(C_Likes.new(), e_alice)):
## print("Bob likes Alice!")
## [/codeblock]
##
## [b]Component-Based Relationship Example:[/b]
## [codeblock]
## # Create a damage type hierarchy using components as targets
## var fire_damage = C_FireDamage.new(50)
## var poison_damage = C_PoisonDamage.new(25)
##
## # Entity has different types of damage
## entity.add_relationship(Relationship.new(C_Damaged.new(), fire_damage))
## entity.add_relationship(Relationship.new(C_Damaged.new(), poison_damage))
##
## # Query for entities with any damage type (wildcard)
## var damaged_entities = ECS.world.query.with_relationship([
## Relationship.new(C_Damaged.new(), null)
## ]).execute()
##
## # Query for entities with fire damage amount >= 50 using component query
## var fire_damaged = ECS.world.query.with_relationship([
## Relationship.new(C_Damaged.new(), {C_FireDamage: {'amount': {"_gte": 50}}})
## ]).execute()
##
## # Check if entity has any fire damage (type matching)
## var has_fire_damage = entity.has_relationship(
## Relationship.new(C_Damaged.new(), C_FireDamage.new())
## )
## [/codeblock]
##
## [b]Component Query Examples:[/b]
## [codeblock]
## # Query relation by property value
## var entities = ECS.world.query.with_relationship([
## Relationship.new({C_Eats: {'value': {"_eq": 8}}}, e_apple)
## ]).execute()
##
## # Query target by property value
## var entities = ECS.world.query.with_relationship([
## Relationship.new(C_Damage.new(), {C_Health: {'amount': {"_gte": 50}}})
## ]).execute()
##
## # Query both relation AND target
## var entities = ECS.world.query.with_relationship([
## Relationship.new(
## {C_Buff: {'duration': {"_gt": 10}}},
## {C_Player: {'level': {"_gte": 5}}}
## )
## ]).execute()
## [/codeblock]
class_name Relationship
extends Resource
## The relation component of the relationship.
## This defines the type of relationship and can contain additional data.
var relation
## The target of the relationship.
## This can be an [Entity], a [Component], an archetype, or null.
var target
## The source of the relationship.
var source
## Component query for relation matching (if relation was created from dictionary)
var relation_query: Dictionary = {}
## Component query for target matching (if target was created from dictionary)
var target_query: Dictionary = {}
## Flag to track if this relationship was created from a component query dictionary (private - used for validation)
var _is_query_relationship: bool = false
func _init(_relation = null, _target = null):
# Handle component queries (dictionaries) for relation
if _relation is Dictionary:
_is_query_relationship = true
# Extract component type and query from dictionary
for component_type in _relation:
var query = _relation[component_type]
# Store the query and create component instance
relation_query = query
_relation = component_type.new()
break
# Handle component queries (dictionaries) for target
if _target is Dictionary:
_is_query_relationship = true
# Extract component type and query from dictionary
for component_type in _target:
var query = _target[component_type]
# Store the query and create component instance
target_query = query
_target = component_type.new()
break
# Assert for class reference vs instance for relation (skip for dictionaries)
if not _relation is Dictionary:
assert(
not (_relation != null and (_relation is GDScript or _relation is Script)),
"Relation must be an instance of Component (did you forget to call .new()?)"
)
# Assert for relation type
assert(
_relation == null or _relation is Component, "Relation must be null or a Component instance"
)
# Assert for class reference vs instance for target (skip for dictionaries)
if not _target is Dictionary:
assert(
not (_target != null and _target is GDScript and _target is Component),
"Target must be an instance of Component (did you forget to call .new()?)"
)
# Assert for target type
assert(
_target == null or _target is Entity or _target is Script or _target is Component,
"Target must be null, an Entity instance, a Script archetype, or a Component instance"
)
relation = _relation
target = _target
## Checks if this relationship matches another relationship.
## [param other]: The [Relationship] to compare with.
## [return]: `true` if both the relation and target match, `false` otherwise.
##
## [b]Matching Modes:[/b]
## [br]• [b]Type Matching:[/b] Components match by type (default behavior)
## [br]• [b]Query Matching:[/b] If component query dictionary used, evaluates property criteria
## [br]• [b]Wildcard Matching:[/b] [code]null[/code] relations or targets act as wildcards and match anything
func matches(other: Relationship) -> bool:
var rel_match = false
var target_match = false
# Compare relations
if other.relation == null or relation == null:
# If either relation is null, consider it a match (wildcard)
rel_match = true
else:
# Check if other relation has component query (query relationships)
if not other.relation_query.is_empty():
# Other has component query, check if this relation matches that query
if relation.get_script() == other.relation.get_script():
rel_match = ComponentQueryMatcher.matches_query(relation, other.relation_query)
else:
rel_match = false
# Check if this relation has component query (this is query relationship)
elif not relation_query.is_empty():
# This has component query, check if other relation matches this query
if relation.get_script() == other.relation.get_script():
rel_match = ComponentQueryMatcher.matches_query(other.relation, relation_query)
else:
rel_match = false
else:
# Standard type matching by script type
rel_match = relation.get_script() == other.relation.get_script()
# Compare targets
if other.target == null or target == null:
# If either target is null, consider it a match (wildcard)
target_match = true
else:
if target == other.target:
target_match = true
elif target is Entity and other.target is Script:
# target is an entity instance, other.target is an archetype
target_match = target.get_script() == other.target
elif target is Script and other.target is Entity:
# target is an archetype, other.target is an entity instance
target_match = other.target.get_script() == target
elif target is Entity and other.target is Entity:
# Both targets are entities; compare references directly
target_match = target == other.target
elif target is Script and other.target is Script:
# Both targets are archetypes; compare directly
target_match = target == other.target
elif target is Component and other.target is Component:
# Both targets are components; check for query or type matching
# Check if other target has component query
if not other.target_query.is_empty():
# Other has component query, check if this target matches that query
if target.get_script() == other.target.get_script():
target_match = ComponentQueryMatcher.matches_query(target, other.target_query)
else:
target_match = false
# Check if this target has component query
elif not target_query.is_empty():
# This has component query, check if other target matches this query
if target.get_script() == other.target.get_script():
target_match = ComponentQueryMatcher.matches_query(other.target, target_query)
else:
target_match = false
else:
# Standard type matching by script type
target_match = target.get_script() == other.target.get_script()
elif target is Component and other.target is Script:
# target is component instance, other.target is component archetype
target_match = target.get_script() == other.target
elif target is Script and other.target is Component:
# target is component archetype, other.target is component instance
target_match = other.target.get_script() == target
else:
# Unable to compare targets
target_match = false
return rel_match and target_match
func valid() -> bool:
# make sure the target is valid or null
var target_valid = false
if target == null:
target_valid = true
elif target is Entity:
target_valid = is_instance_valid(target)
elif target is Component:
# Components are Resources, so they're always valid once created
target_valid = true
elif target is Script:
# Script archetypes are always valid
target_valid = true
else:
target_valid = false
# Ensure the source is a valid Entity instance; it cannot be null
var source_valid = is_instance_valid(source)
return target_valid and source_valid
## Provides a consistent string representation for cache keys and debugging.
## Two relationships with the same relation type and target should produce identical strings.
func _to_string() -> String:
var parts = []
# Format relation component
if relation == null:
parts.append("null")
elif not relation_query.is_empty():
# This is a query relationship - include the query criteria
parts.append(relation.get_script().resource_path + str(relation_query))
else:
# Standard relation - just the type
parts.append(relation.get_script().resource_path)
# Format target
if target == null:
parts.append("null")
elif target is Entity:
# Use instance_id for stability - entity ID may not be set yet
parts.append("Entity#" + str(target.get_instance_id()))
elif target is Component:
if not target_query.is_empty():
# Component with query
parts.append(target.get_script().resource_path + str(target_query))
else:
# Type matching - use Script instance ID (consistent with query caching)
parts.append(target.get_script().resource_path + "#" + str(target.get_script().get_instance_id()))
elif target is Script:
# Archetype target
parts.append("Archetype:" + target.resource_path)
else:
parts.append(str(target))
return "Relationship(" + parts[0] + " -> " + parts[1] + ")"

View File

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

459
addons/gecs/ecs/system.gd Normal file
View File

@@ -0,0 +1,459 @@
## System[br]
##
## The base class for all systems within the ECS framework.[br]
##
## Systems contain the core logic and behavior, processing [Entity]s that have specific [Component]s.[br]
## Each system overrides the [method System.query] and returns a query using [code]q[/code] or [code]ECS.world.query[/code][br]
## to define the required [Component]s for it to process [Entity]s and implements the [method System.process] method.[br][br]
## [b]Example (Simple):[/b]
##[codeblock]
## class_name MovementSystem
## extends System
##
## func query():
## return q.with_all([Transform, Velocity])
##
## func process(entities: Array[Entity], components: Array, delta: float) -> void:
## # Per-entity processing (simple but slower)
## for entity in entities:
## var transform = entity.get_component(Transform)
## var velocity = entity.get_component(Velocity)
## transform.position += velocity.direction * velocity.speed * delta
##[/codeblock]
## [b]Example (Optimized with iterate()):[/b]
##[codeblock]
## func query():
## return q.with_all([Transform, Velocity]).iterate([Transform, Velocity])
##
## func process(entities: Array[Entity], components: Array, delta: float) -> void:
## # Batch processing with component arrays (faster)
## var transforms = components[0]
## var velocities = components[1]
## for i in entities.size():
## transforms[i].position += velocities[i].velocity * delta
##[/codeblock]
@icon("res://addons/gecs/assets/system.svg")
class_name System
extends Node
#region Enums
## These control when the system should run in relation to other systems.
enum Runs {
## This system should run before all the systems defined in the array ex: [TransformSystem] means it will run before the [TransformSystem] system runs
Before,
## This system should run after all the systems defined in the array ex: [TransformSystem] means it will run after the [TransformSystem] system runs
After,
}
#endregion Enums
#region Exported Variables
## What group this system belongs to. Systems can be organized and run by group
@export var group: String = ""
## Determines whether the system should run even when there are no [Entity]s to process.
@export var process_empty := false
## Is this system active. (Will be skipped if false)
@export var active := true
@export_group("Parallel Processing")
## Enable parallel processing for this system's entities (No access to scene tree in process method)
@export var parallel_processing := false
## Minimum entities required to use parallel processing (performance threshold)
@export var parallel_threshold := 50
#endregion Exported Variables
#region Public Variables
## Is this system paused. (Will be skipped if true)
var paused := false
## Logger for system debugging and tracing
var systemLogger = GECSLogger.new().domain("System")
## Data for debugger and profiling - you can add ANY arbitrary data here when ECS.debug is enabled
## All keys and values will automatically appear in the GECS debugger tab
## Example:
## if ECS.debug:
## lastRunData["my_counter"] = 123
## lastRunData["player_stats"] = {"health": 100, "mana": 50}
## lastRunData["events"] = ["event1", "event2"]
var lastRunData := {}
## Reference to the world this system belongs to (set by World.add_system)
var _world: World = null
## Convenience property for accessing query builder (returns _world.query or ECS.world.query)
var q: QueryBuilder:
get:
return _world.query if _world else ECS.world.query
## Cached query to avoid recreating it every frame (lazily initialized)
var _query_cache: QueryBuilder = null
## Cached component paths for iterate() fast path (6.0.0 style)
var _component_paths: Array[String] = []
## Cached subsystems array (6.0.0 style)
var _subsystems_cache: Array = []
#endregion Public Variables
#region Public Methods
## Override this method to define the [System]s that this system depends on.[br]
## If not overridden the system will run based on the order of the systems in the [World][br]
## and the order of the systems in the [World] will be based on the order they were added to the [World].[br]
func deps() -> Dictionary[int, Array]:
return {
Runs.After: [],
Runs.Before: [],
}
## Override this method and return a [QueryBuilder] to define the required [Component]s for the system.[br]
## If not overridden, the system will run on every update with no entities.[br][br]
## You can use [code]q[/code] or [code]ECS.world.query[/code] - both are equivalent.
func query() -> QueryBuilder:
process_empty = true
return _world.query if _world else ECS.world.query
## Override this method to define any sub-systems that should be processed by this system.[br]
## Each subsystem is defined as [QueryBuilder, Callable][br]
## Return empty array if not using subsystems (base implementation)[br][br]
## You can use [code]q[/code] or [code]ECS.world.query[/code] in subsystems - both work.[br][br]
## [b]Example:[/b]
## [codeblock]
## func sub_systems() -> Array[Array]:
## return [
## [q.with_all([C_Velocity]).iterate([C_Velocity]), process_velocity],
## [q.with_all([C_Health]), process_health]
## ]
##
## 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)
## [/codeblock]
func sub_systems() -> Array[Array]:
return [] # Base returns empty - overridden systems return populated Array[Array]
## Runs once after the system has been added to the [World] to setup anything on the system one time[br]
func setup():
pass # Override in subclasses if needed
## The main processing function for the system.[br]
## Override this method to define your system's behavior.[br]
## [param entities] Array of entities matching the system's query[br]
## [param components] Array of component arrays (in order from iterate()), or empty if no iterate() call[br]
## [param delta] The time elapsed since the last frame[br][br]
## [b]Simple approach:[/b] Loop through entities and use get_component()[br]
## [b]Fast approach:[/b] Use iterate() in query and access component arrays directly
func process(entities: Array[Entity], components: Array, delta: float) -> void:
pass # Override in subclasses - base implementation does nothing
#endregion Public Methods
#region Private Methods
## INTERNAL: Called by World.add_system() to initialize the system
## DO NOT CALL OR OVERRIDE - this is framework code
func _internal_setup():
# Call user setup
setup()
## Process entities in parallel using WorkerThreadPool
## Splits entities into batches and processes them concurrently
func _process_parallel(entities: Array[Entity], components: Array, delta: float) -> void:
if entities.is_empty():
return
# Use OS thread count as fallback since WorkerThreadPool.get_thread_count() doesn't exist
var worker_count = OS.get_processor_count()
var batch_size = max(1, entities.size() / worker_count)
var tasks = []
# Submit tasks for each batch
for batch_start in range(0, entities.size(), batch_size):
var batch_end = min(batch_start + batch_size, entities.size())
# Slice entities and components for this batch
var batch_entities = entities.slice(batch_start, batch_end)
var batch_components = []
for comp_array in components:
batch_components.append(comp_array.slice(batch_start, batch_end))
var task_id = WorkerThreadPool.add_task(_process_batch_callable.bind(batch_entities, batch_components, delta))
tasks.append(task_id)
# Wait for all tasks to complete
for task_id in tasks:
WorkerThreadPool.wait_for_task_completion(task_id)
## Process a batch of entities - called by worker threads
func _process_batch_callable(entities: Array[Entity], components: Array, delta: float) -> void:
process(entities, components, delta)
## Called by World.process() each frame - main entry point for system execution
## [param delta] The time elapsed since the last frame
func _handle(delta: float) -> void:
if not active or paused:
return
var start_time_usec := 0
if ECS.debug:
start_time_usec = Time.get_ticks_usec()
lastRunData = {
"system_name": get_script().resource_path.get_file().get_basename(),
"frame_delta": delta,
}
var subs = sub_systems()
if not subs.is_empty():
_run_subsystems(delta)
else:
_run_process(delta)
if ECS.debug:
var end_time_usec = Time.get_ticks_usec()
lastRunData["execution_time_ms"] = (end_time_usec - start_time_usec) / 1000.0
## UNIFIED execution function for both main systems and subsystems
## This ensures consistent behavior and entity processing logic
## Subsystems and main systems execute IDENTICALLY - no special behavior
## [param query_builder] The query to execute
## [param callable] The function to call with matched entities
## [param delta] Time delta
## [param subsystem_index] Index for debug tracking (-1 for main system)
func _run_subsystems(delta: float) -> void:
if _subsystems_cache.is_empty():
_subsystems_cache = sub_systems()
var subsystem_index := 0
for subsystem_tuple in _subsystems_cache:
var subsystem_query := subsystem_tuple[0] as QueryBuilder
var subsystem_callable := subsystem_tuple[1] as Callable
var uses_non_structural := _query_has_non_structural_filters(subsystem_query)
var iterate_comps = subsystem_query._iterate_components
if uses_non_structural:
# Gather ALL structural entities first then filter once (avoid per-archetype filtering churn)
var all_entities: Array[Entity] = []
for arch in subsystem_query.archetypes():
if not arch.entities.is_empty():
all_entities.append_array(arch.entities) # no snapshot to allow mid-frame changes visible to later subsystems
var filtered = _filter_entities_global(subsystem_query, all_entities)
if filtered.is_empty():
if ECS.debug:
lastRunData[subsystem_index] = {"subsystem_index": subsystem_index, "entity_count": 0, "fallback_execute": true}
subsystem_index += 1
continue
var components := []
if not iterate_comps.is_empty():
for comp_type in iterate_comps:
components.append(_build_component_column_from_entities(filtered, comp_type))
subsystem_callable.call(filtered, components, delta)
if ECS.debug:
lastRunData[subsystem_index] = {"subsystem_index": subsystem_index, "entity_count": filtered.size(), "fallback_execute": true}
else:
# Structural fast path archetype iteration
var total_entity_count := 0
for archetype in subsystem_query.archetypes():
if archetype.entities.is_empty():
continue
# Snapshot to avoid losing entities during add/remove component archetype moves mid-iteration
var arch_entities = archetype.entities.duplicate()
total_entity_count += arch_entities.size()
var components = []
if not iterate_comps.is_empty():
for comp_type in iterate_comps:
var comp_path = comp_type.resource_path if comp_type is Script else comp_type.get_script().resource_path
components.append(archetype.get_column(comp_path))
subsystem_callable.call(arch_entities, components, delta)
if ECS.debug:
lastRunData[subsystem_index] = {"subsystem_index": subsystem_index, "entity_count": total_entity_count, "fallback_execute": false}
subsystem_index += 1
func _run_process(delta: float) -> void:
if not _query_cache:
_query_cache = query()
if _component_paths.is_empty():
var iterate_comps = _query_cache._iterate_components
for comp_type in iterate_comps:
var comp_path = comp_type.resource_path if comp_type is Script else comp_type.get_script().resource_path
_component_paths.append(comp_path)
var uses_non_structural := _query_has_non_structural_filters(_query_cache)
var iterate_comps = _query_cache._iterate_components
if uses_non_structural:
# Gather all entities across structural archetypes and then filter once
var all_entities: Array[Entity] = []
for arch in _query_cache.archetypes():
if not arch.entities.is_empty():
all_entities.append_array(arch.entities)
if all_entities.is_empty():
if process_empty:
process([], [], delta)
return
var filtered = _filter_entities_global(_query_cache, all_entities)
if filtered.is_empty():
if process_empty:
process([], [], delta)
return
var components := []
if not iterate_comps.is_empty():
for comp_type in iterate_comps:
components.append(_build_component_column_from_entities(filtered, comp_type))
if parallel_processing and filtered.size() >= parallel_threshold:
_process_parallel(filtered, components, delta)
else:
process(filtered, components, delta)
if ECS.debug:
lastRunData["entity_count"] = filtered.size()
lastRunData["archetype_count"
] = _query_cache.archetypes().size()
lastRunData["fallback_execute"] = true
lastRunData["parallel"] = parallel_processing and filtered.size() >= parallel_threshold
return
# Structural fast path
var matching_archetypes = _query_cache.archetypes()
var has_entities = false
var total_entity_count := 0
for arch in matching_archetypes:
if not arch.entities.is_empty():
has_entities = true
total_entity_count += arch.entities.size()
if ECS.debug:
lastRunData["entity_count"] = total_entity_count
lastRunData["archetype_count"] = matching_archetypes.size()
lastRunData["fallback_execute"] = false
if not has_entities and not process_empty:
return
if not has_entities and process_empty:
process([], [], delta)
return
for arch in matching_archetypes:
var arch_entities = arch.entities
if arch_entities.is_empty():
continue
# Snapshot structural entities to avoid mutation skipping during component add/remove
var snapshot_entities = arch_entities.duplicate()
var components = []
if not iterate_comps.is_empty():
for comp_path in _component_paths:
components.append(arch.get_column(comp_path))
if parallel_processing and snapshot_entities.size() >= parallel_threshold:
if ECS.debug:
lastRunData["parallel"] = true
lastRunData["threshold"] = parallel_threshold
_process_parallel(snapshot_entities, components, delta)
else:
if ECS.debug:
lastRunData["parallel"] = false
process(snapshot_entities, components, delta)
## Determine if a query includes non-structural filters requiring execute() fallback
func _query_has_non_structural_filters(qb: QueryBuilder) -> bool:
if not qb._relationships.is_empty():
return true
if not qb._exclude_relationships.is_empty():
return true
if not qb._groups.is_empty():
return true
if not qb._exclude_groups.is_empty():
return true
# Component property queries (ensure actual queries, not placeholders)
if not qb._all_components_queries.is_empty():
for query in qb._all_components_queries:
if not query.is_empty():
return true
if not qb._any_components_queries.is_empty():
for query in qb._any_components_queries:
if not query.is_empty():
return true
return false
## Build component arrays for iterate() when falling back to execute() result (no archetype columns)
func _build_component_column_from_entities(entities: Array[Entity], comp_type) -> Array:
var out := []
for e in entities:
if e == null:
out.append(null)
continue
var comp = e.get_component(comp_type)
out.append(comp)
return out
## Filter entities in an archetype for non-structural query criteria (relationships/groups/property queries)
## Filter a flat entity array for non-structural criteria
func _filter_entities_global(qb: QueryBuilder, entities: Array[Entity]) -> Array[Entity]:
var result: Array[Entity] = []
for e in entities:
if e == null:
continue
var include := true
for rel in qb._relationships:
if not e.has_relationship(rel):
include = false; break
if include:
for ex_rel in qb._exclude_relationships:
if e.has_relationship(ex_rel):
include = false; break
if include and not qb._groups.is_empty():
for g in qb._groups:
if not e.is_in_group(g):
include = false; break
if include and not qb._exclude_groups.is_empty():
for g in qb._exclude_groups:
if e.is_in_group(g):
include = false; break
if include and not qb._all_components_queries.is_empty():
for i in range(qb._all_components.size()):
if i >= qb._all_components_queries.size():
break
var comp_type = qb._all_components[i]
var query = qb._all_components_queries[i]
if not query.is_empty():
var comp = e.get_component(comp_type)
if comp == null or not ComponentQueryMatcher.matches_query(comp, query):
include = false; break
if include and not qb._any_components_queries.is_empty():
var any_match := qb._any_components_queries.is_empty()
for i in range(qb._any_components.size()):
if i >= qb._any_components_queries.size():
break
var comp_type = qb._any_components[i]
var query = qb._any_components_queries[i]
if not query.is_empty():
var comp = e.get_component(comp_type)
if comp and ComponentQueryMatcher.matches_query(comp, query):
any_match = true; break
if not any_match and not qb._any_components.is_empty():
include = false
if include:
result.append(e)
return result
## Debug helper - updates lastRunData (compiled out in production)
func _update_debug_data(callable: Callable = func(): return {}) -> bool:
if ECS.debug:
var data = callable.call()
if data:
lastRunData.assign(data)
return true
## Debug helper - sets lastRunData (compiled out in production)
func _debug_data(_lrd: Dictionary, callable: Callable = func(): return {}) -> bool:
if ECS.debug:
lastRunData = _lrd
lastRunData.assign(callable.call())
return true
#endregion Private Methods

View File

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

1341
addons/gecs/ecs/world.gd Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,9 @@
class_name GecsData
extends Resource
@export var version: String = "0.2"
@export var entities: Array[GecsEntityData] = []
func _init(_entities: Array[GecsEntityData] = []):
entities = _entities

View File

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

View File

@@ -0,0 +1,18 @@
class_name GecsEntityData
extends Resource
@export var entity_name: String = ""
@export var scene_path: String = ""
@export var components: Array[Component] = []
@export var relationships: Array[GecsRelationshipData] = []
@export var auto_included: bool = false
@export var id: String = ""
func _init(_name: String = "", _scene_path: String = "", _components: Array[Component] = [], _relationships: Array[GecsRelationshipData] = [], _auto_included: bool = false, _id: String = ""):
entity_name = _name
scene_path = _scene_path
components = _components
relationships = _relationships
auto_included = _auto_included
id = _id

View File

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

View File

@@ -0,0 +1,95 @@
## GecsRelationshipData
## Resource class for serializing relationship data in GECS
##
## This class stores all the necessary information to recreate a [Relationship]
## during deserialization, including the relation component and target information.
class_name GecsRelationshipData
extends Resource
## The relation component data (duplicated for serialization)
@export var relation_data: Component
## The type of target this relationship points to
## Valid values: "Entity", "Component", "Script"
@export var target_type: String = ""
## The id of the target entity (used when target_type is "Entity")
@export var target_entity_id: String = ""
## The target component data (used when target_type is "Component")
@export var target_component_data: Component
## The resource path of the target script (used when target_type is "Script")
@export var target_script_path: String = ""
## Constructor to create relationship data from a Relationship instance
func _init(
_relation_data: Component = null,
_target_type: String = "",
_target_entity_id: String = "",
_target_component_data: Component = null,
_target_script_path: String = ""
):
relation_data = _relation_data
target_type = _target_type
target_entity_id = _target_entity_id
target_component_data = _target_component_data
target_script_path = _target_script_path
## Creates GecsRelationshipData from a Relationship instance
static func from_relationship(relationship: Relationship) -> GecsRelationshipData:
var data = GecsRelationshipData.new()
# Store relation component (duplicate to avoid reference issues)
if relationship.relation:
data.relation_data = relationship.relation.duplicate(true)
# Determine target type and store appropriate data
if relationship.target == null:
data.target_type = "null"
elif relationship.target is Entity:
data.target_type = "Entity"
data.target_entity_id = relationship.target.id
elif relationship.target is Component:
data.target_type = "Component"
data.target_component_data = relationship.target.duplicate(true)
elif relationship.target is Script:
data.target_type = "Script"
data.target_script_path = relationship.target.resource_path
else:
push_warning("GecsRelationshipData: Unknown target type: " + str(type_string(typeof(relationship.target))))
data.target_type = "unknown"
return data
## Recreates a Relationship from this data (requires entity mapping for Entity targets)
func to_relationship(entity_mapping: Dictionary = {}) -> Relationship:
var relationship = Relationship.new()
# Restore relation component
if relation_data:
relationship.relation = relation_data.duplicate(true)
# Restore target based on type
match target_type:
"null":
relationship.target = null
"Entity":
if target_entity_id in entity_mapping:
relationship.target = entity_mapping[target_entity_id]
else:
push_warning("GecsRelationshipData: Could not resolve entity with ID: " + target_entity_id)
return null
"Component":
if target_component_data:
relationship.target = target_component_data.duplicate(true)
"Script":
if target_script_path != "":
relationship.target = load(target_script_path)
_:
push_warning("GecsRelationshipData: Unknown target type during deserialization: " + target_type)
return null
return relationship

View File

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

219
addons/gecs/io/io.gd Normal file
View File

@@ -0,0 +1,219 @@
## GECS IO Utility Class[br]
##
## Provides functions for generating UUIDs, serializing/deserializing [Entity]s to/from [GecsData],
## and saving/loading [GecsData] to/from files.
class_name GECSIO
## Generates a custom GUID using random bytes.[br]
## The format uses 16 random bytes encoded to hex and formatted with hyphens.
static func uuid() -> String:
const BYTE_MASK: int = 0b11111111
# 16 random bytes with the bytes on index 6 and 8 modified
var b = [
randi() & BYTE_MASK, randi() & BYTE_MASK, randi() & BYTE_MASK, randi() & BYTE_MASK,
randi() & BYTE_MASK, randi() & BYTE_MASK, ((randi() & BYTE_MASK) & 0x0f) | 0x40, randi() & BYTE_MASK,
((randi() & BYTE_MASK) & 0x3f) | 0x80, randi() & BYTE_MASK, randi() & BYTE_MASK, randi() & BYTE_MASK,
randi() & BYTE_MASK, randi() & BYTE_MASK, randi() & BYTE_MASK, randi() & BYTE_MASK,
]
return '%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x' % [
# low
b[0], b[1], b[2], b[3],
# mid
b[4], b[5],
# hi
b[6], b[7],
# clock
b[8], b[9],
# clock
b[10], b[11], b[12], b[13], b[14], b[15]
]
## Serialize a [QueryBuilder] of [Entity](s) to [GecsData] format.[br]
## Optionally takes a [GECSSerializeConfig] to customize what gets serialized.
static func serialize(query: QueryBuilder, config: GECSSerializeConfig = null) -> GecsData:
return serialize_entities(query.execute() as Array[Entity], config)
## Serialize a list of [Entity](s) to [GecsData] format.[br]
## Optionally takes a [GECSSerializeConfig] to customize what gets serialized.
static func serialize_entities(entities: Array, config: GECSSerializeConfig = null) -> GecsData:
# Pass 1: Serialize entities from original query
var entity_data_array: Array[GecsEntityData] = []
var processed_entities: Dictionary = {} # id -> bool
var entity_id_mapping: Dictionary = {} # id -> Entity
for entity in entities:
var effective_config = _resolve_config(entity, config)
var entity_data = _serialize_entity(entity, false, effective_config)
entity_data_array.append(entity_data)
processed_entities[entity.id] = true
entity_id_mapping[entity.id] = entity
# Pass 2: Scan relationships and auto-include referenced entities (if enabled)
var entities_to_check = entities.duplicate()
var check_index = 0
while check_index < entities_to_check.size():
var entity = entities_to_check[check_index]
var effective_config = _resolve_config(entity, config)
# Only proceed if config allows including related entities
if effective_config.include_related_entities:
# Check all relationships of this entity
for relationship in entity.relationships:
if relationship.target is Entity:
var target_entity = relationship.target as Entity
var target_id = target_entity.id
# If this entity hasn't been processed yet, auto-include it
if not processed_entities.has(target_id):
var target_config = _resolve_config(target_entity, config)
var auto_entity_data = _serialize_entity(target_entity, true, target_config)
entity_data_array.append(auto_entity_data)
processed_entities[target_id] = true
entity_id_mapping[target_id] = target_entity
# Add to list for further relationship checking
entities_to_check.append(target_entity)
check_index += 1
return GecsData.new(entity_data_array)
## Save [GecsData] to a file at the specified path.[br]
## If binary is true, saves in binary format (.res), otherwise text format (.tres).
static func save(gecs_data: GecsData, filepath: String, binary: bool = false) -> bool:
var final_path = filepath
var flags = 0
if binary:
# Convert .tres to .res for binary format
final_path = filepath.replace(".tres", ".res")
flags = ResourceSaver.FLAG_COMPRESS # Binary format uses no flags, .res extension determines format
# else: text format (default flags = 0)
var result = ResourceSaver.save(gecs_data, final_path, flags)
if result != OK:
push_error("GECS save: Failed to save resource to: " + final_path)
return false
return true
## Load and deserialize [Entity](s) from a file at the specified path.[br]
## Supports both binary (.res) and text (.tres) formats, tries binary first.
static func deserialize(gecs_filepath: String) -> Array[Entity]:
# Try binary first (.res), then text (.tres)
var binary_path = gecs_filepath.replace(".tres", ".res")
if ResourceLoader.exists(binary_path):
return _load_from_path(binary_path)
elif ResourceLoader.exists(gecs_filepath):
return _load_from_path(gecs_filepath)
else:
push_error("GECS deserialize: File not found: " + gecs_filepath)
return []
## Deserialize [GecsData] into a list of [Entity](s).[br]
## This can be used so you can serialize entities to GECS Data and then Deserailize that [GecsSData] later
static func deserialize_gecs_data(gecs_data: GecsData) -> Array[Entity]:
var entities: Array[Entity] = []
var id_to_entity: Dictionary = {} # id -> Entity
# Pass 1: Create all entities and build ID mapping
for entity_data in gecs_data.entities:
var entity = _deserialize_entity(entity_data)
entities.append(entity)
id_to_entity[entity.id] = entity
# Pass 2: Restore relationships using ID mapping
for i in entities.size():
var entity = entities[i]
var entity_data = gecs_data.entities[i]
# Restore relationships
for rel_data in entity_data.relationships:
var relationship = rel_data.to_relationship(id_to_entity)
if relationship != null:
entity.add_relationship(relationship)
# Note: Invalid relationships are skipped with warning logged in to_relationship()
return entities
## Helper function to resolve the effective configuration for an entity
## Priority: provided_config > entity.serialize_config > world.default_serialize_config > fallback
static func _resolve_config(entity: Entity, provided_config: GECSSerializeConfig) -> GECSSerializeConfig:
if provided_config != null:
return provided_config
return entity.get_effective_serialize_config()
## Helper function to serialize a single entity with its components and relationships
static func _serialize_entity(entity: Entity, auto_included: bool, config: GECSSerializeConfig) -> GecsEntityData:
# Serialize components (filtered by config)
var components: Array[Component] = []
for component in entity.components.values():
if config.should_include_component(component):
# Duplicate the component to avoid modifying the original
components.append(component.duplicate(true))
# Serialize relationships (if enabled by config)
var relationships: Array[GecsRelationshipData] = []
if config.include_relationships:
for relationship in entity.relationships:
var rel_data = GecsRelationshipData.from_relationship(relationship)
relationships.append(rel_data)
return GecsEntityData.new(
entity.name,
entity.scene_file_path if entity.scene_file_path != "" else "",
components,
relationships,
auto_included,
entity.id
)
## Helper function to load and deserialize entities from a given file path
static func _load_from_path(file_path: String) -> Array[Entity]:
print("GECS _load_from_path: Loading file: ", file_path)
var gecs_data = load(file_path) as GecsData
if not gecs_data:
push_error("GECS deserialize: Could not load GecsData resource: " + file_path)
return []
print("GECS _load_from_path: Loaded GecsData with ", gecs_data.entities.size(), " entities")
return deserialize_gecs_data(gecs_data)
## Helper function to deserialize a single entity with its components and uuid
static func _deserialize_entity(entity_data: GecsEntityData) -> Entity:
var entity: Entity
# Check if this entity is a prefab (has scene file)
if entity_data.scene_path != "":
var scene_path = entity_data.scene_path
if ResourceLoader.exists(scene_path):
var packed_scene = load(scene_path) as PackedScene
if packed_scene:
entity = packed_scene.instantiate() as Entity
else:
push_warning("GECS deserialize: Could not load scene: " + scene_path + ", creating new entity")
entity = Entity.new()
else:
push_warning("GECS deserialize: Scene file not found: " + scene_path + ", creating new entity")
entity = Entity.new()
else:
# Create new entity
entity = Entity.new()
# Set entity name
entity.name = entity_data.entity_name
# Restore id (important: set this directly)
entity.id = entity_data.id
# Add components (they're already properly typed as Component resources)
for component in entity_data.components:
entity.add_component(component.duplicate(true))
return entity

1
addons/gecs/io/io.gd.uid Normal file
View File

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

View File

@@ -0,0 +1,33 @@
## This config defines what to include when serializing
## It can be appled to the world as a whole or to specific entities
## This way you can define project level defaults and override them for specific cases
class_name GECSSerializeConfig
extends Resource
## Include all components (true) or only specific components (false)
@export var include_all_components: bool = true
## Which component types to include in serialization (only used when include_all_components = false)
@export var components: Array = []
## Whether to include relationships in serialization
@export var include_relationships: bool = true
## Whether to include related entities in serialization (Related entities are entities referenced by relationships from the serialized entities)
@export var include_related_entities: bool = true
## Helper method to determine if a component should be included in serialization
func should_include_component(component: Component) -> bool:
var comp_type = component.get_script()
return include_all_components or components.any(func(type): return comp_type == type)
## Merge this config with another config, with the other config taking priority
func merge_with(other: GECSSerializeConfig) -> GECSSerializeConfig:
if other == null:
return self
var merged = GECSSerializeConfig.new()
merged.include_all_components = other.include_all_components
merged.components = other.components.duplicate()
merged.include_relationships = other.include_relationships
merged.include_related_entities = other.include_related_entities
return merged

View File

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

View File

@@ -0,0 +1,191 @@
class_name ArrayExtensions
## Intersects two arrays of entities.[br]
## In common terms, use this to find items appearing in both arrays.
## [param array1] The first array to intersect.[br]
## [param array2] The second array to intersect.[br]
## [b]return Array[/b] The intersection of the two arrays.
static func intersect(array1: Array, array2: Array) -> Array:
# Optimize by using the smaller array for lookup
if array1.size() > array2.size():
return intersect(array2, array1)
# Use dictionary for O(1) lookup instead of O(n) Array.has()
var lookup := {}
for entity in array2:
lookup[entity] = true
var result: Array = []
for entity in array1:
if lookup.has(entity):
result.append(entity)
return result
## Unions two arrays of entities.[br]
## In common terms, use this to combine items without duplicates.[br]
## [param array1] The first array to union.[br]
## [param array2] The second array to union.[br]
## [b]return Array[/b] The union of the two arrays.
static func union(array1: Array, array2: Array) -> Array:
# Use dictionary to track uniqueness for O(1) lookups
var seen := {}
var result: Array = []
# Add all from array1
for entity in array1:
if not seen.has(entity):
seen[entity] = true
result.append(entity)
# Add unique items from array2
for entity in array2:
if not seen.has(entity):
seen[entity] = true
result.append(entity)
return result
## Differences two arrays of entities.[br]
## In common terms, use this to find items only in the first array.[br]
## [param array1] The first array to difference.[br]
## [param array2] The second array to difference.[br]
## [b]return Array[/b] The difference of the two arrays (entities in array1 not in array2).
static func difference(array1: Array, array2: Array) -> Array:
# Use dictionary for O(1) lookup instead of O(n) Array.has()
var lookup := {}
for entity in array2:
lookup[entity] = true
var result: Array = []
for entity in array1:
if not lookup.has(entity):
result.append(entity)
return result
## systems_by_group is a dictionary of system groups and their systems
## { "Group1": [SystemA, SystemB], "Group2": [SystemC, SystemD] }
static func topological_sort(systems_by_group: Dictionary) -> void:
# Iterate over each group key in 'systems_by_group'
for group in systems_by_group.keys():
var systems = systems_by_group[group]
# If the group has 1 or fewer systems, no sorting is needed
if systems.size() <= 1:
continue
# Create two data structures:
# 1) adjacency: stores, for a given system, which systems must come after it
# 2) indegree: tracks how many "prerequisite" systems each system has
var adjacency = {}
var indegree = {}
var wildcard_front = []
var wildcard_back = []
for s in systems:
adjacency[s] = []
indegree[s] = 0
# Build adjacency and indegree counts based on dependencies returned by s.deps()
for s in systems:
var deps_dict = s.deps()
# Check for Runs.Before array on s
# If present, each item in s.Runs.Before means "s must run before that item"
# So we add the item to adjacency[s], and increment the item's indegree
# If item is null or ECS.wildcard, we treat it as "run before everything" by pushing 's' onto wildcard_front
if deps_dict.has(System.Runs.Before):
for b in deps_dict[System.Runs.Before]:
if b == null:
# ECS.wildcard AKA 'null' means s should run before all systems
wildcard_front.append(s)
else:
# Find system instance that matches the dependency type
var target_system = _find_system_by_type(systems, b)
if target_system:
# Normal dependency within the group
adjacency[s].append(target_system)
indegree[target_system] += 1
# Check for Runs.After array on s
# If present, each item in s.Runs.After means "s must run after that item"
# So we add 's' to adjacency[item], and increment s's indegree
# If item is null or ECS.wildcard, we treat it as "run after everything" by pushing 's' onto wildcard_back
if deps_dict.has(System.Runs.After):
for a in deps_dict[System.Runs.After]:
if a == null:
# ECS.wildcard AKA 'null' means s should run after all systems
wildcard_back.append(s)
else:
# Find system instance that matches the dependency type
var dependency_system = _find_system_by_type(systems, a)
if dependency_system:
# Normal dependency within the group
adjacency[dependency_system].append(s)
indegree[s] += 1
# Kahn's Algorithm begins:
# 1) Insert all systems with zero indegree into a queue
# 2) Pop from the queue, add to sorted_result
# 3) Decrement indegree for each adjacent system
# 4) Any adjacent system that reaches zero indegree is appended to the queue
var queue = []
# Adjust for wildcard_front and wildcard_back:
# wildcard_front: s runs before everything -> point s -> other
for w in wildcard_front:
for other in systems:
if other != w and not adjacency[w].has(other):
adjacency[w].append(other)
indegree[other] += 1
# wildcard_back: s runs after everything -> point other -> s
for w in wildcard_back:
for other in systems:
if other != w and not adjacency[other].has(w):
adjacency[other].append(w)
indegree[w] += 1
# Collect all systems with zero indegree into the queue as our starting point
for s in systems:
if indegree[s] == 0:
queue.append(s)
var sorted_result = []
# While there are systems with no remaining prerequisites
while queue.size() > 0:
var current = queue.pop_front()
# Add that system to the sorted list
sorted_result.append(current)
# For each system that depends on 'current'
for nxt in adjacency[current]:
# Decrement its indegree because 'current' is now accounted for
indegree[nxt] -= 1
# If it has no more prerequisites, add it to the queue
if indegree[nxt] == 0:
queue.append(nxt)
# If we successfully placed all systems, overwrite the original array with sorted_result
if sorted_result.size() == systems.size():
systems_by_group[group] = sorted_result
else:
assert(
false,
(
"Topological sort failed for group '%s'. Possible cycle or mismatch in dependencies."
% group
)
)
# Otherwise, we found a cycle or mismatch. Fallback to the original unsorted array
systems_by_group[group] = systems
# The function modifies 'systems_by_group' in-place with a topologically sorted order
## Helper function to find a system instance by its type/class
static func _find_system_by_type(systems: Array, target_type) -> System:
for system in systems:
# Check if the system is an instance of the target type
if system.get_script() == target_type:
return system
# Also check class name matching for backward compatibility
if system.get_script() and system.get_script().get_global_name() == str(target_type).get_file().get_basename():
return system
return null

View File

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

View File

@@ -0,0 +1,108 @@
## ComponentQueryMatcher
## Static utility for matching components against query criteria.
## Used by QueryBuilder and Relationship systems for consistent component filtering.
##
## Supports comparison operators (_gt, _lt, _eq), array membership (_in, _nin),
## and custom functions for property-based filtering.
##
## [b]Query Operators:[/b]
## [br]• [b]_eq:[/b] Equal [code]property == value[/code]
## [br]• [b]_ne:[/b] Not equal [code]property != value[/code]
## [br]• [b]_gt:[/b] Greater than [code]property > value[/code]
## [br]• [b]_lt:[/b] Less than [code]property < value[/code]
## [br]• [b]_gte:[/b] Greater or equal [code]property >= value[/code]
## [br]• [b]_lte:[/b] Less or equal [code]property <= value[/code]
## [br]• [b]_in:[/b] In array [code]property in [values][/code]
## [br]• [b]_nin:[/b] Not in array [code]property not in [values][/code]
## [br]• [b]func:[/b] Custom function [code]func(property) -> bool[/code]
##
## [codeblock]
## var component = C_Health.new(75)
## var query = {"health": {"_gte": 50, "_lte": 100}}
## var matches = ComponentQueryMatcher.matches_query(component, query)
##
## # Custom functions
## var func_query = {"level": {"func": func(level): return level >= 40}}
##
## # Array membership
## var type_query = {"type": {"_in": ["fire", "ice"]}}
## [/codeblock]
class_name ComponentQueryMatcher
extends RefCounted
## Checks if a component matches the given query criteria.
## All query operators must pass for the component to match.
##
## [param component]: The [Component] to evaluate
## [param query]: Dictionary mapping property names to operator dictionaries
## [return]: [code]true[/code] if all criteria match, [code]false[/code] otherwise
##
## Returns [code]true[/code] for empty queries. Returns [code]false[/code] if any
## property doesn't exist or any operator fails.
static func matches_query(component: Component, query: Dictionary) -> bool:
if query.is_empty():
return true
for property in query:
# Check if property exists (can't use truthiness check because 0, false, etc. are valid values)
if not property in component:
return false
var property_value = component.get(property)
var property_query = query[property]
for operator in property_query:
match operator:
"func":
if not property_query[operator].call(property_value):
return false
"_eq":
if property_value != property_query[operator]:
return false
"_gt":
if property_value <= property_query[operator]:
return false
"_lt":
if property_value >= property_query[operator]:
return false
"_gte":
if property_value < property_query[operator]:
return false
"_lte":
if property_value > property_query[operator]:
return false
"_ne":
if property_value == property_query[operator]:
return false
"_nin":
if property_value in property_query[operator]:
return false
"_in":
if not (property_value in property_query[operator]):
return false
return true
## Separates component types from query dictionaries in a mixed array.
## Used by QueryBuilder to process component lists that may contain queries.
##
## [param components]: Array of [Component] classes and/or query dictionaries
## [return]: Dictionary with [code]"components"[/code] and [code]"queries"[/code] arrays
##
## Regular components get empty query dictionaries. Query dictionaries are
## split into their component type and criteria.
static func process_component_list(components: Array) -> Dictionary:
var result := {"components": [], "queries": []}
for component in components:
if component is Dictionary:
# Handle component query case
for component_type in component:
result.components.append(component_type)
result.queries.append(component[component_type])
else:
# Handle regular component case
result.components.append(component)
result.queries.append({}) # Empty query for regular components (matches all)
return result

View File

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

View File

@@ -0,0 +1,26 @@
class_name GecsSettings
extends Node
const SETTINGS_LOG_LEVEL = "gecs/settings/log_level"
const SETTINGS_DEBUG_MODE = "gecs/settings/debug_mode"
const project_settings = {
"log_level":
{
"path": SETTINGS_LOG_LEVEL,
"default_value": GECSLogger.LogLevel.ERROR,
"type": TYPE_INT,
"hint": PROPERTY_HINT_ENUM,
"hint_string": "TRACE,DEBUG,INFO,WARNING,ERROR",
"doc": "What log level GECS should log at.",
},
"debug_mode":
{
"path": SETTINGS_DEBUG_MODE,
"default_value": false,
"type": TYPE_BOOL,
"hint": PROPERTY_HINT_NONE,
"hint_string": "",
"doc": "Enable debug mode for GECS operations. Enables editor debugger integration but impacts performance significantly.",
}
}

View File

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

90
addons/gecs/lib/logger.gd Normal file
View File

@@ -0,0 +1,90 @@
## Simplified Logger for GECS
class_name GECSLogger
extends RefCounted
const disabled := true
enum LogLevel {TRACE, DEBUG, INFO, WARNING, ERROR}
var current_level: LogLevel = ProjectSettings.get_setting(GecsSettings.SETTINGS_LOG_LEVEL, LogLevel.ERROR)
var current_domain: String = ""
func set_level(level: LogLevel):
current_level = level
func domain(domain_name: String) -> GECSLogger:
current_domain = domain_name
return self
func log(level: LogLevel, msg = ""):
if disabled:
return
var level_name: String
if level >= current_level:
match level:
LogLevel.TRACE:
level_name = "TRACE"
LogLevel.DEBUG:
level_name = "DEBUG"
LogLevel.INFO:
level_name = "INFO"
LogLevel.WARNING:
level_name = "WARNING"
LogLevel.ERROR:
level_name = "ERROR"
_:
level_name = "UNKNOWN"
print("%s [%s]: %s" % [current_domain, level_name, msg])
func trace(msg = "", arg1 = null, arg2 = null, arg3 = null, arg4 = null, arg5 = null):
self.log(LogLevel.TRACE, concatenate_msg_and_args(msg, arg1, arg2, arg3, arg4, arg5))
func debug(msg = "", arg1 = null, arg2 = null, arg3 = null, arg4 = null, arg5 = null):
self.log(LogLevel.DEBUG, concatenate_msg_and_args(msg, arg1, arg2, arg3, arg4, arg5))
func info(msg = "", arg1 = null, arg2 = null, arg3 = null, arg4 = null, arg5 = null):
self.log(LogLevel.INFO, concatenate_msg_and_args(msg, arg1, arg2, arg3, arg4, arg5))
func warning(msg = "", arg1 = null, arg2 = null, arg3 = null, arg4 = null, arg5 = null):
self.log(LogLevel.WARNING, concatenate_msg_and_args(msg, arg1, arg2, arg3, arg4, arg5))
func error(msg = "", arg1 = null, arg2 = null, arg3 = null, arg4 = null, arg5 = null):
self.log(LogLevel.ERROR, concatenate_msg_and_args(msg, arg1, arg2, arg3, arg4, arg5))
## Concatenates all given args into one single string, in consecutive order starting with 'msg'.[br]
## Stolen from Loggie
static func concatenate_msg_and_args(
msg: Variant,
arg1: Variant = null,
arg2: Variant = null,
arg3: Variant = null,
arg4: Variant = null,
arg5: Variant = null,
arg6: Variant = null
) -> String:
var final_msg = convert_to_string(msg)
var arguments = [arg1, arg2, arg3, arg4, arg5, arg6]
for arg in arguments:
if arg != null:
final_msg += (" " + convert_to_string(arg))
return final_msg
## Converts [param something] into a string.[br]
## If [param something] is a Dictionary, uses a special way to convert it into a string.[br]
## You can add more exceptions and rules for how different things are converted to strings here.[br]
## Stolen from Loggie
static func convert_to_string(something: Variant) -> String:
var result: String
if something is Dictionary:
result = JSON.new().stringify(something, " ", false, true)
else:
result = str(something)
return result

View File

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

321
addons/gecs/lib/set.gd Normal file
View File

@@ -0,0 +1,321 @@
## Set is Mathematical set data structure for collections of unique values.[br]
##
## Built on Dictionary for O(1) membership testing. Used throughout GECS for
## entity filtering and component indexing.
##
## Supports standard set operations like union, intersection, and difference.
## No inherent ordering - elements are stored by hash.
##
## [codeblock]
## var numbers = Set.new([1, 2, 3, 4, 5])
## numbers.add(6)
## print(numbers.has(3)) # true
##
## var set_a = Set.new([1, 2, 3, 4])
## var set_b = Set.new([3, 4, 5, 6])
## var intersection = set_a.intersect(set_b) # [3, 4]
## [/codeblock]
class_name Set
extends RefCounted
## Internal storage using Dictionary keys for O(1) average-case operations.
## Values in the dictionary are always [code]true[/code] and ignored.
var _data: Dictionary = {}
## Initializes a new Set from Array, Dictionary keys, or another Set.
## [param data]: Optional initial data. Duplicates are automatically removed.
func _init(data = null) -> void:
if data:
if data is Array:
# Add array elements, automatically deduplicating
for value in data:
_data[value] = true
elif data is Set:
# Copy from another set
for value in data._data.keys():
_data[value] = true
elif data is Dictionary:
# Use dictionary keys as set elements
for key in data.keys():
_data[key] = true
#region Basic Set Operations
## Adds a value to the set. Has no effect if the value is already present.
## [param value]: The value to add to the set. Can be any hashable type.
##
## [b]Time Complexity:[/b] O(1) average case
## [codeblock]
## var my_set = Set.new([1, 2, 3])
## my_set.add(4) # Set now contains [1, 2, 3, 4]
## my_set.add(2) # No change, 2 already exists
## [/codeblock]
func add(value) -> void:
_data[value] = true
## Removes a value from the set. Has no effect if the value is not present.
## [param value]: The value to remove from the set
##
## [b]Time Complexity:[/b] O(1) average case
## [codeblock]
## var my_set = Set.new([1, 2, 3, 4])
## my_set.erase(3) # Set now contains [1, 2, 4]
## my_set.erase(5) # No change, 5 doesn't exist
## [/codeblock]
func erase(value) -> void:
_data.erase(value)
## Tests whether a value exists in the set.
## [param value]: The value to test for membership
## [return]: [code]true[/code] if the value exists in the set, [code]false[/code] otherwise
##
## [b]Time Complexity:[/b] O(1) average case
## [codeblock]
## var my_set = Set.new(["apple", "banana", "cherry"])
## print(my_set.has("banana")) # true
## print(my_set.has("grape")) # false
## [/codeblock]
func has(value) -> bool:
return _data.has(value)
## Removes all elements from the set, making it empty.
## [b]Time Complexity:[/b] O(1)
## [codeblock]
## var my_set = Set.new([1, 2, 3, 4, 5])
## my_set.clear()
## print(my_set.is_empty()) # true
## [/codeblock]
func clear() -> void:
_data.clear()
## Returns the number of elements in the set.
## [return]: Integer count of unique elements in the set
##
## [b]Time Complexity:[/b] O(1)
## [codeblock]
## var my_set = Set.new(["a", "b", "c", "a", "b"]) # Duplicates ignored
## print(my_set.size()) # 3
## [/codeblock]
func size() -> int:
return _data.size()
## Tests whether the set contains no elements.
## [return]: [code]true[/code] if the set is empty, [code]false[/code] otherwise
##
## [b]Time Complexity:[/b] O(1)
## [codeblock]
## var empty_set = Set.new()
## var filled_set = Set.new([1, 2, 3])
## print(empty_set.is_empty()) # true
## print(filled_set.is_empty()) # false
## [/codeblock]
func is_empty() -> bool:
return _data.is_empty()
## Returns all elements in the set as an Array.
## The order of elements is not guaranteed and may vary between calls.
## [return]: Array containing all set elements
##
## [b]Time Complexity:[/b] O(n) where n is the number of elements
## [codeblock]
## var my_set = Set.new([3, 1, 4, 1, 5])
## var elements = my_set.values() # [1, 3, 4, 5] (order may vary)
## [/codeblock]
func values() -> Array:
return _data.keys()
#endregion
#region Set Algebra Operations
## Returns the union of this set with another set (A B).
## Creates a new set containing all elements that exist in either set.
## [param other]: The other set to union with
## [return]: New [Set] containing all elements from both sets
##
## [b]Time Complexity:[/b] O(|A| + |B|) where |A| and |B| are set sizes
## [codeblock]
## var set_a = Set.new([1, 2, 3])
## var set_b = Set.new([3, 4, 5])
## var union_set = set_a.union(set_b) # Contains [1, 2, 3, 4, 5]
## [/codeblock]
func union(other: Set) -> Set:
var result = Set.new()
result._data = _data.duplicate()
for key in other._data.keys():
result._data[key] = true
return result
## Returns the intersection of this set with another set (A ∩ B).
## Creates a new set containing only elements that exist in both sets.
## Automatically optimizes by iterating over the smaller set.
## [param other]: The other set to intersect with
## [return]: New [Set] containing elements common to both sets
##
## [b]Time Complexity:[/b] O(min(|A|, |B|)) - optimized for smaller set
## [codeblock]
## var set_a = Set.new([1, 2, 3, 4])
## var set_b = Set.new([3, 4, 5, 6])
## var intersection = set_a.intersect(set_b) # Contains [3, 4]
## [/codeblock]
func intersect(other: Set) -> Set:
# Optimization: iterate over smaller set for better performance
if other.size() < _data.size():
return other.intersect(self )
var result = Set.new()
for key in _data.keys():
if other._data.has(key):
result._data[key] = true
return result
## Returns the difference of this set minus another set (A - B).
## Creates a new set containing elements in this set but not in the other.
## [param other]: The set whose elements to exclude
## [return]: New [Set] containing elements only in this set
##
## [b]Time Complexity:[/b] O(|A|) where |A| is the size of this set
## [codeblock]
## var set_a = Set.new([1, 2, 3, 4])
## var set_b = Set.new([3, 4, 5, 6])
## var difference = set_a.difference(set_b) # Contains [1, 2]
## [/codeblock]
func difference(other: Set) -> Set:
var result = Set.new()
for key in _data.keys():
if not other._data.has(key):
result._data[key] = true
return result
## Returns the symmetric difference of this set with another set (A ⊕ B).
## Creates a new set containing elements in either set, but not in both.
## Equivalent to (A - B) (B - A).
## [param other]: The other set for symmetric difference
## [return]: New [Set] containing elements in exactly one of the two sets
##
## [b]Time Complexity:[/b] O(|A| + |B|)
## [codeblock]
## var set_a = Set.new([1, 2, 3, 4])
## var set_b = Set.new([3, 4, 5, 6])
## var sym_diff = set_a.symmetric_difference(set_b) # Contains [1, 2, 5, 6]
## [/codeblock]
func symmetric_difference(other: Set) -> Set:
var result = Set.new()
# Add elements from this set that aren't in other
for key in _data.keys():
if not other._data.has(key):
result._data[key] = true
# Add elements from other set that aren't in this set
for key in other._data.keys():
if not _data.has(key):
result._data[key] = true
return result
#endregion
#region Set Relationship Testing
## Tests whether this set is a subset of another set (A ⊆ B).
## Returns [code]true[/code] if every element in this set also exists in the other set.
## [param other]: The potential superset to test against
## [return]: [code]true[/code] if this set is a subset of other, [code]false[/code] otherwise
##
## [b]Time Complexity:[/b] O(|A|) where |A| is the size of this set
## [codeblock]
## var small_set = Set.new([1, 2])
## var large_set = Set.new([1, 2, 3, 4, 5])
## print(small_set.is_subset(large_set)) # true
## print(large_set.is_subset(small_set)) # false
## [/codeblock]
func is_subset(other: Set) -> bool:
for key in _data.keys():
if not other._data.has(key):
return false
return true
## Tests whether this set is a superset of another set (A ⊇ B).
## Returns [code]true[/code] if this set contains every element from the other set.
## [param other]: The potential subset to test
## [return]: [code]true[/code] if this set is a superset of other, [code]false[/code] otherwise
##
## [b]Time Complexity:[/b] O(|B|) where |B| is the size of the other set
## [codeblock]
## var large_set = Set.new([1, 2, 3, 4, 5])
## var small_set = Set.new([2, 4])
## print(large_set.is_superset(small_set)) # true
## [/codeblock]
func is_superset(other: Set) -> bool:
return other.is_subset(self )
## Tests whether this set contains exactly the same elements as another set (A = B).
## Two sets are equal if they have the same size and this set is a subset of the other.
## [param other]: The set to compare for equality
## [return]: [code]true[/code] if sets contain identical elements, [code]false[/code] otherwise
##
## [b]Time Complexity:[/b] O(min(|A|, |B|)) - fails fast on size mismatch
## [codeblock]
## var set_a = Set.new([1, 2, 3])
## var set_b = Set.new([3, 1, 2]) # Order doesn't matter
## var set_c = Set.new([1, 2, 3, 4])
## print(set_a.is_equal(set_b)) # true
## print(set_a.is_equal(set_c)) # false
## [/codeblock]
func is_equal(other) -> bool:
# Quick size check for early exit
if _data.size() != other._data.size():
return false
return self.is_subset(other)
#endregion
#region Utility Methods
## Creates a shallow copy of this set.
## The returned set is independent - modifications to either set won't affect the other.
## However, if the set contains reference types, the references are shared.
## [return]: New [Set] containing the same elements
##
## [b]Time Complexity:[/b] O(n) where n is the number of elements
## [codeblock]
## var original = Set.new([1, 2, 3])
## var copy = original.duplicate()
## copy.add(4) # Only affects copy
## print(original.size()) # 3
## print(copy.size()) # 4
## [/codeblock]
func duplicate() -> Set:
var result = Set.new()
result._data = _data.duplicate()
return result
## Converts the set to an Array containing all elements.
## This is an alias for [method values] provided for API consistency.
## The order of elements is not guaranteed.
## [return]: Array containing all set elements
##
## [b]Time Complexity:[/b] O(n) where n is the number of elements
## [codeblock]
## var my_set = Set.new(["x", "y", "z"])
## var array = my_set.to_array() # ["x", "y", "z"] (order may vary)
## [/codeblock]
func to_array() -> Array:
return _data.keys()
#endregion

View File

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

View File

@@ -0,0 +1,34 @@
## This is a node that automatically fills in the [member System.group] property
## of any [System] that is a child of this node.
## Allowing you to visually organize your systems in the scene tree
## without having to manually set the group property on each [System].
## Add this node to your scene tree and make [System]s children of it.
## The name of the SystemGroup node will be set to [member System.group] for
## all child [System]s.
@tool
@icon('res://addons/gecs/assets/system_folder.svg')
class_name SystemGroup
extends Node
## Put the [System]s in the group based on the [member Node.name] of the [SystemGroup]
@export var auto_group := true
## called when the node enters the scene tree for the first time.
func _enter_tree() -> void:
# Connect signals
if not child_entered_tree.is_connected(_on_child_entered_tree):
child_entered_tree.connect(_on_child_entered_tree)
# Set the group for all child systems
if auto_group:
for child in get_children():
if child is System:
child.group = name
## Anytime a child enters the tree, set its group if it's a System
func _on_child_entered_tree(node: Node) -> void:
if auto_group:
if node is System:
node.group = name

View File

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

6
addons/gecs/plugin.cfg Normal file
View File

@@ -0,0 +1,6 @@
[plugin]
name="GECS"
description="GECS - Godot Entity Component System"
author="Quantum Tangent Games"
version="6.7.2"
script="plugin.gd"

72
addons/gecs/plugin.gd Normal file
View File

@@ -0,0 +1,72 @@
@tool
extends EditorPlugin
var gecs_editor_debugger = preload("res://addons/gecs/debug/gecs_editor_debugger.gd").new()
func _enter_tree():
add_autoload_singleton("ECS", "res://addons/gecs/ecs/ecs.gd")
# Pass editor interface to debugger so it can select nodes
gecs_editor_debugger.editor_interface = get_editor_interface()
add_debugger_plugin(gecs_editor_debugger)
add_gecs_project_settings()
func _exit_tree():
remove_autoload_singleton("ECS")
remove_debugger_plugin(gecs_editor_debugger)
# remove_gecs_project_setings()
func _on_settings_changed():
pass
## Adds a new project setting to Godot.
## TODO: Figure out how to also add the documentation to the ProjectSetting so that it shows up
## in the Godot Editor tooltip when the setting is hovered over.
func add_project_setting(
setting_name: String,
default_value: Variant,
value_type: int,
type_hint: int = PROPERTY_HINT_NONE,
hint_string: String = "",
documentation: String = ""
):
if !ProjectSettings.has_setting(setting_name):
ProjectSettings.set_setting(setting_name, default_value)
ProjectSettings.set_initial_value(setting_name, default_value)
ProjectSettings.add_property_info(
{"name": setting_name, "type": value_type, "hint": type_hint, "hint_string": hint_string}
)
ProjectSettings.set_as_basic(setting_name, true)
var error: int = ProjectSettings.save()
if error:
push_error("GECS - Encountered error %d while saving project settings." % error)
## Adds new GECS related ProjectSettings to Godot.
func add_gecs_project_settings():
ProjectSettings.settings_changed.connect(_on_settings_changed)
for setting in GecsSettings.project_settings.values():
add_project_setting(
setting["path"],
setting["default_value"],
setting["type"],
setting["hint"],
setting["hint_string"],
setting["doc"]
)
## Removes GECS related ProjectSettings from Godot.
func remove_gecs_project_setings():
ProjectSettings.settings_changed.disconnect(_on_settings_changed)
for setting in GecsSettings.project_settings.values():
ProjectSettings.set_setting(setting["path"], null)
var error: int = ProjectSettings.save()
if error != OK:
push_error("GECS - Encountered error %d while saving project settings." % error)

View File

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

View File

@@ -0,0 +1,21 @@
class_name C_ComplexSerializationTest
extends Component
@export var array_value: Array[int] = [1, 2, 3, 4, 5]
@export var string_array: Array[String] = ["hello", "world", "test"]
@export var dict_value: Dictionary = {"key1": "value1", "key2": 123, "key3": true}
@export var empty_array: Array = []
@export var empty_dict: Dictionary = {}
func _init(
_array_value: Array[int] = [1, 2, 3, 4, 5],
_string_array: Array[String] = ["hello", "world", "test"],
_dict_value: Dictionary = {"key1": "value1", "key2": 123, "key3": true},
_empty_array: Array = [],
_empty_dict: Dictionary = {}
):
array_value = _array_value
string_array = _string_array
dict_value = _dict_value
empty_array = _empty_array
empty_dict = _empty_dict

View File

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

View File

@@ -0,0 +1,4 @@
class_name C_DebugTrackingTestA
extends Component
@export var value: float = 0.0

View File

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

View File

@@ -0,0 +1,4 @@
class_name C_DebugTrackingTestB
extends Component
@export var count: int = 0

View File

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

View File

@@ -0,0 +1,3 @@
class_name C_DomainTestA
extends Component
@export var v_a: int = 1

View File

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

View File

@@ -0,0 +1,3 @@
class_name C_DomainTestB
extends Component
@export var v_b: int = 2

View File

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

View File

@@ -0,0 +1,22 @@
## Test health component for observer tests with proper property_changed signal emission
class_name C_ObserverHealth
extends Component
@export var health: int = 100 : set = set_health
@export var max_health: int = 100 : set = set_max_health
func set_health(new_health: int):
var old_health = health
health = new_health
# Emit signal for observers to detect the change
property_changed.emit(self, "health", old_health, new_health)
func set_max_health(new_max: int):
var old_max = max_health
max_health = new_max
# Emit signal for observers to detect the change
property_changed.emit(self, "max_health", old_max, new_max)
func _init(_health: int = 100, _max_health: int = 100):
health = _health
max_health = _max_health

View File

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

View File

@@ -0,0 +1,22 @@
## Test component for observer tests with proper property_changed signal emission
class_name C_ObserverTest
extends Component
@export var value: int = 0 : set = set_value
@export var name_prop: String = "" : set = set_name_prop
func set_value(new_value: int):
var old_value = value
value = new_value
# Emit signal for observers to detect the change
property_changed.emit(self, "value", old_value, new_value)
func set_name_prop(new_name: String):
var old_name = name_prop
name_prop = new_name
# Emit signal for observers to detect the change
property_changed.emit(self, "name_prop", old_name, new_name)
func _init(_value: int = 0, _name: String = ""):
value = _value
name_prop = _name

View File

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

View File

@@ -0,0 +1,3 @@
class_name C_OrderTestA
extends Component
@export var value_a: int = 1

View File

@@ -0,0 +1 @@
uid://12rys1s4dqub

View File

@@ -0,0 +1,3 @@
class_name C_OrderTestB
extends Component
@export var value_b: int = 2

View File

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

View File

@@ -0,0 +1,3 @@
class_name C_OrderTestC
extends Component
@export var value_c: int = 3

View File

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

Some files were not shown because too many files have changed in this diff Show More