
Godot Game Loop Collection
- 159 installs
- 454 repo stars
- Updated July 28, 2026
- thedivergentai/gd-agentic-skills
Use godot-game-loop-collection for development tasks
About
godot-game-loop-collection: A skill for development. This provides functionality for development workflows.
- godot-game-loop-collection
Godot Game Loop Collection by the numbers
- 159 all-time installs (skills.sh)
- +18 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #2,395 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-game-loop-collectionAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 159 |
|---|---|
| repo stars | ★ 454 |
| Last updated | July 28, 2026 |
| Repository | thedivergentai/gd-agentic-skills ↗ |
What it does
Use godot-game-loop-collection for development tasks
Files
Collection Game Loops
Overview
This skill provides a standardized framework for "Collection Loops" – gameplay objectives where the player must find and gather a specific set of items (e.g., hidden eggs, data logs, coins).
NEVER Do
- NEVER use free() to destroy an active state node or level — This can cause crashes if the node is still processing. Always use
queue_free()to safely dispose of it at the end of the frame. - NEVER calculate physics-dependent game state in _process() — Movement and precise collisions must happen in
_physics_process()to stay synced with the engine's fixed timestep. - NEVER execute heavy state transitions (like loading massive levels) synchronously — Calling
load()on a huge scene stalls the main thread. UseResourceLoader.load_threaded_request(). - NEVER use exact floating-point equality (==) for time-based states — Floating-point errors will eventually cause missed triggers. Use
is_equal_approx()or relative comparisons. - NEVER manipulate the active SceneTree from a background thread — The SceneTree is not thread-safe. Use
call_deferred()to push results back to the main thread. - NEVER rely on a monolithic "GameManager" with hardcoded absolute paths — This creates tight coupling. Use groups, signals, and exported references for a modular architecture.
- NEVER assume child nodes are ready before their parent —
_ready()executes from bottom-to-top. If you need child references, use@onreadyorawait ready. - NEVER use string-based signals for critical state transitions — Avoid
connect("signal", _on_func). Use the Signal object syntax (signal.connect(_on_func)) for compile-time validation. - NEVER poll for input state every frame for discrete menu events — Use the
_unhandled_input()callback to cleanly intercept events without wasting CPU cycles in_process(). - NEVER crash the engine intentionally via CRASH_NOW_MSG — Regular state handling should always recover gracefully. Crashing is for unrecoverable internal engine failures.
- NEVER hardcode spawn positions in code — Always use
Marker3DorCollisionShape3Dnodes in the scene so designers can adjust layout without touching code. - NEVER neglect "juice" before an item disappears — Immediate
queue_free()feels dry. Always spawn particles or play a sound before removal. - NEVER use global variables for local collection progress — Keep counts encapsulated within the
CollectionManagerand emit signals to update the UI. - NEVER leave orphaned nodes in the tree during state swaps — Always ensure the previous level/state is properly queued for deletion before instantiating the new one.
- NEVER scale collision shapes non-uniformly for collectibles — This breaks collision detection math. Adjust the internal shape resource properties instead.
---
Available Scripts
MANDATORY: Read the appropriate script before implementing the corresponding pattern.
collection_loop_patterns.gd
Collection of 10 expert patterns: Custom MainLoop extensions, deferred scene switching, threaded loading, and frame throttling.
collection_manager.gd
The central brain of the hunt. Tracks progress and manages completion signals.
collection_compass.gd
Spatial radar for pointing towards the nearest collectible using vector math.
---
Expert Collection Patterns
1. Persistent Collection (Save/Load)
To ensure progress survives restarts, use FileAccess to store data in user://.
func save_progress(data: Dictionary):
var file = FileAccess.open("user://save.dat", FileAccess.WRITE)
file.store_var(data) # Binary serialization for performance
func load_progress() -> Dictionary:
if not FileAccess.file_exists("user://save.dat"): return {}
var file = FileAccess.open("user://save.dat", FileAccess.READ)
return file.get_var()2. Collection Archive UI (Silhouettes)
Display uncollected items as silhouettes without extra textures by using modulate.
- Technique: Use an
ItemListorTextureRectgrid. - Silhouette: Set
modulate = Color(0, 0, 0, 0.5)for locked items. - Reveal: Set
modulate = Color(1, 1, 1, 1)once collected.
Reference
- Master Skill: godot-master
class_name CollectibleItem
extends Area3D
## A base class for items that can be collected (e.g., Hidden Eggs).
## Must be an Area3D or Area2D (Node type can be adapted).
# The ID this item belongs to (e.g., "red_egg_2024")
@export var collection_id: String = "easter_egg"
# If true, the item is removed from the scene upon collection.
@export var consume_on_collect: bool = true
# Optional: Sound or Particle effect to spawn on collection.
@export var vfx_on_collect: PackedScene
# Signal: Emitted when collected, providing the ID to the Manager.
signal item_collected(id: String)
func _ready() -> void:
body_entered.connect(_on_body_entered)
func _on_body_entered(body: Node) -> void:
# Assume 'body' is the player.
# In a real game, check: if body.is_in_group("player")
collect()
func collect() -> void:
# 1. Play feedback (Audio/VFX)
if vfx_on_collect:
var vfx = vfx_on_collect.instantiate()
get_parent().add_child(vfx)
vfx.global_position = global_position
# 2. Notify Manager
# The Manager should listen to this signal via manual connection of `get_tree().get_nodes_in_group("collectibles")`
# OR simpler: Use a global signal bus.
# Here, we emit locally. A manager would connect to this instance if spawned dynamically.
item_collected.emit(collection_id)
# 3. Destroy
if consume_on_collect:
queue_free()
class_name CollectionCompass
extends Sprite2D
## Spatial Radar/Compass for tracking collectibles.
## Rotates the sprite to point towards the nearest or targeted collectible.
@export var search_group: StringName = &"collectible"
@export var rotation_speed: float = 10.0
var target_collectible: Node2D
func _process(delta: float) -> void:
# Find nearest collectible if we don't have a target
if not is_instance_valid(target_collectible):
target_collectible = _find_nearest_collectible()
if is_instance_valid(target_collectible):
# Calculate the target rotation using look_at or direction_to
var target_pos = target_collectible.global_position
var angle_to_target = get_angle_to(target_pos)
# Smoothly rotate towards the target
rotation += angle_to_target * rotation_speed * delta
func _find_nearest_collectible() -> Node2D:
var collectibles = get_tree().get_nodes_in_group(search_group)
var nearest: Node2D = null
var min_dist = INF
for c in collectibles:
if c is Node2D:
var dist = global_position.distance_to(c.global_position)
if dist < min_dist:
min_dist = dist
nearest = c
return nearest
# collection_loop_patterns.gd
extends Node
# 1. Custom MainLoop Extension
# EXPERT NOTE: Extends the absolute lowest level of the engine loop, bypassing the SceneTree entirely.
# Use for high-performance systems or custom engine drivers.
# class_name CustomMainLoop extends MainLoop
# var time_elapsed := 0.0
# func _process(delta: float) -> bool:
# time_elapsed += delta
# return Input.is_key_pressed(KEY_ESCAPE) # Returns true to quit the game loop.
# 2. State Machine Pattern Matching
# EXPERT NOTE: Uses Godot 4's advanced match statement for clean state management.
func process_game_state(state: StringName) -> void:
match state:
&"playing", &"paused":
print("Game is active.")
&"loading":
print("Transitioning...")
_:
push_warning("Unknown state.")
# 3. Proper SceneTree Pausing
# EXPERT NOTE: Halts all nodes that have their process_mode set to inherit/pausable.
func toggle_pause() -> void:
get_tree().paused = not get_tree().paused
# 4. Awaiting Physics Synchronization
# EXPERT NOTE: Yields execution cleanly until the engine completes the current physics frame.
# Crucial for logic that depends on the latest physics server state.
func sync_with_physics() -> void:
await get_tree().physics_frame
# 5. Deferred Scene Switching
# EXPERT NOTE: Queues a function to execute safely during the engine's idle time.
func end_level(path: String) -> void:
call_deferred(&"_deferred_goto_scene", path)
func _deferred_goto_scene(path: String) -> void:
if get_tree().current_scene:
get_tree().current_scene.free()
var next_scene = ResourceLoader.load(path) as PackedScene
var instance = next_scene.instantiate()
get_tree().root.add_child(instance)
get_tree().current_scene = instance
# 6. Global Event Broadcasting via Groups
# EXPERT NOTE: Immediately calls a method on all nodes registered to a specific group.
func reset_all_entities() -> void:
get_tree().call_group(&"entities", &"reset_state")
# 7. Safe Node Casting for State Transitions
# EXPERT NOTE: Uses the 'as' keyword for safe type casting, returning null if the cast fails.
func handle_body_collision(body: Node) -> void:
var player := body as CharacterBody2D
if player:
player.set_physics_process(false) # Example state change
# 8. Asynchronous Threaded Loading
# EXPERT NOTE: Loads heavy game states in the background without freezing the main thread.
func preload_level(path: String) -> void:
ResourceLoader.load_threaded_request(path)
# 9. Polling Async Load Status
# EXPERT NOTE: Retrieves the background loaded state safely after polling status.
func fetch_loaded_level(path: String) -> PackedScene:
if ResourceLoader.load_threaded_get_status(path) == ResourceLoader.THREAD_LOAD_LOADED:
return ResourceLoader.load_threaded_get(path) as PackedScene
return null
# 10. Frame Throttling / Modulo Processing
# EXPERT NOTE: Throttles heavy loop calculations by only executing every Nth frame.
func _process(_delta: float) -> void:
if Engine.get_process_frames() % 5 == 0:
# Run expensive logic (e.g., distant AI, non-critical UI updates)
pass
class_name CollectionManager
extends Node
## A manager for tracking collection objectives (e.g., "Find 5/5 Red Eggs").
## Can handle multiple active collections via dictionary tracking.
# Emitted when progress is made: (collection_id, current_count, target_count)
signal collection_updated(id: String, current: int, target: int)
# Emitted when a specific collection is finished
signal collection_completed(id: String)
# Data structure: { "collection_id": { "current": 0, "target": 10 } }
var _active_collections: Dictionary = {}
func start_collection(id: String, target_count: int) -> void:
_active_collections[id] = {
"current": 0,
"target": target_count
}
# Initial update
collection_updated.emit(id, 0, target_count)
func register_item_collection(id: String) -> void:
if not _active_collections.has(id):
# Optional: Auto-start if not explicit? Better to require start() for explicit control.
# For now, we ignore items that aren't part of an active quest to avoid spam.
return
var data = _active_collections[id]
data["current"] += 1
collection_updated.emit(id, data["current"], data["target"])
if data["current"] >= data["target"]:
collection_completed.emit(id)
# Optional: remove from active? Or keep for archival?
# Keeping it allows UI to query "10/10" states.
func get_progress(id: String) -> Dictionary:
return _active_collections.get(id, {"current": 0, "target": 0})
class_name HiddenItemSpawner
extends Node3D
## Automates the placement of collectible items in a level.
## Useful for "100 Eggs" hunts where manual placement is tedious.
# The item scene to spawn (must be a CollectibleItem or similar)
@export var item_scene: PackedScene
# List of Marker3D nodes to use as spawn points.
# If empty, random spawning inside collision volume loop logic would require specific shape data.
@export var spawn_points: Array[Node3D] = []
# Chance (0.0 to 1.0) to spawn an item at each point.
@export_range(0.0, 1.0) var spawn_chance: float = 0.5
# Total limit of items to spawn. 0 = No limit.
@export var max_spawn_count: int = 0
func _ready() -> void:
spawn_items()
func spawn_items() -> void:
if not item_scene:
push_warning("HiddenItemSpawner: No item_scene assigned.")
return
var available_points = spawn_points.duplicate()
# If no manual points, try to find collision shapes to use as volumes
if available_points.is_empty():
for child in get_children():
if child is CollisionShape3D:
available_points.append(child)
if available_points.is_empty():
push_warning("HiddenItemSpawner: No spawn points or collision shapes found!")
return
available_points.shuffle()
var spawned_count = 0
for point_node in available_points:
if max_spawn_count > 0 and spawned_count >= max_spawn_count:
break
if randf() > spawn_chance:
continue
# Determine position
var pos = point_node.global_position
if point_node is CollisionShape3D:
pos = _get_random_point_in_shape(point_node)
_spawn_at(pos)
spawned_count += 1
func _get_random_point_in_shape(shape_node: CollisionShape3D) -> Vector3:
var shape = shape_node.shape
if not shape:
return shape_node.global_position
var local_point = Vector3.ZERO
if shape is BoxShape3D:
var s = shape.size
local_point = Vector3(
randf_range(-s.x / 2.0, s.x / 2.0),
randf_range(-s.y / 2.0, s.y / 2.0),
randf_range(-s.z / 2.0, s.z / 2.0)
)
elif shape is SphereShape3D:
# Random point inside sphere (volume)
var r = shape.radius * pow(randf(), 1.0/3.0)
local_point = Vector3.random_on_unit_sphere() * r
return shape_node.to_global(local_point)
func _spawn_at(pos: Vector3) -> void:
var item = item_scene.instantiate()
add_child(item)
item.global_position = pos