
Godot Composition Apps
- 189 installs
- 454 repo stars
- Updated July 28, 2026
- thedivergentai/gd-agentic-skills
Use godot-composition-apps for development tasks
About
godot-composition-apps: A skill for development. This provides functionality for development workflows.
- godot-composition-apps
Godot Composition Apps by the numbers
- 189 all-time installs (skills.sh)
- +16 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #2,129 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/thedivergentai/gd-agentic-skills --skill godot-composition-appsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 189 |
|---|---|
| repo stars | ★ 454 |
| Last updated | July 28, 2026 |
| Repository | thedivergentai/gd-agentic-skills ↗ |
What it does
Use godot-composition-apps for development tasks
Files
Godot Composition & Architecture (Apps & UI)
This skill enforces the Single Responsibility Principle within Godot's Node system. Whether building an RPG or a SaaS Dashboard, the rule remains: One Script = One Job.
The Core Philosophy
The Litmus Test
Before writing a script, ask: "If I attached this script to a literal rock, would it still function?"
- Pass: An
AuthComponenton a rock allows the rock to log in. (Context Agnostic) - Fail: A
LoginFormscript on a rock tries to grab text fields the rock doesn't have. (Coupled)
The Backpack Model (Has-A > Is-A)
Stop extending base classes to add functionality. Treat the Root Node as an empty Backpack.
- Wrong (Inheritance):
SubmitButtonextendsAnimatedButtonextendsBaseButton. - Right (Composition):
SubmitButton(Root) HAS-AAnimationComponentand HAS-ANetworkRequestComponent.
The Hierarchy of Power (Communication Rules)
Strictly enforce this communication flow to prevent "Spaghetti Code":
| Direction | Source → Target | Method | Reason |
|---|---|---|---|
| Downward | Orchestrator → Component | Function Call | Manager owns the workers; knows they exist. |
| Upward | Component → Orchestrator | Signals | Workers are blind; they just yell "I'm done!" |
| Sideways | Component A ↔ Component B | FORBIDDEN | Siblings must never talk directly. |
The Sideways Fix: Component A signals the Orchestrator; Orchestrator calls function on Component B.
The Orchestrator Pattern
The Root Node script (e.g., LoginScreen.gd, UserProfile.gd) is now an Orchestrator.
- Math/Logic: 0%
- State Management: 100%
- Job: Wire components together. Listen to Component signals and trigger other Component functions.
Example: App/UI Context
| Concept | App/UI Example |
|---|---|
| Orchestrator | UserProfile.gd |
| Component 1 | AuthValidator (Logic) |
| Component 2 | AuthVisualSyncer (Visuals) |
| Component 3 | ThemeManager (Visuals) |
Implementation Standards
1. Type Safety
Define components globally. Never use dynamic typing for core architecture.
# auth_component.gd
class_name AuthComponent extends Node2. Dependency Injection
NEVER use get_node("Path/To/Child"). Paths are brittle. ALWAYS use Typed Exports and drag-and-drop in the Inspector.
# Orchestrator script
@export var auth: AuthComponent
@export var form_ui: Control3. Scene Unique Names
If internal referencing within a scene is strictly necessary for the Orchestrator, use the % Unique Name feature.
@onready var submit_btn = %SubmitButton4. Stateless Components
Components should process the data given to them.
- Bad:
NetworkComponentfinds the username text field itself. - Good:
NetworkComponenthas a functionlogin(username, password). The Orchestrator passes the text field data into that function.
NEVER Do (Expert Architectural Rules)
Hierarchy & Dependencies
- NEVER use get_parent() to fetch data — Components must be blind. If they need data, it must be injected via
@exportor passed into a function call. - NEVER talk sideways —
ComponentAmust never call functions onComponentB. High-coupling makes refactoring impossible. Always signal up to the Orchestrator. - NEVER use brittle Node Paths —
get_node("Child/Subchild/Node")breaks when you move a single node. Use@exportand the Inspector.
Logic & State
- NEVER put business logic in the Orchestrator — The Orchestrator should only have
_on_signalmethods that delegate to other components. - NEVER store global state in individual components — Use a shared
ContextResource or the Global Autoload for cross-scene state. - NEVER assume a component's parent is of a specific type — If a
HealthComponentrequires its parent to be aCharacterBody2D, it fails the "Rock Test."
Polish & Orchestration
- NEVER skip signal cleanup — Connecting signals dynamically without disconnecting can lead to memory leaks or multiple execution bugs.
- NEVER let Logic know about Visuals — A
CombatComponentshould never callAnimationPlayer.play(). It emitsattack_performed, and aSyncerorOrchestratorhandles the visual response.
Code Structure Example (General App)
Component: clipboard_copier.gd
class_name ClipboardCopier extends Node
signal copy_success
signal copy_failed(reason)
func copy_text(text: String) -> void:
if text.is_empty():
copy_failed.emit("Text empty")
return
DisplayServer.clipboard_set(text)
copy_success.emit()Orchestrator: share_menu.gd
extends Control
# Wired via Inspector
@export var copier: ClipboardCopier
@export var link_label: Label
func _ready():
# Downward communication
%CopyButton.pressed.connect(_on_copy_button_pressed)
# Upward communication listening
copier.copy_success.connect(_on_copy_success)
func _on_copy_button_pressed():
# Orchestrator delegation
copier.copy_text(link_label.text)
func _on_copy_success():
# Orchestrator managing UI state based on signal
%ToastNotification.show("Link Copied!")Expert Composition Patterns (Apps)
1. App-Level Service Locator
Avoid polluting the Global Autoload list with dozens of Node-based singletons. Use Engine.register_singleton() for lightweight, non-node business logic services (Auth, Config, Networking) [6].
# AppServiceLocator.gd (Autoload)
func _ready() -> void:
# Register a lightweight RefCounted object as a global singleton
if not Engine.has_singleton(&"AuthService"):
Engine.register_singleton(&"AuthService", AuthService.new())
# Access from anywhere
var auth = Engine.get_singleton(&"AuthService")2. Visual-Logic-Syncers (VLS)
Strictly decouple UI animations and VFX from business logic. The Logic component emits signals, and the VLS component listens and triggers the AnimationPlayer [12, 13].
# AuthVisualSyncer.gd
@export var logic: AuthFormLogic
@export var anim: AnimationPlayer
func _ready() -> void:
logic.login_failed.connect(_on_login_failed)
func _on_login_failed(reason: String):
anim.play("shake_form")
# Procedural juice via Tweens
var t = create_tween()
t.tween_property(self, "modulate", Color.RED, 0.2)3. O(1) Component Registry
In complex dashboards, use a Dictionary in the Orchestrator to store sibling components for instant lookup, bypassing brittle get_node() paths [3, 13].
# DashboardOrchestrator.gd
var _registry: Dictionary = {}
func _ready() -> void:
for child in get_children():
_registry[child.name] = child
for group in child.get_groups():
_registry[group] = child
func get_comp(key: StringName) -> Node:
return _registry.get(key)Reference
- Master Skill: godot-master
comp_orchestrator_base.gd
Central hub for signal delegation and component wiring. Logic-free manager.
comp_base_component.gd
Foundational component with type-safe signals and auto-group registration.
comp_health_component.gd
Context-agnostic health/damage logic that works on players, enemies, or barrels.
comp_hitbox_component.gd
Area-based collision interface that bridges physical hits to the HealthComponent.
comp_ability_sequencer.gd
Dynamic ability manager that executes child 'Ability' nodes via unified interfaces.
comp_data_driven_config.gd
Late-binding configuration loader for hot-swapping behavior via Resources (.tres).
comp_dependency_injector.gd
Expert injection pattern for passing refs to dynamic components without get_node.
comp_persistence_component.gd
Automated save/load registration for modular node persistence.
comp_logic_visual_syncer.gd
Decoupling agent that syncs gameplay logic state to visual animations/VFX.
comp_rock_test_boilerplate.gd
Architectural validator to ensure components are truly decoupled.
Skill Evaluation Report: godot-composition-apps
Summary
- Total Score: 110/120 (91%)
- Grade: A
- Pattern: Mindset
- Knowledge Ratio: E:A:R = 80:15:5
- Verdict: Vital translation of game architecture patterns to application/UI development in Godot.
Dimension Scores
| Dimension | Score | Max | Notes |
|---|---|---|---|
| D1: Knowledge Delta | 18 | 20 | Applies game patterns to App/UI context, which is a high-value niche insight. |
| D2: Mindset vs Mechanics | 14 | 15 | "One Script = One Job" is universally applicable but critical here. |
| D3: Anti-Pattern Quality | 14 | 15 | Identifies specific UI coupling issues. |
| D4: Specification Compliance | 15 | 15 | clear distinction for APPS/UI/TOOLS. |
| D5: Progressive Disclosure | 13 | 15 | Resources (Auth/Theme) are relevant and helpful. |
| D6: Freedom Calibration | 13 | 15 | Good balance. |
| D7: Pattern Recognition | 9 | 10 | Strong Mindset pattern. |
| D8: Practical Usability | 14 | 15 | clear separation of concerns for UI logic. |
Critical Issues
None.
Top 3 Improvements
1. Expand on Godot's specific Control node quirks in composition. 2. Add an example of a complex form with validation components. 3. Discuss data flow for "Save/Load" systems in app context.
class_name AuthComponent extends Node
signal login_success(user_data: Dictionary)
signal login_failed(error_message: String)
func login(username: String, password: String) -> void:
if username.is_empty() or password.is_empty():
login_failed.emit("Username and password cannot be empty.")
return
# Simulate async network request
await get_tree().create_timer(1.0).timeout
if username == "admin" and password == "password":
login_success.emit({"id": 1, "username": "admin", "role": "admin"})
else:
login_failed.emit("Invalid credentials.")
func logout() -> void:
# Cleanup session
pass
class_name ThemeManager extends Node
signal theme_changed(is_dark_mode: bool)
@export var dark_theme: Theme
@export var light_theme: Theme
@export var control_root: Control
var is_dark_mode: bool = true
func _ready() -> void:
apply_theme()
func toggle_theme() -> void:
is_dark_mode = not is_dark_mode
apply_theme()
theme_changed.emit(is_dark_mode)
func apply_theme() -> void:
if not control_root:
return
if is_dark_mode:
control_root.theme = dark_theme
else:
control_root.theme = light_theme
class_name CompAbilitySequencer
extends Node
## Expert Ability Orchestrator.
## Manages child 'Ability' nodes and their activation sequence.
func cast_ability(ability_name: String) -> void:
var ability = get_node_or_null(ability_name)
if ability and ability.has_method("execute"):
ability.execute()
## Rule: Adding new skills is as simple as adding a new child node with a script.
class_name CompBaseComponent
extends Node
## Expert Base Component.
## Job: Encapsulate a single responsibility. Must work "on a rock".
signal task_completed(result: Variant)
func _ready() -> void:
# Optional: Register to a group for mass orchestration
add_to_group("Components")
## Rule: NEVER use 'get_parent()' to access data. Use signals or dependency injection.
class_name CompDataDrivenConfig
extends Node
## Expert Resource-driven Component configuration.
## Allows behavior to be hot-swapped via .tres files.
@export var config: Resource
func _ready() -> void:
if config:
_apply_config()
func _apply_config() -> void:
# Pattern: Access properties from the Resource.
# Example: parent.speed = config.movement_speed
pass
## Rule: Decouple 'Values' (Resource) from 'Logic' (Component).
class_name CompDependencyInjector
extends Node
## Expert Runtime Dependency Injection.
## Use when components are added dynamically and need references.
func inject(deps: Dictionary) -> void:
for key in deps:
if key in self:
set(key, deps[key])
## Rule: Use injection to avoid 'get_node' or 'get_parent' inside dynamic components.
class_name CompHealthComponent
extends Node
## Expert Decoupled Health Component.
## Acts as a data-store and logic-gate for damage.
signal health_changed(new_health: float)
signal health_depleted
@export var max_health: float = 100.0
@onready var current_health: float = max_health
func damage(amount: float) -> void:
current_health = clamp(current_health - amount, 0, max_health)
health_changed.emit(current_health)
if current_health <= 0:
health_depleted.emit()
## Rule: This component knows nothing about 'Player' or 'Enemies', only 'Health'.
class_name CompHitboxComponent
extends Area2D
## Expert Hitbox Component.
## Maps physical collision to a HealthComponent.
@export var health_component: CompHealthComponent
func _ready() -> void:
area_entered.connect(_on_area_entered)
func _on_area_entered(area: Area2D) -> void:
if area.has_method("get_damage_amount"):
health_component.damage(area.get_damage_amount())
## Rule: Use '@export' to link components in the inspector, never 'get_node'.
class_name CompLogicVisualSyncer
extends Node
## Expert pattern: Logic vs Visual Separation.
## Synchronizes a VisualComponent (VFX/Anim) to a LogicComponent.
@export var logic: Node
@export var visuals: Node
func _ready() -> void:
if logic.has_signal("state_changed"):
logic.state_changed.connect(_on_logic_state_changed)
func _on_logic_state_changed(new_state: String) -> void:
if visuals.has_method("play_animation"):
visuals.play_animation(new_state)
## Rule: Never let 'MovementLogic' know 'AnimationPlayer' exists. Use this syncer.
class_name CompOrchestratorBase
extends Node
## Expert Orchestrator Pattern.
## Job: Wire components together. Math/Logic = 0%, State Management = 100%.
func _ready() -> void:
_connect_components()
func _connect_components() -> void:
# Pattern: Listen to signal from Component A, trigger function on Component B.
# Example:
# $InputComponent.action_pressed.connect($MovementComponent.apply_velocity)
pass
## Rule: Siblings must NEVER talk directly. Always signal UP to Orchestrator.
class_name CompPersistenceComponent
extends Node
## Expert Persistence Component.
## Automatically registers the Orchestrator for saving.
func _ready() -> void:
add_to_group("Saveable")
func get_save_data() -> Dictionary:
# Orchestrator calls this to collect state
return {}
## Rule: Keep save logic in a component to easily add/remove persistence to any node.
class_name CompRockTestBoilerplate
extends Node
## Expert 'Rock Test' debug utility.
## Validates if a component is truly decoupled and context-agnostic.
func run_rock_test(component: Node) -> void:
var rock = Node.new()
rock.name = "LiteralRock"
add_child(rock)
rock.add_child(component)
print("Testing Component: ", component.name)
# If the component throws errors because 'get_parent()' isn't a Player, it FAILS.
# Proper components should just sit on the rock waiting for signals/calls.
## Tip: Run this test during development to catch hard-coupling early.