basic ECS spawner
121
addons/gecs/LICENSE
Normal 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
@@ -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._
|
||||
9
addons/gecs/assets/component.svg
Normal 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 |
43
addons/gecs/assets/component.svg.import
Normal 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
|
||||
13
addons/gecs/assets/entity.svg
Normal 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 |
43
addons/gecs/assets/entity.svg.import
Normal 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
|
||||
BIN
addons/gecs/assets/gecs-logo.psd
Normal file
BIN
addons/gecs/assets/logo.png
Normal file
|
After Width: | Height: | Size: 135 KiB |
40
addons/gecs/assets/logo.png.import
Normal 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
|
||||
5
addons/gecs/assets/observer.svg
Normal 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 |
43
addons/gecs/assets/observer.svg.import
Normal 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
|
||||
15
addons/gecs/assets/system.svg
Normal 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 |
43
addons/gecs/assets/system.svg.import
Normal 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
|
||||
7
addons/gecs/assets/system_folder.svg
Normal 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 |
43
addons/gecs/assets/system_folder.svg.import
Normal 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
|
||||
17
addons/gecs/assets/world.svg
Normal 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 |
43
addons/gecs/assets/world.svg.import
Normal 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
|
||||
128
addons/gecs/debug/gecs_editor_debugger.gd
Normal 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
|
||||
1
addons/gecs/debug/gecs_editor_debugger.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://fndnnk201xlo
|
||||
227
addons/gecs/debug/gecs_editor_debugger_messages.gd
Normal 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
|
||||
1
addons/gecs/debug/gecs_editor_debugger_messages.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://d08dvfk13egq7
|
||||
1501
addons/gecs/debug/gecs_editor_debugger_tab.gd
Normal file
1
addons/gecs/debug/gecs_editor_debugger_tab.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ca7erogu58fca
|
||||
169
addons/gecs/debug/gecs_editor_debugger_tab.tscn
Normal 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
|
||||
724
addons/gecs/docs/BEST_PRACTICES.md
Normal 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."_
|
||||
182
addons/gecs/docs/COMPONENT_QUERIES.md
Normal 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`.
|
||||
699
addons/gecs/docs/CORE_CONCEPTS.md
Normal 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."_
|
||||
357
addons/gecs/docs/DEBUG_VIEWER.md
Normal 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
|
||||
341
addons/gecs/docs/GETTING_STARTED.md
Normal 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."_
|
||||
351
addons/gecs/docs/OBSERVERS.md
Normal 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."_
|
||||
418
addons/gecs/docs/PERFORMANCE_OPTIMIZATION.md
Normal 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."_
|
||||
216
addons/gecs/docs/PERFORMANCE_TESTING.md
Normal 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._
|
||||
893
addons/gecs/docs/RELATIONSHIPS.md
Normal 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."_
|
||||
218
addons/gecs/docs/SERIALIZATION.md
Normal 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
|
||||
434
addons/gecs/docs/TROUBLESHOOTING.md
Normal 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."_
|
||||
299
addons/gecs/ecs/archetype.gd
Normal 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
|
||||
1
addons/gecs/ecs/archetype.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://vrhpkju2aq7q
|
||||
48
addons/gecs/ecs/component.gd
Normal 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
|
||||
1
addons/gecs/ecs/component.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b6k13gc2m4e5s
|
||||
102
addons/gecs/ecs/ecs.gd
Normal 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)
|
||||
1
addons/gecs/ecs/ecs.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dfqwl5njvdnmq
|
||||
509
addons/gecs/ecs/entity.gd
Normal 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
|
||||
1
addons/gecs/ecs/entity.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cl6glf45pcrns
|
||||
74
addons/gecs/ecs/observer.gd
Normal 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
|
||||
1
addons/gecs/ecs/observer.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dd3umv3f8qyx5
|
||||
571
addons/gecs/ecs/query_builder.gd
Normal 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 )
|
||||
1
addons/gecs/ecs/query_builder.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dhyy752meflri
|
||||
184
addons/gecs/ecs/query_cache_key.gd
Normal 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()
|
||||
1
addons/gecs/ecs/query_cache_key.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://rjjelegj3npr
|
||||
294
addons/gecs/ecs/relationship.gd
Normal 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] + ")"
|
||||
1
addons/gecs/ecs/relationship.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bsyujqr14xkrv
|
||||
459
addons/gecs/ecs/system.gd
Normal 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
|
||||
1
addons/gecs/ecs/system.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dyrahdwwpjpri
|
||||
1341
addons/gecs/ecs/world.gd
Normal file
1
addons/gecs/ecs/world.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cdu5tlyk72uu4
|
||||
9
addons/gecs/io/gecs_data.gd
Normal 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
|
||||
1
addons/gecs/io/gecs_data.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://pagmg5srhrnd
|
||||
18
addons/gecs/io/gecs_entity_data.gd
Normal 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
|
||||
1
addons/gecs/io/gecs_entity_data.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cphey3uadg1ai
|
||||
95
addons/gecs/io/gecs_relationship_data.gd
Normal 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
|
||||
1
addons/gecs/io/gecs_relationship_data.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bbqc1v8555562
|
||||
219
addons/gecs/io/io.gd
Normal 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
@@ -0,0 +1 @@
|
||||
uid://drhirabcyqlvk
|
||||
33
addons/gecs/io/serialize_config.gd
Normal 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
|
||||
1
addons/gecs/io/serialize_config.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cf84mkp0nv2mk
|
||||
191
addons/gecs/lib/array_extensions.gd
Normal 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
|
||||
1
addons/gecs/lib/array_extensions.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://h7vbvqjotxmf
|
||||
108
addons/gecs/lib/component_query_matcher.gd
Normal 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
|
||||
1
addons/gecs/lib/component_query_matcher.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://beqw44pppbpl
|
||||
26
addons/gecs/lib/gecs_settings.gd
Normal 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.",
|
||||
}
|
||||
}
|
||||
1
addons/gecs/lib/gecs_settings.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://buvg6dnpqcnys
|
||||
90
addons/gecs/lib/logger.gd
Normal 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
|
||||
1
addons/gecs/lib/logger.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://betmoqpwcq0wc
|
||||
321
addons/gecs/lib/set.gd
Normal 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
|
||||
1
addons/gecs/lib/set.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://oqdcekkxyt52
|
||||
34
addons/gecs/lib/system_group.gd
Normal 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
|
||||
1
addons/gecs/lib/system_group.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b3vi2ingux88g
|
||||
6
addons/gecs/plugin.cfg
Normal 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
@@ -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)
|
||||
1
addons/gecs/plugin.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ddl20uqtqukbm
|
||||
21
addons/gecs/tests/components/c_complex_serialization_test.gd
Normal 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
|
||||
@@ -0,0 +1 @@
|
||||
uid://cpvr163gwyx2d
|
||||
4
addons/gecs/tests/components/c_debug_tracking_test_a.gd
Normal file
@@ -0,0 +1,4 @@
|
||||
class_name C_DebugTrackingTestA
|
||||
extends Component
|
||||
|
||||
@export var value: float = 0.0
|
||||
@@ -0,0 +1 @@
|
||||
uid://d0vhjx22wswv5
|
||||
4
addons/gecs/tests/components/c_debug_tracking_test_b.gd
Normal file
@@ -0,0 +1,4 @@
|
||||
class_name C_DebugTrackingTestB
|
||||
extends Component
|
||||
|
||||
@export var count: int = 0
|
||||
@@ -0,0 +1 @@
|
||||
uid://bijx0kal4npp
|
||||
3
addons/gecs/tests/components/c_domain_test_a.gd
Normal file
@@ -0,0 +1,3 @@
|
||||
class_name C_DomainTestA
|
||||
extends Component
|
||||
@export var v_a: int = 1
|
||||
1
addons/gecs/tests/components/c_domain_test_a.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cqsmow0liv20e
|
||||
3
addons/gecs/tests/components/c_domain_test_b.gd
Normal file
@@ -0,0 +1,3 @@
|
||||
class_name C_DomainTestB
|
||||
extends Component
|
||||
@export var v_b: int = 2
|
||||
1
addons/gecs/tests/components/c_domain_test_b.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bjodoqd54f6pq
|
||||
22
addons/gecs/tests/components/c_observer_health.gd
Normal 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
|
||||
1
addons/gecs/tests/components/c_observer_health.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c0o4jh5t35hqw
|
||||
22
addons/gecs/tests/components/c_observer_test.gd
Normal 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
|
||||
1
addons/gecs/tests/components/c_observer_test.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cmxcdgnk537l
|
||||
3
addons/gecs/tests/components/c_order_test_a.gd
Normal file
@@ -0,0 +1,3 @@
|
||||
class_name C_OrderTestA
|
||||
extends Component
|
||||
@export var value_a: int = 1
|
||||
1
addons/gecs/tests/components/c_order_test_a.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://12rys1s4dqub
|
||||
3
addons/gecs/tests/components/c_order_test_b.gd
Normal file
@@ -0,0 +1,3 @@
|
||||
class_name C_OrderTestB
|
||||
extends Component
|
||||
@export var value_b: int = 2
|
||||
1
addons/gecs/tests/components/c_order_test_b.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://brsnu840dpdnw
|
||||
3
addons/gecs/tests/components/c_order_test_c.gd
Normal file
@@ -0,0 +1,3 @@
|
||||
class_name C_OrderTestC
|
||||
extends Component
|
||||
@export var value_c: int = 3
|
||||
1
addons/gecs/tests/components/c_order_test_c.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bkx8tgtgdngvs
|
||||