Install any skill in seconds. Free to start, no credit card required.
Get Started Free →gecs ECS framework API reference — the Entity-Component-System addon for Godot 4.x used by this project. Covers Entity, Component, System, World, QueryBuilder, Relationship, Observer, CommandBuffer, SystemTimer. Use when the task involves ECS architecture: creating entities with components, defining component data classes (C_ prefix), writing game logic systems, querying entities by component composition, entity relationships or links, reactive observers for component changes, safe structural ch
.claude/skills/randallliuxin-gecs/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 104% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 102% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 88% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 69% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 52% | 0% |
$ARGUMENTS
gecs is the ECS backend for GodotMaker. It has zero LLM training data coverage — all API knowledge must come from this skill.
| gecs Class | Godot Base | Key Insight | |------------|-----------|-------------| | Entity | extends Node | Entity IS a Node — lives in scene tree, can have child nodes | | Component | extends Resource | Pure data, @export properties with defaults, no logic | | System | extends Node | Contains game logic, queries entities, placed in scene tree | | World | extends Node | Manages all entities/systems, archetype storage, query engine | | QueryBuilder | extends RefCounted | Chain API: with_all/with_any/with_none, auto-cached | | Relationship | extends Resource | Pair (relation_component, target), archetype-level indexing | | Observer | extends Node | Reactive: fires on component add/remove/change events | | CommandBuffer | extends RefCounted | Safe structural changes during iteration via cmd | | ECS | Autoload singleton | Global access: ECS.world, ECS.process(delta, group) |
gdscript# --- Component (pure data, extends Resource) --- class_name C_Health extends Component @export var current: float = 100.0 @export var maximum: float = 100.0 class_name C_Velocity extends Component @export var direction: Vector3 = Vector3.ZERO @export var speed: float = 100.0 # --- Entity (extends Node, define default components) --- class_name Player extends Entity func define_components() -> Array: return [C_Health.new(), C_Velocity.new()] func on_ready(): add_to_group("player") # --- System (game logic, extends Node) --- class_name MovementSystem extends System func query() -> QueryBuilder: return q.with_all([C_Velocity]) func process(entities: Array[Entity], components: Array, delta: float) -> void: for entity in entities: var vel = entity.get_component(C_Velocity) var pos = entity.get_component(C_Position) pos.value += vel.direction * vel.speed * delta # Entity is Node, not Node2D! # --- Main scene processing --- # main.gd func _process(delta): ECS.process(delta, "input") ECS.process(delta, "gameplay") func _physics_process(delta): ECS.process(delta, "physics") ECS.process(delta, "run-last")
| Type | Class Name | File Name | Example | |------|-----------|-----------|---------| | Component | C_Name | c_name.gd | C_Health / c_health.gd | | System | NameSystem | s_name.gd | MovementSystem / s_movement.gd | | Entity | Name | e_name.gd | Player / e_player.gd | | Observer | NameObserver | o_name.gd | HealthUIObserver / o_health_ui.gd | | Relationship component | R_Action | r_action.gd | R_ChildOf / r_child_of.gd |
gdscript# Create programmatically var entity = Player.new() ECS.world.add_entity(entity) # Instantiate from scene prefab (.tscn with Entity root) var entity = preload("res://entities/e_player.tscn").instantiate() get_tree().current_scene.add_child(entity) ECS.world.add_entity(entity) # Component operations (pass CLASS to get/has, INSTANCE to add/remove) entity.add_component(C_Health.new(100)) var health = entity.get_component(C_Health) # returns instance or null var has = entity.has_component(C_Health) # bool check entity.remove_component(health) # pass the instance # Enable/disable entity.enabled = false # excluded from queries ECS.world.disable_entity(entity) ECS.world.enable_entity(entity) # Destroy (calls on_destroy, queue_free, cleans up relationships) ECS.world.remove_entity(entity)
gdscript# In a System — use q shorthand func query() -> QueryBuilder: return q.with_all([C_Health, C_Velocity]) # must have ALL .with_any([C_Player, C_Enemy]) # must have at least ONE .with_none([C_Dead]) # must NOT have .enabled() # only enabled entities # Batch component access (faster — avoids per-entity get_component): func query() -> QueryBuilder: return q.with_all([C_Velocity]).iterate([C_Velocity]) func process(entities: Array[Entity], components: Array, delta: float): var velocities = components[0] # Array of C_Velocity, same order as entities for i in entities.size(): var pos = entities[i].get_component(C_Position) pos.value += velocities[i].direction * delta # Entity is Node, not Node2D! # Standalone query (outside a System): var enemies = ECS.world.query.with_all([C_Health, C_Enemy]).execute() var player = ECS.world.query.with_all([C_Player]).execute_one()
gdscriptclass_name LifetimeSystem extends System func query(): return q.with_all([C_Lifetime]) func process(entities: Array[Entity], components: Array, delta: float): for entity in entities: # safe forward iteration var lt = entity.get_component(C_Lifetime) lt.time -= delta if lt.time <= 0: cmd.remove_entity(entity) # queued if should_upgrade(entity): cmd.remove_component(entity, C_OldState) # queued cmd.add_component(entity, C_NewState.new()) # queued # auto-executes after system completes (FlushMode.PER_SYSTEM default)
gdscript# Add a relationship entity.add_relationship(Relationship.new(R_ChildOf.new(), parent_entity)) # Query entities with a relationship var children = ECS.world.query.with_relationship([ Relationship.new(R_ChildOf.new(), parent_entity) ]).execute() # Wildcard query (any target) var has_allies = entity.has_relationship(Relationship.new(R_AllyTo.new(), null)) # Remove with limit entity.remove_relationship(Relationship.new(R_Buff.new(), null), 1) # remove 1 entity.remove_relationship(Relationship.new(R_Effect.new(), null)) # remove all
Main.tscn
+-- World (World node)
+-- Systems (Node)
| +-- input (SystemGroup)
| | +-- PlayerControlsSystem
| +-- gameplay (SystemGroup)
| | +-- HealthSystem
| | +-- DeathSystem
| +-- physics (SystemGroup)
| | +-- MovementSystem
| | +-- CollisionSystem
| +-- run-last (SystemGroup)
| +-- PendingDeleteSystem
+-- Entities (Node — spawned entities go here)
+-- Level (Node3D — level geometry)SystemGroup nodes auto-assign their name as the group property of child Systems.
Read the relevant file when you need detailed API beyond this quick reference:
| Need | File | When to read | |------|------|-------------| | Entity lifecycle, prefabs, spawning | references/entity.md | Creating entities, scene prefab setup, on_ready/on_destroy | | Component design, @export patterns | references/component.md | Defining new components, constructor patterns | | System impl, CommandBuffer, timers | references/system.md | Writing systems, sub_systems, tick rates, deps, parallel | | World setup, entity management | references/world.md | World init, add/remove entities/systems, process groups | | Queries, Relationships, Observers | references/query.md | Complex queries, entity linking, reactive systems | | Debug tools, profiling | references/debug.md | Runtime inspection, editor debugger, performance | | Naming, file org, scene architecture | references/patterns.md | Project structure, cross-cutting patterns, ECS_DESIGN adaptation |
Before writing ANY gecs code, read gotchas.md. It contains 19 hard-won pitfalls with wrong→correct code examples.
If you hit a compile or runtime error, check gotchas.md first — most ECS errors are covered there.
@export properties MUST have default values — Godot errors on Resource export without defaultsget_component() takes the CLASS, not an instance — entity.get_component(C_Health) not entity.get_component(health_instance)cmd for structural changes during iteration — direct add/remove during process() causes entity skippingwith_group() in queries — ~50x slower than with_all([C_Tag]) due to SceneTree traversal. Use tag components insteaddefine_components() must return fresh .new() instances — returning cached/shared instances causes state leakage between entitiesentity.position does NOT work. Store position in a C_Position component. See gotchas.md G1_init() must have default params — func _init(v: float = 0.0) so Component.new() works for duplicationadd_entity() — get_component() returns null until entity enters scene tree. See gotchas.md G2system.process() directly in tests — causes ArrayEntity] type error + CommandBuffer not flushed. Never write test_system_has_query — q is null outside World. See G10, G14add_system() second param is bool, not group name — set system.group before add_system(). See G15world.process(delta) without group skips grouped systems — must pass group name. See G16@export in .tscn unreliable for World — set entity_nodes_root/system_nodes_root in _init(). See G17_process — overlap systems must run in physics group. See G18:= type inference fails with ternary + null — use explicit type annotation. See G19| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-16 | pass→pass | 14,870 | 8,640 | -42% | 1 | 1 | 0% | 2,429 | 4,422 | +82% | 0 | 0 | — |
case-01 | fail→pass | 14,038 | 12,103 | -14% | 1 | 1 | 0% | 2,555 | 5,207 | +104% | 0 | 0 | — |
case-02 | fail→pass | 14,008 | 11,986 | -14% | 1 | 1 | 0% | 2,384 | 4,810 | +102% | 0 | 0 | — |
case-03 | fail→pass | 16,686 | 13,134 | -21% | 1 | 1 | 0% | 2,805 | 5,270 | +88% | 0 | 0 | — |
case-04 | pass→pass | 10,112 | 5,193 | -49% | 1 | 1 | 0% | 1,589 | 3,686 | +132% | 0 | 0 | — |
case-05 | pass→pass | 11,148 | 5,789 | -48% | 1 | 1 | 0% | 1,910 | 3,782 | +98% | 0 | 0 | — |
case-06 | fail→pass | 16,374 | 9,409 | -43% | 1 | 1 | 0% | 2,603 | 4,390 | +69% | 0 | 0 | — |
case-07 | fail→pass | 15,225 | 7,078 | -54% | 1 | 1 | 0% | 2,657 | 4,034 | +52% | 0 | 0 | — |
case-08 | pass→pass | 13,091 | 6,129 | -53% | 1 | 1 | 0% | 2,014 | 3,828 | +90% | 0 | 0 | — |
case-09 | pass→pass | 14,490 | 8,350 | -42% | 1 | 1 | 0% | 2,345 | 4,171 | +78% | 0 | 0 | — |
case-10 | fail→pass | 9,606 | 4,826 | -50% | 1 | 1 | 0% | 1,584 | 3,644 | +130% | 0 | 0 | — |
case-11 | fail→pass | 13,513 | 11,613 | -14% | 1 | 1 | 0% | 2,323 | 4,923 | +112% | 0 | 0 | — |
case-12 | fail→pass | 10,871 | 4,167 | -62% | 1 | 1 | 0% | 1,749 | 3,437 | +97% | 0 | 0 | — |
case-13 | pass→pass | 8,552 | 5,606 | -34% | 1 | 1 | 0% | 1,398 | 3,752 | +168% | 0 | 0 | — |
case-14 | pass→pass | 6,124 | 6,830 | +12% | 1 | 1 | 0% | 1,202 | 3,966 | +230% | 0 | 0 | — |
case-15 | fail→pass | 15,593 | 14,883 | -5% | 1 | 1 | 0% | 3,153 | 5,463 | +73% | 0 | 0 | — |
case-17 | pass→pass | 9,856 | 6,601 | -33% | 1 | 1 | 0% | 1,615 | 3,947 | +144% | 0 | 0 | — |
case-18 | fail→pass | 8,610 | 4,406 | -49% | 1 | 1 | 0% | 1,391 | 3,462 | +149% | 0 | 0 | — |
case-19 | fail→pass | 15,612 | 6,929 | -56% | 1 | 1 | 0% | 2,375 | 3,941 | +66% | 0 | 0 | — |
case-20 | pass→pass | 6,047 | 3,587 | -41% | 1 | 1 | 0% | 1,070 | 3,415 | +219% | 0 | 0 | — |
case-21 | fail→fail | 9,202 | 6,652 | -28% | 1 | 1 | 0% | 1,620 | 3,829 | +136% | 0 | 0 | — |
case-22 | pass→pass | 8,829 | 6,540 | -26% | 1 | 1 | 0% | 1,660 | 3,949 | +138% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +50 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.