
Godot Game Loop Harvest
- 133 installs
- 454 repo stars
- Updated July 28, 2026
- thedivergentai/gd-agentic-skills
Helps with ai & agent building tasks during AI-assisted development.
About
godot-game-loop-harvest is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- godot-game-loop-harvest
- AI & Agent Building
- AI-coding skill
Godot Game Loop Harvest by the numbers
- 133 all-time installs (skills.sh)
- +16 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #3,627 of 16,546 AI & Agent Building 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-harvestAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 133 |
|---|---|
| repo stars | ★ 454 |
| Last updated | July 28, 2026 |
| Repository | thedivergentai/gd-agentic-skills ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Godot Game Loop: Harvest
Implement decoupled, data-driven gathering mechanics. This system handles tool validation, depletion, and respawning.
1. Component Reference
| Component | Asset | Description |
|---|---|---|
| Resource Data | resource_data.gd | Resource: Defines health, yield, and tool requirements. |
| Tool Data | harvest_tool_data.gd | Resource: Defines damage, type, and tier. |
| Harvestable Node | harvestable_node.gd | StaticBody3D: The world interaction entity. |
| Respawn Manager | harvest_respawn_manager.gd | Node: (Singleton) Manages world persistence. |
| Inventory Manager | harvest_inventory_manager.gd | Node: Hub for resource collection. |
| Auto-Save Manager | harvest_autosave_manager.gd | Node: Interval-based progress safety. |
2. Implementation Guide
Step 1: Resource Setup
- Create a
HarvestResourceDataresource in the inspector. - Configure
Required Tool Type(e.g., "pickaxe", "axe") andRequired Tier. - Set
Yield Range(Vector2i) and optionalItem Scenefor physical drops.
Step 2: Node Configuration
- Attach
harvestable_node.gdto aStaticBody3Dnode. - Assign the
ResourceDatafrom Step 1. - Assign a child
Node3D(e.g., a Mesh) tomesh_to_shakefor visual feedback. - Physics: Ensure the node is on Layer 1 for interaction.
Step 3: Global Systems (Recommended)
- Add
harvest_respawn_manager.gdas an Autoload namedHarvestRespawnManager. - The
HarvestableNodewill automatically use this manager if it is found at/root/HarvestRespawnManager.
3. Interaction & Signals
Calling Hits
When a player interacts (e.g., via RayCast), call apply_hit(tool_data).
if collider is HarvestableNode:
collider.apply_hit(player_tool)Signal Map
| Signal | Payload | Integration |
|---|---|---|
harvested | (data, amount) | Connect to InventoryManager.add_resource. |
took_damage | (curr, max) | Connect to a Progress Bar or Damage Popups. |
interaction_failed | (reason: String) | Handles "wrong_tool" or "low_tier" UI feedback. |
NEVER Do
- NEVER use float variables to store massively accumulated harvest resources — Large floats lose precision, which can lead to "missing" resources in idle/clicker games. Always use
intfor core counts. - NEVER process gathering logic in _process() without multiplying rates by delta — If you don't use
delta, the harvesting speed will fluctuate wildly based on the player's hardware performance/framerate. - NEVER run heavy array mathematics for thousands of resources on the main thread — This will cause micro-stutters. Distribute heavy calculations using
WorkerThreadPool. - NEVER leave a gathering game running at full GPU utilization — For UI-heavy harvest games, enable
OS.low_processor_usage_modeto drastically reduce battery drain on mobile/laptops. - NEVER trust OS.get_ticks_msec() for offline progress — This only tracks system uptime. Rely on
Time.get_unix_time_from_system()to calculate real-world time passed between sessions. - NEVER use Timer nodes for precise audio-visual harvesting synchronization — Timer nodes are subject to framerate variations. For frame-perfect sync, use code-based timers or the animation system.
- NEVER couple your resource logic directly to UI counters — Use a signal bus or event system to notify the UI of changes, keeping the game logic decoupled from the presentation.
- NEVER constantly instantiate and destroy Label nodes for "floating numbers" — Frequent allocation/deallocation leads to memory fragmentation. Use an object pool for damage/harvest popups.
- NEVER modify a globally shared Resource without calling duplicate() — If you modify a shared
Resource(like a base crop yield), every instance using that resource will be updated. Useduplicate(true). - NEVER access shared harvest data from background threads without a Mutex — Simultaneous access will eventually corrupt your inventory data. Always use a
Mutexto lock sensitive blocks. - NEVER hardcode yield values in your gathering scripts — Use exports and custom
Resourcefiles so designers can balance the economy without touching the code. - NEVER use queue_free() on a harvested node before the VFX/SFX finish — You'll cut off the "juice." Hide the mesh and disable collision, then
queue_free()once the effect signals completion. - NEVER check tool requirements via string comparisons if possible — Use enums or class types. Strings are prone to typos and are slower for high-frequency checks.
- NEVER neglect to save the UNIX timestamp on exit — If you forget this, you lose the ability to calculate offline earnings when the player returns.
- NEVER scale collision shapes non-uniformly for harvestable objects — This breaks the underlying physics calculations. Adjust the shape resource dimensions instead.
---
Available Scripts
MANDATORY: Read the appropriate script before implementing the corresponding pattern.
harvest_loop_patterns.gd
Expert patterns for idle optimization, UNIX-based offline gains, and threaded resource processing.
resource_data.gd
Resource container: Defines health, yield, and tool requirements for a harvestable object.
harvestable_node.gd
StaticBody3D: The world interaction entity that handles hits, shakes, and depletion.
harvest_autosave_manager.gd
Manages interval-based auto-saving for harvest progress using FileAccess.
---
Expert Harvest Patterns
1. Proc-Gen Resource Veins (Noise)
Instead of random placement, use FastNoiseLite to create organic "clusters" of resources.
var noise = FastNoiseLite.new()
func _should_spawn(pos: Vector2) -> bool:
# noise_val is -1.0 to 1.0. Higher thresholds create tighter veins.
return noise.get_noise_2dv(pos) > 0.5 2. Tool Durability System
Avoid hardcoding durability into the player; use a Resource to encapsulate tool state.
- Benefit: Allows easy serialization, swapping tools, and sharing logic across different tools.
- Implementation: See
harvest_tool_data.gd. Tools should emitdurability_changedandtool_brokensignals.
Reference
- Master Skill: godot-master
class_name HarvestAutoSaveManager
extends Node
## Manages interval-based auto-saving for harvest progress.
## Uses FileAccess and JSON for serialization to user://.
@export var save_interval: float = 60.0 # Seconds
@export var save_path: String = "user://harvest_progress.json"
var _timer: Timer
func _ready() -> void:
_timer = Timer.new()
_timer.wait_time = save_interval
_timer.autostart = true
add_child(_timer)
_timer.timeout.connect(execute_save)
## Collects data from the game and saves to disk.
func execute_save() -> void:
# Note: In a real project, pull this from a global Inventory or Stats manager
var data_to_save = _gather_harvest_data()
var file = FileAccess.open(save_path, FileAccess.WRITE)
if file:
var json_string = JSON.stringify(data_to_save)
file.store_line(json_string)
print_rich("[color=cyan][Harvest] Progress auto-saved to %s[/color]" % save_path)
else:
push_error("Failed to open save file at %s" % save_path)
func _gather_harvest_data() -> Dictionary:
# Placeholder for actual data gathering logic
return {
"timestamp": Time.get_datetime_dict_from_system(),
"resources": {} # Populate with actual inventory counts
}
# harvest_inventory_manager.gd
# [GDSKILLS] godot-game-loop-harvest
# EXPORT_REFERENCE: harvest_inventory_manager.gd
extends Node
signal inventory_updated(resource_name: String, new_amount: int)
var inventory: Dictionary = {}
func add_resource(resource: HarvestResourceData, amount: int) -> void:
var id = resource.display_name
if not inventory.has(id):
inventory[id] = 0
inventory[id] += amount
inventory_updated.emit(id, inventory[id])
print("Harvested %d %s. Total: %d" % [amount, id, inventory[id]])
func get_resource_count(resource_name: String) -> int:
return inventory.get(resource_name, 0)
func has_resources(resource_name: String, amount: int) -> bool:
return get_resource_count(resource_name) >= amount
func consume_resource(resource_name: String, amount: int) -> bool:
if not has_resources(resource_name, amount):
return false
inventory[resource_name] -= amount
inventory_updated.emit(resource_name, inventory[resource_name])
return true
# harvest_loop_patterns.gd
extends Node
# 1. Low Processor Mode Optimization
# EXPERT NOTE: Drastically saves battery on mobile devices for UI-driven gathering games.
func setup_idle_optimization() -> void:
OS.low_processor_usage_mode = true
OS.low_processor_usage_mode_sleep_usec = 6900 # ~144 Hz cap for responsiveness
# 2. Calculating Offline Gains (UNIX Time)
# EXPERT NOTE: Evaluates real-world seconds passed since the player last saved.
func get_offline_ticks(last_save_time_unix: int) -> int:
var current_time := int(Time.get_unix_time_from_system())
return current_time - last_save_time_unix
# 3. Threaded Resource Batch Calculations
# EXPERT NOTE: Offloads heavy math over thousands of elements to background CPU cores.
func process_harvest_batch(data_array: Array) -> void:
WorkerThreadPool.add_group_task(_compute_yield, data_array.size(), -1, true, "HarvestMath")
func _compute_yield(_idx: int) -> void:
# Perform expensive per-item math here
pass
# 4. Type-Safe resource Dictionaries
# EXPERT NOTE: Enforces strictly typed dictionaries to prevent bad data entries/crashes.
var warehouse: Dictionary[StringName, int] = {
&"wood": 0,
&"stone": 0
}
# 5. SceneTreeTimer for Harvesting Delays
# EXPERT NOTE: Creates an inline, one-shot timer without needing a physical Timer node.
func gather_resource(type: StringName, duration: float) -> void:
await get_tree().create_timer(duration).timeout
warehouse[type] += 1
# 6. Global Signal Bus for UI decoupling
# EXPERT NOTE: Broadcast changes via signals rather than direct node references.
signal resource_updated(type: StringName, total: int)
func update_stock(type: StringName, amount: int) -> void:
warehouse[type] += amount
resource_updated.emit(type, warehouse[type])
# 7. Unbinding Signal Parameters
# EXPERT NOTE: Drops unused signal arguments natively for cleaner callbacks.
func setup_ui_connections(btn: Button) -> void:
btn.pressed.connect(_on_harvest_start.unbind(1))
func _on_harvest_start() -> void:
pass
# 8. Optimized Array Reductions for Income
# EXPERT NOTE: Uses optimized C++ internal loops to quickly sum all generator outputs.
func get_total_income(generators: Array[int]) -> int:
return generators.reduce(func(sum, val): return sum + val, 0)
# 9. Format Strings for Resource Readouts
# EXPERT NOTE: Neatly formats string data for localized UIs.
func get_formatted_amount(type: StringName, amount: int) -> String:
return tr("HARVEST_LABEL").format({ "type": type, "count": amount })
# 10. Thread-Safe Mutex Locking
# EXPERT NOTE: Ensures background threads don't corrupt the main inventory total.
var _inventory_mutex := Mutex.new()
var total_resources := 0
func add_resource_safely(amount: int) -> void:
_inventory_mutex.lock()
total_resources += amount
_inventory_mutex.unlock()
# harvest_respawn_manager.gd
# [GDSKILLS] godot-game-loop-harvest
# EXPORT_REFERENCE: harvest_respawn_manager.gd
extends Node
## Global Registry for managing depleted resource nodes.
## This is an "Open World" persistence pattern for harvesting.
signal node_respawned(node: Node)
## Tracking of nodes currently depleted and their remaining respawn time.
var _depleted_nodes: Array[Dictionary] = []
func register_depletion(node: Node3D, respawn_time: float) -> void:
# Store reference and time
var depletion_info = {
"node": node,
"respawn_at": Time.get_ticks_msec() + (respawn_time * 1000)
}
_depleted_nodes.append(depletion_info)
# Node-specific handling (e.g., hiding and disabling collision)
node.collision_layer = 1 << 15 # Layer 16 (Inactive)
node.hide()
func _process(_delta: float) -> void:
var current_time = Time.get_ticks_msec()
# Process respawns
var i = _depleted_nodes.size() - 1
while i >= 0:
var node_info = _depleted_nodes[i]
if current_time >= node_info.respawn_at:
_respawn_node(node_info.node)
_depleted_nodes.remove_at(i)
i -= 1
func _respawn_node(node: Node3D) -> void:
if node.has_method("respawn"):
node.respawn()
else:
node.collision_layer = 1 << 0 # Layer 1 (World)
node.show()
node_respawned.emit(node)
# harvest_tool_data.gd
# [GDSKILLS] godot-game-loop-harvest
# EXPORT_REFERENCE: harvest_tool_data.gd
extends Resource
class_name HarvestToolData
@export_group("Stats")
## Human-readable name (e.g., "Steel Axe").
@export var display_name: String = "Tool"
## The type of tool (e.g., "axe", "pickaxe").
@export var tool_type: String = "any"
## Damage dealt per hit to the resource.
@export var damage: int = 1
## Tier of the tool (0 = basic, 1 = advanced).
@export var tier: int = 0
# harvestable_node.gd
# [GDSKILLS] godot-game-loop-harvest
# EXPORT_REFERENCE: harvestable_node.gd
extends StaticBody3D
signal harvested(data: HarvestResourceData, amount: int)
signal took_damage(current_health: int, max_health: int)
signal interaction_failed(reason: String) # Feedback for "Wrong Tool" or "Low Tier"
@export var resource_data: HarvestResourceData
@export var mesh_to_shake: Node3D # "Hit Juice" visual target
@export var respawn_manager: Node # (Optional) Global HarvestRespawnManager
var current_health: int
var _original_mesh_pos: Vector3
func _ready() -> void:
if not respawn_manager:
respawn_manager = get_node_or_null("/root/HarvestRespawnManager")
if resource_data:
current_health = resource_data.health
if mesh_to_shake:
_original_mesh_pos = mesh_to_shake.position
func apply_hit(tool: HarvestToolData) -> void:
# 1. Tool-Specific Validation
if resource_data.required_tool_type != "any" and tool.tool_type != resource_data.required_tool_type:
interaction_failed.emit("wrong_tool")
return
if tool.tier < resource_data.required_tier:
interaction_failed.emit("low_tier")
return
# 2. Damage Logic
current_health -= tool.damage
took_damage.emit(current_health, resource_data.health)
# 3. Hit Juice (Shake the mesh)
_apply_hit_juice()
if current_health <= 0:
_on_depleted()
func _apply_hit_juice() -> void:
if not mesh_to_shake: return
var tween = create_tween()
var shake_offset = Vector3(randf_range(-0.1, 0.1), 0, randf_range(-0.1, 0.1))
tween.tween_property(mesh_to_shake, "position", _original_mesh_pos + shake_offset, 0.05)
tween.tween_property(mesh_to_shake, "position", _original_mesh_pos, 0.05)
func _on_depleted() -> void:
var yield_amount = randi_range(resource_data.yield_range.x, resource_data.yield_range.y)
# Instance item scene if it exists (e.g. drop gems or logs)
if resource_data.item_scene:
var item = resource_data.item_scene.instantiate()
get_parent().add_child(item)
if item is Node3D:
item.global_position = global_position
harvested.emit(resource_data, yield_amount)
if respawn_manager and respawn_manager.has_method("register_depletion"):
respawn_manager.register_depletion(self, resource_data.respawn_time)
else:
_hide_and_wait()
func _hide_and_wait() -> void:
collision_layer = 1 << 15 # Layer 16 (Inactive)
hide()
await get_tree().create_timer(resource_data.respawn_time).timeout
respawn()
func respawn() -> void:
current_health = resource_data.health
collision_layer = 1 << 0 # Layer 1 (World)
show()
# resource_data.gd
# [GDSKILLS] godot-game-loop-harvest
# EXPORT_REFERENCE: resource_data.gd
extends Resource
class_name HarvestResourceData
@export_group("Stats")
## Human-readable name (e.g., "Iron Ore").
@export var display_name: String = "Resource"
## Number of hits required to harvest.
@export var health: int = 3
## Minimum/Maximum yield per harvest.
@export var yield_range: Vector2i = Vector2i(1, 3)
@export_group("Interaction")
## Required tool type to harvest this resource.
@export var required_tool_type: String = "any"
## The minimum tool tier required to harvest this (e.g., 0 for basic, 1 for advanced).
@export var required_tier: int = 0
## The visual scene to instance (for items or effects).
@export var item_scene: PackedScene
@export_group("Respawn")
## Time in seconds before the node regrows.
@export var respawn_time: float = 60.0