
Godot Genre Tower Defense
- 162 installs
- 454 repo stars
- Updated July 28, 2026
- thedivergentai/gd-agentic-skills
Use godot-genre-tower-defense for development tasks
About
godot-genre-tower-defense: A skill for development. This provides functionality for development workflows.
- godot-genre-tower-defense
Godot Genre Tower Defense by the numbers
- 162 all-time installs (skills.sh)
- +16 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #2,378 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-genre-tower-defenseAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 162 |
|---|---|
| repo stars | ★ 454 |
| Last updated | July 28, 2026 |
| Repository | thedivergentai/gd-agentic-skills ↗ |
What it does
Use godot-genre-tower-defense for development tasks
Files
Genre: Tower Defense
Strategic placement, resource management, and escalating difficulty define tower defense.
Core Loop
1. Prepare: Build/upgrade towers with available currency 2. Wave: Enemies spawn and traverse path toward goal 3. Defend: Towers auto-target and damage enemies 4. Reward: Kills grant currency 5. Escalate: Waves increase in difficulty/complexity
NEVER Do (Expert Anti-Patterns)
Design & Strategy
- NEVER make all towers have the same niche; strictly ensure distinct specialties: Aura Slow, Armor Piercing, Anti-Air, Burst Sniper, and Splash Damage.
- NEVER allow a "Death Spiral" with no exit; strictly provide small comeback bonuses or interest on saved gold to prevent early inevitable failure.
- NEVER make early waves feel like busywork; strictly provide an "Early Call" bonus to skip wait times and accelerate engagement.
- NEVER trust client-side economy updates; strictly require the authoritative server to validate currency addition and tower purchases in co-op.
Pathing & Placement
- NEVER allow the player to "Seal" the exit in mazing games; strictly validate path existence with `NavigationServer2D.map_get_path()` before finalizing tower placement.
- NEVER use synchronous
bake_navigation_polygon()for mazing; strictly offload to a worker thread to prevent 100ms+ frame hitches during placement. - NEVER use global coordinates for grid logic; strictly convert to Vector2i/Vector3i to ensure pixel-perfect tower alignment.
Performance & Systems
- NEVER call
get_overlapping_bodies()every frame; strictly use signals (body_entered/body_exited) to maintain a local target cache. - NEVER use
_process()for projectile movement if count > 500; strictly use the PhysicsServer2D/3D directly for high-performance bullet-hell tiers. - NEVER spawn hundreds of projectiles as full Nodes; strictly use Object Pooling to reuse resources and avoid garbage collection stutters.
- NEVER use standard Strings for priorities; strictly use
StringName(&"first", &"strongest") for O(1) hash comparisons in targeting loops. - NEVER ignore the
progressproperty on PathFollow nodes; strictly use it as the O(1) way to identify the target closest to exit. - NEVER process tower search logic every frame; strictly throttle ACQUIRE searches (e.g., every 5-10 frames) to save significant CPU cycles.
- NEVER scale Tower
CollisionShapenon-uniformly; strictly adjust the radius property of the Shape resource to preserve collision math. - NEVER delete enemies immediately on death; strictly use set_deferred("disabled", true) and wait one frame to prevent physics server crashes.
- NEVER hardcode waves in huge switch statements; strictly use Custom Resources (.tres) for clean balancing and sequence editing.
---
🛠 Expert Components (scripts/)
Original Expert Patterns
- wave_manager.gd - Professional wave orchestrator with Resource-based enemy composition and cleanup.
- tower.gd - Base turret class with FSM state management and firing logic.
- tower_targeting_system.gd - Autonomous priority logic (First/Last/Strongest/Weakest) for efficient targeting.
Modular Components
- tower_defense_patterns.gd - Collection of patterns for furthest-target logic and PhysicsServer projectile optimization.
---
| Phase | Skills | Purpose |
|---|---|---|
| 1. Grid/Path | godot-tilemap-mastery, navigation-2d | Defining where enemies walk and towers build |
| 2. Towers | math-geometry, area-2d | Range checks, rotation, projectile prediction |
| 3. Enemies | path-following, steering-behaviors | Movement along paths |
| 4. Management | state-machines, loop-management | Wave spawning logic, game phases |
| 5. UI | ui-system, drag-and-drop | Building towers, inspecting stats |
Architecture Overview
1. Wave Manager
Handles the timing and godot-composition of enemy waves.
# wave_manager.gd
extends Node
signal wave_started(wave_index: int)
signal wave_cleared
signal enemy_spawned(enemy: Node2D)
@export var waves: Array[Resource] # Array of WaveDefinition resources
var current_wave_index: int = 0
var active_enemies: int = 0
func start_next_wave() -> void:
if current_wave_index >= waves.size():
print("All waves cleared!")
return
var wave_data = waves[current_wave_index]
wave_started.emit(current_wave_index)
_spawn_wave(wave_data)
current_wave_index += 1
func _spawn_wave(wave: WaveResource) -> void:
for group in wave.groups:
await get_tree().create_timer(group.delay).timeout
for i in group.count:
var enemy = group.enemy_scene.instantiate()
add_child(enemy)
active_enemies += 1
enemy.tree_exiting.connect(_on_enemy_died)
await get_tree().create_timer(group.interval).timeout
func _on_enemy_died() -> void:
active_enemies -= 1
if active_enemies <= 0:
wave_cleared.emit()2. Tower Logic (State Machine)
Towers act as autonomous agents.
- States:
Idle,AcquireTarget,Attack,Cooldown. - Targeting Priority:
First,Last,Strongest,Weakest,Closest.
# tower.gd
extends Node2D
var targets_in_range: Array[Node2D] = []
var current_target: Node2D
func _physics_process(delta: float) -> void:
if current_target == null or not is_instance_valid(current_target):
_acquire_target()
if current_target:
_rotate_turret(current_target.global_position)
if can_fire():
fire_projectile()
func _acquire_target() -> void:
# Example: Target closest to end of path
var max_progress = -1.0
for enemy in targets_in_range:
if enemy.progress > max_progress:
current_target = enemy
max_progress = enemy.progress3. Pathfinding Variants
A. Fixed Path (Kingdom Rush style)
Enemies follow a pre-defined Path2D.
- Implementation:
PathFollow2Das parent of Enemy. - Pros: Deterministic, easy to balance, optimized.
- Cons: Less player agency in shaping the path.
B. Mazing (Fieldrunners style)
Players build towers to block/reroute enemies.
- Implementation:
NavigationAgent2Don enemies. Towers updateNavigationRegion2D(bake on separate thread). - Pros: High strategic depth.
- Cons: Computationally expensive recalculation, needs anti-blocking logic (don't let player seal the exit).
Key Mechanics Implementation
Targeting Math (Projectile Prediction)
To hit a moving target, you must predict where it will be.
func get_predicted_position(target: Node2D, projectile_speed: float) -> Vector2:
var to_target = target.global_position - global_position
var time_to_hit = to_target.length() / projectile_speed
return target.global_position + (target.velocity * time_to_hit)Economy
Money management is the secondary core loop.
- Kill Rewards: Direct feedback for success.
- Interest/Income: Rewarding saved money (risk/reward).
- Early Calling: Bonus money for starting the next wave early.
Common Pitfalls
1. Death Spirals: If a player leaks one enemy, they lose money/lives, making the next wave harder, leading to inevitable failure. Fix: Catch-up mechanics or discrete wave difficulty. 2. Useless Towers: Every tower type must have a distinct niche (AoE, Slow, Armor Pierce, Anti-Air). 3. Path Blocking: In mazing games, ensure players cannot completely block the path to the exit. Use NavigationServer2D.map_get_path to validate placement before building.
Godot-Specific Tips
- Physics Layers: Put enemies on a specific layer (e.g., Layer 2) and tower "range" Areas on a different mask to avoid towers detecting each other or walls.
- Area2D Performance: For massive numbers of enemies, avoid
monitorable/monitoringon every frame if possible. UsePhysicsServer2Dqueries for optimization if enemy count > 500. - Object Pooling: Essential for projectiles and enemies to avoid garbage collection stutters during intense waves.
---
🚀 Elite Technical Implementations (Batch 09)
1. Navigation-Path-Validation (Maze Sealing Prevention)
In mazing TD games, players must not be able to block the exit. Use AStarGrid2D to simulate building placement and verify that a valid path still exists from spawn to core.
class_name GridPathValidator extends Node
var _astar_grid: AStarGrid2D
@export var spawn_point: Vector2i = Vector2i(0, 0)
@export var core_point: Vector2i = Vector2i(20, 20)
func _ready() -> void:
_astar_grid = AStarGrid2D.new()
_astar_grid.region = Rect2i(0, 0, 40, 40)
_astar_grid.cell_size = Vector2(64, 64)
_astar_grid.diagonal_mode = AStarGrid2D.DIAGONAL_MODE_NEVER
_astar_grid.update()
## Simulates placing a tower. Returns true if the path remains valid.
func can_build_tower_at(cell_coords: Vector2i) -> bool:
if _astar_grid.is_point_solid(cell_coords):
return false
# 1. Temporarily mark the cell as solid
_astar_grid.set_point_solid(cell_coords, true)
# 2. Query path from start to finish
var test_path: Array[Vector2i] = _astar_grid.get_id_path(spawn_point, core_point)
# 3. If empty, maze is sealed. Revert and deny.
if test_path.is_empty():
_astar_grid.set_point_solid(cell_coords, false)
return false
return true2. Burst-Searching (Frame-Sliced Targeting)
Towers scanning for enemies every frame create CPU spikes. Use Engine.get_process_frames() with a random offset to distribute targeting logic across multiple frames.
class_name BurstSearchTower extends Node2D
@export var search_interval_frames: int = 10
@export var attack_range: float = 250.0
var _frame_offset: int = 0
var _current_target: Node2D = null
func _ready() -> void:
# Stagger search frame per tower
_frame_offset = randi() % search_interval_frames
func _physics_process(_delta: float) -> void:
# Execute expensive logic only once every N frames
if (Engine.get_process_frames() + _frame_offset) % search_interval_frames == 0:
_burst_search_for_target()
func _burst_search_for_target() -> void:
var enemies: Array[Node] = get_tree().get_nodes_in_group("enemies")
# ... distance squared logic to pick closest target ...3. Bezier-Path-Follow (Organic Movement)
Smooth, curved enemy movement is achieved using Path2D and PathFollow2D. Increase the progress property to move the enemy along the spline.
class_name OrganicEnemyMovement extends PathFollow2D
@export var move_speed: float = 150.0
func _physics_process(delta: float) -> void:
# Use 'progress' (Godot 4) to advance along the Curve2D
progress += move_speed * delta
if progress_ratio >= 1.0:
# Reached the core
queue_free()- Master Skill: godot-master
extends Area3D
class_name HomingProjectile3D
## Expert Homing Projectile (Godot 4.6).
## Uses Quaternion slerp for smooth tracking and handles 'Target Lost' gracefully.
@export var speed: float = 15.0
@export var turn_speed: float = 5.0
var target: Node3D = null
func _physics_process(delta: float) -> void:
# 1. Aim Logic (Rotation)
if is_instance_valid(target):
var target_pos = target.global_position
var dir = global_position.direction_to(target_pos)
# Expert Pattern: Smoothly rotate basis using Quaternions
var target_basis = Basis.looking_at(dir)
var current_quat = global_transform.basis.get_rotation_quaternion()
var target_quat = target_basis.get_rotation_quaternion()
global_transform.basis = Basis(current_quat.slerp(target_quat, turn_speed * delta))
# 2. Movement Logic (Local Forward)
global_position += -global_transform.basis.z * speed * delta
## [SKILL NOTICE]: Use 'is_instance_valid(target)' to check if the enemy
## was freed/killed before impact to prevent null pointer crashes.
# tower_defense_patterns.gd
extends Node
# 1. Finding Primary Target (Furthest/First)
# EXPERT NOTE: Use functional reduction to efficiently find the enemy with the most path progress.
func get_first_target(enemies: Array[Node3D]) -> Node3D:
if enemies.is_empty(): return null
return enemies.reduce(func(max_e, e): return e if e.get(&"progress") > max_e.get(&"progress") else max_e)
# 2. Bypassing Nodes for Projectiles (PhysicsServer3D)
# EXPERT NOTE: Create bullets directly in the physics server for massive performance in bullet-hell scenarios.
func spawn_fast_bullet(space: RID, transform: Transform3D) -> RID:
var body := PhysicsServer3D.body_create()
PhysicsServer3D.body_set_mode(body, PhysicsServer3D.BODY_MODE_KINEMATIC)
PhysicsServer3D.body_set_space(body, space)
PhysicsServer3D.body_set_state(body, PhysicsServer3D.BODY_STATE_TRANSFORM, transform)
return body
# 3. ShapeCast for AoE Splash Damage
# EXPERT NOTE: Instantly grab all enemies in an explosion radius using the direct physics space state.
func apply_aoe_damage(pos: Transform3D, shape_rid: RID, damage: float) -> void:
var space := get_world_3d().direct_space_state
var query := PhysicsShapeQueryParameters3D.new()
query.transform = pos
query.shape_rid = shape_rid
for result in space.intersect_shape(query):
if result.collider.has_method(&"take_damage"):
result.collider.call(&"take_damage", damage)
# 4. Spawning PathFollowers for Wave Minions
# EXPERT NOTE: Standard pattern for moving enemies along a predefined track with automatic orientation.
func spawn_minion(path: Path3D, scene: PackedScene) -> PathFollow3D:
var follower := PathFollow3D.new()
path.add_child(follower)
follower.add_child(scene.instantiate())
return follower
# 5. Deferred Collision Disabling for Corpses
# EXPERT NOTE: Safely remove collisions from dead enemies without causing physics server crashes.
func handle_death(collider: CollisionShape3D) -> void:
collider.set_deferred(&"disabled", true)
# 6. Optimized Enemy Type ID (StringName)
# EXPERT NOTE: Use StringName for significantly faster hash comparisons in high-frequency wave logic.
func check_enemy_type(type: StringName) -> void:
if type == &"armored_orc":
pass
# 7. Authoritative Economy Validation
# EXPERT NOTE: Always validate tower purchases on the server to prevent cheating in co-op.
@rpc("any_peer", "call_local", "reliable")
func request_tower_purchase(id: StringName, pos: Vector3) -> void:
if multiplayer.is_server():
# validate_funds(multiplayer.get_remote_sender_id())
print("Server validated purchase: ", id)
# 8. Unreliable Wave State Syncing
# EXPERT NOTE: Sync many moving minions via UDP (unreliable) to save bandwidth.
func sync_minion_positions(data: PackedByteArray) -> void:
multiplayer.send_bytes(data, 0, MultiplayerPeer.TRANSFER_MODE_UNRELIABLE)
# 9. Strict Vector3i Grid Placements
# EXPERT NOTE: Use integer vectors for grid-based tower placement to ensure mathematical precision.
var tower_grid: Dictionary[Vector3i, Node3D] = {}
# 10. Awaiting Timers for Wave Spawning
# EXPERT NOTE: Cleanest way to handle fixed-interval spawning without complex timer nodes.
func start_wave_sequence(count: int, delay: float) -> void:
for i in count:
# spawn_enemy()
await get_tree().create_timer(delay).timeout
# skills/genre-tower-defense/code/tower_targeting_system.gd
extends Node3D
## Tower Targeting Expert Pattern
## Implements Projectile Prediction (Leading) and Priority Modes.
enum Priority { FIRST, LAST, STRONGEST, WEAKEST }
@export var target_priority: Priority = Priority.FIRST
@export var range: float = 10.0
@export var projectile_speed: float = 20.0
var _current_target: Node3D = null
func _process(_delta: float) -> void:
_update_target()
if _current_target:
_aim_at_target()
func _update_target() -> void:
var enemies = get_tree().get_nodes_in_group("enemies")
var potential_targets = []
for enemy in enemies:
var dist = global_position.distance_to(enemy.global_position)
if dist <= range:
potential_targets.append(enemy)
if potential_targets.is_empty():
_current_target = null
return
# 1. Targeting Priority Logic
match target_priority:
Priority.FIRST:
# Assumes enemies have a 'progress' property from PathFollow3D
potential_targets.sort_custom(func(a, b): return a.progress > b.progress)
Priority.STRONGEST:
potential_targets.sort_custom(func(a, b): return a.health > b.health)
# Add LAST and WEAKEST similarly
_current_target = potential_targets[0]
func _aim_at_target() -> void:
# 2. Projectile Prediction (Leading the Target)
var target_pos = _current_target.global_position
var target_vel = _current_target.velocity if "velocity" in _current_target else Vector3.ZERO
var dist = global_position.distance_to(target_pos)
var time_to_impact = dist / projectile_speed
var predicted_pos = target_pos + (target_vel * time_to_impact)
# Smoothly look at predicted position
var target_basis = Basis.looking_at(predicted_pos - global_position)
global_basis = global_basis.slerp(target_basis, 0.1)
## EXPERT NOTE:
## For 'Mazing' TD games, use an A* check before allowing tower placement.
## If 'astar.get_id_path(start, end).is_empty()', the path is blocked—REFUSE placement.
## Use 'NavigationServer3D' for enemy movement to automatically handle
## complex paths around player-built mazes without per-frame pathfinding costs.
# skills/genre-tower-defense/scripts/tower.gd
extends Node2D
## Tower Logic (Expert Pattern)
## Autonomous turret with targeting priority and projectile prediction.
## Separates visual rotation from firing logic.
class_name Tower
enum TargetPriority { FIRST, LAST, CLOSEST, STRONGEST }
@export var range_radius: float = 200.0
@export var fire_rate: float = 1.0 # Shots per second
@export var projectile_scene: PackedScene
@export var turret_visual: Node2D
@export var priority: TargetPriority = TargetPriority.FIRST
var targets_in_range: Array[Node2D] = []
var current_target: Node2D
var _cooldown: float = 0.0
func _ready() -> void:
# Setup Area2D for range
var area = Area2D.new()
var shape = CollisionShape2D.new()
var circle = CircleShape2D.new()
circle.radius = range_radius
shape.shape = circle
area.add_child(shape)
add_child(area)
area.body_entered.connect(_on_body_entered)
area.body_exited.connect(_on_body_exited)
func _physics_process(delta: float) -> void:
_cooldown -= delta
if not is_instance_valid(current_target):
_acquire_target()
if current_target:
_rotate_toward(current_target.global_position)
if _cooldown <= 0:
_fire()
func _acquire_target() -> void:
if targets_in_range.is_empty():
current_target = null
return
# Sort or pick based on priority
# Simplified: just pick first valid
current_target = targets_in_range[0]
func _fire() -> void:
_cooldown = 1.0 / fire_rate
if projectile_scene:
var proj = projectile_scene.instantiate()
get_tree().root.add_child(proj)
proj.global_position = global_position
# Assume projectile has setup method
if proj.has_method("setup") and current_target:
var dir = global_position.direction_to(current_target.global_position)
proj.setup(dir, global_position)
func _rotate_toward(pos: Vector2) -> void:
if turret_visual:
turret_visual.look_at(pos)
func _on_body_entered(body: Node2D) -> void:
if body.is_in_group("enemy"):
targets_in_range.append(body)
func _on_body_exited(body: Node2D) -> void:
targets_in_range.erase(body)
if body == current_target:
current_target = null
## EXPERT USAGE:
## Assign projectile_scene. Ensure enemies are in "enemy" group.
## Adjust Range Radius in inspector.
# skills/genre-tower-defense/scripts/wave_manager.gd
extends Node
## TD Wave Manager (Expert Pattern)
## Data-driven wave spawning with support for multiple enemy types, delays, and wave intervals.
class_name WaveManager
signal wave_started(index: int)
signal wave_completed(index: int)
signal all_waves_complete
# Inner class for Wave Data (or use external Resources)
class WaveGroup:
var enemy_scene: PackedScene
var count: int
var interval: float = 1.0 # Time between spawns
var initial_delay: float = 0.0
var waves: Array[Array] = [] # Array of Arrays of WaveGroups
var current_wave_index: int = -1
var active_enemies: int = 0
var is_wave_active: bool = false
@export var spawn_points: Array[Node2D]
func _ready() -> void:
# Example setup - in prod, load this from Resources
_setup_debug_waves()
func start_next_wave() -> void:
if is_wave_active:
return
current_wave_index += 1
if current_wave_index >= waves.size():
all_waves_complete.emit()
return
is_wave_active = true
wave_started.emit(current_wave_index)
var wave_groups = waves[current_wave_index]
# Process groups in parallel or sequence? usually parallel logic per group
for group in wave_groups:
_process_wave_group(group)
func _process_wave_group(group: WaveGroup) -> void:
await get_tree().create_timer(group.initial_delay).timeout
for i in range(group.count):
_spawn_enemy(group.enemy_scene)
await get_tree().create_timer(group.interval).timeout
func _spawn_enemy(scene: PackedScene) -> void:
var spawn = spawn_points[0] # Simple single spawn logic
var enemy = scene.instantiate()
spawn.add_child(enemy) # Or add to a container
enemy.global_position = spawn.global_position
active_enemies += 1
# Connect signal safely
if enemy.has_signal("died"):
enemy.died.connect(_on_enemy_died)
else:
# Fallback if no specific signal, use tree_exiting (less reliable for "death" vs "freed")
enemy.tree_exiting.connect(_on_enemy_died)
func _on_enemy_died() -> void:
active_enemies -= 1
if active_enemies <= 0 and _all_spawns_finished():
is_wave_active = false
wave_completed.emit(current_wave_index)
func _all_spawns_finished() -> bool:
# Need a more robust check in real prod to ensure not just 0 enemies but 0 pending spawns
# For now, simplest check:
return true
func _setup_debug_waves() -> void:
# Placeholder to prevent crash
pass
## EXPERT USAGE:
## Populate 'waves' with WaveGroup resources. Link 'spawn_points'.
## Connect to 'wave_completed' to show UI or grant gold.
extends Node
class_name WaveResourceSpawner
## Expert Wave Spawner (Godot 4.6).
## Uses WaveData Resources and Path3D for track-based enemy spawning.
@export var waves: Array[Resource] # Array of WaveData
@export var track: Path3D
var current_wave: int = 0
func spawn_wave() -> void:
if current_wave >= waves.size(): return
var data = waves[current_wave]
for i in data.count:
_spawn_unit(data.enemy_scene)
await get_tree().create_timer(data.interval).timeout
current_wave += 1
func _spawn_unit(scene: PackedScene) -> void:
# Expert Pattern: Standard TD movement using PathFollow3D child
var follower = PathFollow3D.new()
follower.loop = false
track.add_child(follower)
var unit = scene.instantiate()
follower.add_child(unit)
## [SKILL NOTICE]: Instantiate enemies as children of 'PathFollow3D'
## and update the 'progress' property to move them along the track.