Swarm Defender: Godot Multiplayer and Advanced AI
After working with GameMaker for years, I wanted to try something different. Enter Godot Engine 4.6—a modern, open-source engine with powerful built-in features. The result is Swarm Defender, a 3D multiplayer co-op tower defense game where players work together to defend a central core against waves of intelligent enemies.
Why Godot?
Making the switch from GameMaker to Godot was motivated by several factors:
- Native 3D Support: Unlike GameMaker, Godot has full 3D engine capabilities out of the box
- Scene-Node System: Godot's hierarchical scene structure makes complex projects more manageable
- Built-in Multiplayer: RPC system and MultiplayerSynchronizer nodes simplify networking
- GDScript: Python-like scripting that's easy to read and write
- Open Source: Complete control and no licensing concerns
Steam Integration
One of the first major challenges was integrating Steam multiplayer. Using the GodotSteam addon and custom Steam Multiplayer Peer implementation, the game features:
- Steam lobby system for matchmaking (up to 4 players)
- Peer-to-peer networking through Steam's infrastructure
- Steam achievements and stats (planned)
- Friends invite system
# Example: Setting up Steam multiplayer
func create_lobby():
Steam.createLobby(Steam.LOBBY_TYPE_PUBLIC, 4)
func _on_lobby_created(connect_result, lobby_id):
if connect_result == 1:
current_lobby_id = lobby_id
multiplayer.multiplayer_peer = steam_peer
Advanced Enemy AI System
The most technically interesting aspect of Swarm Defender is the enemy AI. After several refactoring passes, I've built a sophisticated system that includes:
Navigation and Pathfinding
- NavigationAgent3D: Godot's built-in pathfinding for intelligent obstacle avoidance
- NavigationRegion3D: Baked navigation meshes for efficient pathfinding
- Dynamic Path Updates: Enemies recalculate paths when targets move
State Machine Architecture
Each enemy operates on a three-state system:
- Idle: Default state, waiting for patrol or detection
- Patrol: Following predetermined paths between path nodes
- Chase: Actively pursuing detected players or attacking the core
Vision and Detection System
Enemies don't just magically know where players are. They have realistic vision:
- Cone-based Vision: Configurable vision angle and range
- Line-of-Sight Checks: Raycasting to ensure enemies can actually see targets
- Alert System: Audio cues when enemies spot players
- Memory: Enemies remember last known player position briefly
# Example: Vision cone detection
func can_see_target(target_position: Vector3) -> bool:
var to_target = target_position - global_position
var angle = rad_to_deg(forward_vector.angle_to(to_target))
if angle > vision_angle / 2.0:
return false
# Perform raycast for line-of-sight
var space_state = get_world_3d().direct_space_state
var result = space_state.intersect_ray(query)
return result and result.collider == target
Patrol Path System
One of my favorite features is the automatic patrol path system:
- Auto-Discovery: Enemies find path nodes using Godot's group system
- Multiple Sorting Methods: Alphabetical, indexed, or distance-based
- Looping or Reversing: Configurable patrol behaviors
- Continuous or Paused: Wait at nodes or keep moving
Enemy Variants
Using inheritance, I created multiple enemy types from a base NPC class:
- Standard Alien: 80 HP, melee attacks, standard speed
- Tank: 350 HP, 45 damage, slow but devastating
- Thrower: 120 HP, ranged projectile attacks, keeps distance
- Hopper: Hopping movement pattern with standard AI
- Floater: Flying enemy with vertical navigation capabilities
Performance Optimization
With 75+ enemies potentially active at once, performance was critical. Key optimizations include:
Staggered Updates
Instead of all enemies updating every frame, they're distributed across frames:
# Assign each NPC to a different update frame
var update_offset = randi() % performance_update_distribute_frames
func _physics_process(delta):
if Engine.get_process_frames() % performance_update_distribute_frames != update_offset:
return
# Only this subset of enemies updates this frame
update_ai(delta * performance_update_distribute_frames)
Distance-Based Updates
- Nearby enemies update every frame
- Medium-distance enemies update every 2-3 frames
- Distant enemies update less frequently
Cached References
- Player list cached and shared across all NPCs
- Navigation queries throttled
- Vision checks optimized with early-exit conditions
Code Organization
One thing I'm proud of is the code structure. All NPC scripts use #region blocks for organization:
- Exports (configurable properties)
- Node References
- Internal State
- Lifecycle Functions
- Movement/Physics
- Vision/Detection
- Combat Systems
- Pathfinding
- Animation Management
This makes navigating 500+ line files much easier and helps other developers understand the codebase.
Server-Authoritative Multiplayer
The game uses a server-authoritative model where the host manages:
- All enemy AI and pathfinding
- Damage calculations
- Entity spawning and deletion
- Wave progression
Clients handle:
- Player input
- Rendering and interpolation
- Local audio playback
- UI updates
What's Next?
Swarm Defender is still in active development. Upcoming features include:
- Complete wave spawning system with difficulty scaling
- Player economy and upgrade system
- Additional enemy types and bosses
- More interactive level elements
- Player ability system
Lessons from the Switch
Moving from GameMaker to Godot has been enlightening:
- Scene System is Powerful: Instancing and composition make complex hierarchies manageable
- Signals are Elegant: Much cleaner than GameMaker's event system for object communication
- Built-in Features Save Time: Navigation, multiplayer, and 3D rendering just work
- Open Source Matters: Being able to read engine source code when debugging is invaluable
If you're considering learning Godot, I highly recommend it—especially for 3D projects.