
Godot Genre Moba
- 129 installs
- 454 repo stars
- Updated July 28, 2026
- thedivergentai/gd-agentic-skills
Use godot-genre-moba for development tasks
About
godot-genre-moba: A skill for development. This provides functionality for development workflows.
- godot-genre-moba
Godot Genre Moba by the numbers
- 129 all-time installs (skills.sh)
- +8 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #2,731 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-mobaAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 129 |
|---|---|
| repo stars | ★ 454 |
| Last updated | July 28, 2026 |
| Repository | thedivergentai/gd-agentic-skills ↗ |
What it does
Use godot-genre-moba for development tasks
Files
Genre: MOBA (Multiplayer Online Battle Arena)
Expert blueprint for MOBAs emphasizing competitive balance and strategic depth.
NEVER Do (Expert Anti-Patterns)
Networking & Authority
- NEVER trust the client for damage calculation or resource costs; strictly validate mana, ranges, and hit detection on the authoritative server using
multiplayer.is_server(). - NEVER use
TRANSFER_MODE_RELIABLEfor continuous movement; strictly useUNRELIABLEorUNRELIABLE_ORDEREDfor position/velocity to prevent network congestion. - NEVER sync units at 60Hz; strictly use a lower tick rate (10-20Hz) via
MultiplayerSynchronizerand implement Interp/Client-Side Prediction for visual smoothness. - NEVER attach individual synchronizers to hundreds of minions; strictly batch state updates into compressed byte arrays via a central manager.
- NEVER synchronize complex Engine objects directly; strictly serialize state into primitive properties or Dictionaries for reliable peer-to-peer sync.
AI & Pathfinding
- NEVER use expensive pathfinding for all minions every frame; strictly use Time Slicing to spread
get_next_path_position()calls across multiple frames. - NEVER query
NavigationAgentpaths inside_process(); strictly use_physics_process()to interact with the navigation server and avoidance systems. - NEVER use complex visual geometry for NavMesh baking; parse simple primitives to avoid stalling the
RenderingServeror crashing the engine. - NEVER set
path_search_max_polygonstoo low in large maps; agents will stop or walk incorrectly if the limit is reached before the destination. - NEVER use
Area2Dfor high-performance Fog of War LOS; strictly use nodeless physics queries (intersect_ray) to bypass node overhead.
Gameplay & Balancing
- NEVER forget Tower "Dive" protection; towers MUST switch targets immediately if an enemy Hero damages an allied Hero within range (Priority: Hero attacking Ally > Minion > Hero).
- NEVER allow "Snowballing" without counter-play; strictly implement Comeback Mechanisms (Kill Bounties, Catch-up XP) to maintain competitive tension.
- NEVER manage hero stats as standard Node variables; strictly use custom
Resourcescripts for data separation and memory efficiency. - NEVER forget to call
duplicate(true)on shared ability Resources; modifying a buff on a shared resource will affect all heroes globally.
Technical & Performance
- NEVER use standard strings for status checks (e.g., "stunned"); strictly use
StringName(&"stunned") for pointer-speed comparisons. - NEVER loop over massive Fog of War grids with floats; strictly use
Vector2iandTileMapLayerto prevent precision jitter. - NEVER execute heavy world/minimap logic on the main thread; strictly offload complex array math to
WorkerThreadPoolto maintain 60+ FPS. - NEVER rigidly couple UI cooldowns to Hero scripts; strictly use a Signal Bus or
Callablebindings for decoupled architecture. - NEVER evaluate exact floating-point equality (==); strictly use
is_equal_approx()for range, cooldown, and mana validations.
---
🛠 Expert Components (scripts/)
Original Expert Patterns
- skill_shot_indicator.gd - Mouse-driven targeting system for range, width, and direction visualization.
- tower_priority_aggro.gd - Advanced AI for defensive towers following competitive priority rules.
Modular Components
- server_minion_sync.gd - Authoritative sync for high-count units using compressed byte arrays.
- fog_visibility_check.gd - Physics raycasting for high-performance Line-of-Sight checks.
- fog_grid_mask.gd - TileMap-driven visibility masking system using Vector2i grid logic.
- status_effect_data.gd - Lightweight Resource container forDefining buffs, debuffs, and stuns.
- status_effect_manager.gd - Modular logic for applying and managing unique status effect instances.
- decoupled_ability_damage.gd - Inter-hero combat interaction using safe duck-typing patterns.
- hero_state_machine.gd - Optimized StringName-based state machine for hero logic.
- async_arena_baker.gd - Background thread-safe navigation mesh updates for dynamic arenas.
- ability_ui_binder.gd - Signal-based UI decoupling for ability cooldown tracking.
- minion_flow_calculator.gd - Parallelized pathing and intelligence using WorkerThreadPool.
---
Core Loop
1. Lane: Player farms minions for gold/XP in a designated lane. 2. Trade: Player exchanges damage with opponent hero. 3. Gank: Player roams to other lanes to surprise enemies. 4. Push: Team destroys towers to open the map. 5. End: Destroy the enemy Core/Nexus.
Skill Chain
| Phase | Skills | Purpose |
|---|---|---|
| 1. Control | rts-controls | Right-click to move, A-move, Stop |
| 2. AI | godot-navigation-pathfinding | Minion waves, Tower aggro logic |
| 3. Combat | godot-ability-system, godot-rpg-stats | QWER abilities, cooldowns, scaling |
| 4. Network | godot-multiplayer-networking | Authority, lag compensation, prediction |
| 5. Map | godot-3d-world-building | Lanes, Jungle, River, Bases |
Architecture Overview
1. Lane Manager
Spawns waves of minions periodically.
# lane_manager.gd
extends Node
@export var lane_path: Path3D
@export var spawn_interval: float = 30.0
var timer: float = 0.0
func _process(delta: float) -> void:
timer -= delta
if timer <= 0:
spawn_wave()
timer = spawn_interval
func spawn_wave() -> void:
# Spawn 3 Melee, 3 Ranged, 1 Cannon (every 3rd wave)
for i in range(3):
spawn_minion(MeleeMinion, lane_path)
await get_tree().create_timer(1.0).timeout2. Minion AI
Simple but follows strict rules.
# minion_ai.gd
extends CharacterBody3D
enum State { MARCH, COMBAT }
var current_target: Node3D
func _physics_process(delta: float) -> void:
match state:
State.MARCH:
move_along_path()
scan_for_enemies()
State.COMBAT:
if is_instance_valid(current_target):
attack(current_target)
else:
state = State.MARCH3. Tower Aggro Logic
The most misunderstood mechanic by new players.
# tower.gd
func _on_aggro_check() -> void:
# Priority 1: Enemy Hero attacking Ally Hero
# Priority 2: Enemy Unit attacking Ally Hero
# Priority 3: Closest Enemy Minion
# Priority 4: Closest Enemy Hero
var target = determine_best_target()
if target:
shoot_at(target)4. Skill-Shot Ability Cycle
Implementation pattern for "QWER" targeting: 1. Idle: Waiting for input. 2. Telegraphed: Show indicator (skill_shot_indicator.gd) while mouse is held. 3. Active: Spawn hitbox/projectile on release. 4. Recovery: Brief backswing animation where movement/casting is locked.
Key Mechanics Implementation
Click-to-Move (RTS Style)
Raycasting from camera to terrain.
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("move"):
var result = raycast_from_mouse()
if result:
nav_agent.target_position = result.positionAbility System (Data Driven)
Defining "Fireball" or "Hook" without unique scripts for everything.
# ability_data.gd
class_name Ability extends Resource
@export var cooldown: float
@export var mana_cost: float
@export var damage: float
@export var effect_scene: PackedSceneGodot-Specific Tips
- NavigationAgent3D: Use
avoidance_enabledfor minions so they flow around each other like water, rather than stacking. - MultiplayerSynchronizer: Sync Health, Mana, and Cooldowns. Do NOT sync position every frame if using Client-Side Prediction (advanced).
- Fog of War: Use a
SubViewportwith a fog texture. Paint "holes" in the texture where allies are. Project this texture onto the terrain shader.
Common Pitfalls
1. Snowballing: Winning team gets too strong too fast. Fix: Implement "Comeback XP/Gold" mechanisms (bounties). 2. Pathfinding Lag: 100 minions pathing every frame. Fix: Distribute pathfinding updates over multiple frames (Time Slicing). 3. Hacking: Client says "I dealt 1000 damage". Fix: Client says "I cast Spell Q at Direction V". Server calculates damage.
Advanced MOBA Meta-Systems
Professional implementation of match playback, network smoothing, and advanced jungle AI.
1. Match Replay System (Binary Serialization)
For high-performance match recording, use var_to_bytes() to serialize state dictionaries into a compressed binary format. Avoid JSON for replays to minimize disk I/O and file size.
class_name ReplayManager extends Node
var frame_history: Array[PackedByteArray] = []
func record_frame(state: Dictionary) -> void:
# Efficiently convert data to bytes
frame_history.append(var_to_bytes(state))
func save_replay(match_id: String) -> void:
var file := FileAccess.open("user://replays/" + match_id + ".dat", FileAccess.WRITE)
if file:
file.store_var(frame_history) # Stores the whole array as a variant
file.close()
func play_frame(frame_index: int) -> Dictionary:
return bytes_to_var(frame_history[frame_index])2. Networked Interpolated Sync
Use Godot 4.x's built-in physics interpolation to mask network jitter. Combined with MultiplayerSynchronizer, this provides smooth hero movement even at low tick rates (15-20Hz).
class_name HeroNetSync extends CharacterBody3D
func _ready() -> void:
# Enable native engine interpolation for visual smoothness
physics_interpolation_mode = Node.PHYSICS_INTERPOLATION_MODE_ON
if is_multiplayer_authority():
setup_synchronizer()
func setup_synchronizer() -> void:
var sync := $MultiplayerSynchronizer
var config := SceneReplicationConfig.new()
# Sync position/rotation via unreliable ordered packets
config.add_property(NodePath(".:global_position"))
sync.replication_config = config3. Jungle-AI (Camp Leashing)
Implement a state machine for jungle monsters that monitors distance from their spawn point. If a hero draws them too far, they enter a "Leashing" state, becoming invulnerable and returning home.
class_name JungleCreep extends CharacterBody3D
@export var leash_radius: float = 12.0
@onready var spawn_pos := global_position
func _physics_process(_delta: float) -> void:
var dist_from_home := global_position.distance_to(spawn_pos)
match state:
State.CHASING:
if dist_from_home > leash_radius:
state = State.LEASHING
State.LEASHING:
# Move back to spawn_pos using NavigationAgent3D
nav_agent.target_position = spawn_pos
if global_position.distance_to(spawn_pos) < 1.0:
state = State.IDLE
health = max_health # Reset health on returnExpert Tip: Always use NavigationServer3D.map_get_iteration_id() to ensure the navigation map is fully synced before allowing AI to pathfind after spawning.
Reference
- Master Skill: godot-master
# ability_ui_binder.gd
extends Control
class_name AbilityUIBinder
# Decoupled UI Cooldown Tracking
# Binds UI elements to hero signals to keep visual logic separate from combat.
func _ready() -> void:
# Pattern: Find local player hero in a predictable group.
var hero: Node = get_tree().get_first_node_in_group(&"local_player")
if hero and hero.has_signal(&"ability_cast"):
# Bind identity metadata to the callback for modularity.
hero.ability_cast.connect(_on_ability_cast.bind(0)) # Bind index 0 for Q
func _on_ability_cast(cooldown_remaining: float, ability_index: int) -> void:
# Logic to update cooldown radial progress bars.
print("Ability ", ability_index, " cooldown started: ", cooldown_remaining)
# async_arena_baker.gd
extends Node
class_name AsyncArenaBaker
# Asynchronous Navigation Mesh Baking
# Prevents main thread freezes when updating terrain collision in MOBAs.
@export var navigation_region: NavigationRegion2D
func update_nav_mesh() -> void:
if not navigation_region: return
# Pattern: Bake on background thread.
# Godot 4 handles the threading internally when 'on_thread' is true.
navigation_region.bake_navigation_mesh(true)
# Wait for completion without locking the game loop.
await navigation_region.bake_finished
print("Arena navigation updated asynchronously.")
# decoupled_ability_damage.gd
extends Area2D
class_name DecoupledAbilityDamage
# Safe Duck-Typing for Ability Damage
# Interacts with target heroes/entities without hard class coupling.
@export var damage_amount: int = 50
func _on_body_entered(body: Node) -> void:
# Pattern: Check if method exists before calling.
# Allows projectiles to hit minions, heroes, and buildings interchangeably.
if body.has_method(&"take_damage"):
body.take_damage(damage_amount)
_on_hit_confirmed()
func _on_hit_confirmed() -> void:
# Handle projectile destruction or effects.
queue_free()
# fog_grid_mask.gd
extends Node2D
class_name FogGridMask
# TileMapLayer Fog of War Masking
# Efficiently clears grid cells based on unit vision radius.
@export var fog_layer: TileMapLayer
func reveal_circle(world_pos: Vector2, cell_radius: int) -> void:
if not fog_layer: return
# Convert world to local grid coordinates.
var center_cell: Vector2i = fog_layer.local_to_map(fog_layer.to_local(world_pos))
# Iterate through a square bounding box and clear within the radius.
for x in range(-cell_radius, cell_radius + 1):
for y in range(-cell_radius, cell_radius + 1):
if Vector2(x, y).length() <= cell_radius:
# -1 source_id clears the cell in Godot 4 TileMapLayer.
fog_layer.set_cell(center_cell + Vector2i(x, y), -1)
# fog_visibility_check.gd
extends Node2D
class_name FogVisibilityCheck
# Fast Physics-Server Raycasting for Fog of War
# Nodeless raycasting for instant, high-performance line-of-sight checks.
func can_see_target(target: Node2D) -> bool:
if not is_instance_valid(target): return false
var space_state := get_world_2d().direct_space_state
# Create query from observer to target.
var query := PhysicsRayQueryParameters2D.create(global_position, target.global_position)
# EXTREMELY IMPORTANT: Exclude self and allies to prevent self-blocking vision.
if get_parent() is CollisionObject2D:
query.exclude = [get_parent().get_rid()]
var result := space_state.intersect_ray(query)
# Visible if the ray hit nothing (clear path) or hit the target directly.
return result.is_empty() or result.collider == target
# hero_state_machine.gd
extends Node
class_name HeroStateMachine
# State Machine Pattern Matching with StringNames
# Optimized state switching for deterministic logic in competitive MOBA heroes.
@export var current_state: StringName = &"idle"
func _physics_process(_delta: float) -> void:
# Fast pointer comparisons using StringNames.
match current_state:
&"idle", &"moving":
_handle_locomotion()
&"stunned":
_handle_stun_lock()
&"casting":
_handle_ability_channeling()
_:
push_error("Hero transitioned to invalid state: ", current_state)
func _handle_locomotion() -> void: pass
func _handle_stun_lock() -> void: pass
func _handle_ability_channeling() -> void: pass
# minion_flow_calculator.gd
extends Node
class_name MinionFlowCalculator
# Offloading Heavy Parallel Computations
# Uses WorkerThreadPool to process minion intelligence/paths without lag spikes.
func process_minion_batch(minions: Array[Node]) -> void:
if minions.is_empty(): return
# Pattern: Distribute logic across all available CPU cores.
var task_id := WorkerThreadPool.add_group_task(_compute_minion_logic.bind(minions), minions.size())
# Wait for the batch to finish before moving to the next frame step.
WorkerThreadPool.wait_for_group_task_completion(task_id)
func _compute_minion_logic(index: int, minion_list: Array[Node]) -> void:
var minion = minion_list[index]
# Perform expensive path or visibility calculations here.
# Note: Must only access thread-safe data within this function.
pass
extends Node
## Expert MOBA Pathfinding (Godot 4.6).
## Multi-threaded deterministic minion navigation.
var minion_list: Array[Node3D] = []
var target_pos: Vector3
func _process(_delta: float) -> void:
if minion_list.is_empty(): return
# Distribute pathfinding across CPU cores
var task = WorkerThreadPool.add_group_task(_calc_minion_path, minion_list.size())
WorkerThreadPool.wait_for_group_task_completion(task)
func _calc_minion_path(idx: int) -> void:
var m = minion_list[idx]
var map = m.get_world_3d().get_navigation_map()
var path = NavigationServer3D.map_get_path(map, m.global_position, target_pos, true)
if path.size() > 1:
m.velocity = m.global_position.direction_to(path[1]) * 5.0
## [SKILL NOTICE]: Disable 'avoidance_enabled' (RVO) for minions
## to prevent jitter. Use 'WorkerThreadPool' for massive waves.
# server_minion_sync.gd
extends Node
class_name ServerMinionSync
# Centralized Server-Authoritative Minion Sync
# Batches hundreds of minion positions into a single byte array for low-overhead sync.
func _physics_process(_delta: float) -> void:
# Only the server should broadcast state.
if multiplayer.is_server():
var minion_data := PackedFloat32Array()
var minions := get_tree().get_nodes_in_group(&"minions")
for minion in minions:
# Sync core transform data to minimal primitives.
minion_data.push_back(minion.global_position.x)
minion_data.push_back(minion.global_position.y)
# Bypasses high-overhead RPCs, sending raw bytes unreliably for maximum speed.
multiplayer.send_bytes(minion_data.to_byte_array(), 0, MultiplayerPeer.TRANSFER_MODE_UNRELIABLE)
# godot-master/scripts/moba_skill_shot_indicator.gd
extends Node2D
## Skill-Shot Indicator Expert Pattern
## Procedural ground telegraphing for ability paths.
@export var indicator_color: Color = Color(1, 0, 0, 0.4)
var _path_width: float = 50.0
var _path_length: float = 400.0
var _is_active: bool = false
func _draw() -> void:
if not _is_active: return
# 1. Visual Telegraphing
# Draw a rectangle representing the skill-shot collision box.
var rect = Rect2(0, -_path_width/2, _path_length, _path_width)
draw_rect(rect, indicator_color, true)
draw_rect(rect, Color.WHITE, false, 2.0) # Outline
func show_indicator(width: float, length: float) -> void:
_path_width = width
_path_length = length
_is_active = true
queue_redraw()
func hide_indicator() -> void:
_is_active = false
queue_redraw()
func _physics_process(_delta: float) -> void:
if _is_active:
# 2. Input Alignment
# The indicator should always point toward the mouse cursor.
look_at(get_global_mouse_position())
## EXPERT NOTE:
## For performance, use a single MeshInstance2D or a Shader-based Plane
## instead of _draw() if many indicators are active simultaneously.
# status_effect_data.gd
extends Resource
class_name StatusEffectData
# Resource-Based Status Effect Data
# Lightweight persistent container for MOBA buffs/debuffs.
@export var effect_id: StringName = &"stun"
@export var duration: float = 1.0
@export var speed_multiplier: float = 1.0
@export var damage_over_time: int = 0
@export var is_cleansable: bool = true
@export var icon: Texture2D
# status_effect_manager.gd
extends Node
class_name StatusEffectManager
# Applying Status Effects Safely
# Manages active buffs without modifying original globally shared resources.
var active_effects: Array[StatusEffectData] = []
func apply_effect(base_effect: StatusEffectData) -> void:
if not base_effect: return
# Pattern: Use duplicate() to ensure unique state for this instance.
# Modifying duration/stacks on one unit won't affect others.
var unique_instance := base_effect.duplicate() as StatusEffectData
active_effects.append(unique_instance)
_on_effect_added(unique_instance)
func _on_effect_added(_effect: StatusEffectData) -> void:
# Trigger logic like slowing speed or initiating stuns.
pass
extends CharacterBody3D
## Expert MOBA Cooldown Sync (Godot 4.6).
## Server Authority + Client Prediction.
@export var current_cooldown: float = 0.0 # Synced via MultiplayerSynchronizer
@export var max_cooldown: float = 5.0
func _process(delta: float) -> void:
if current_cooldown > 0.0:
current_cooldown = maxf(0.0, current_cooldown - delta)
func cast_ability() -> void:
if current_cooldown == 0.0:
# Client Prediction: Instant visual feedback
current_cooldown = max_cooldown
# Request authoritative cast
_server_cast.rpc_id(1)
@rpc("any_peer", "call_remote", "reliable")
func _server_cast() -> void:
if not multiplayer.is_server(): return
if current_cooldown <= 0.0:
current_cooldown = max_cooldown
# Spawn ability projectile/effect here
## [SKILL NOTICE]: Always use 'MultiplayerSynchronizer' to
## replicate 'current_cooldown' property from server to peers.
# godot-master/scripts/moba_tower_priority_aggro.gd
extends Node2D
## Tower Priority Aggro Expert Pattern
## Implementation of a "Priority Stack" for target selection.
@onready var detector = $AggroArea
var current_target: Node2D = null
func _physics_process(_delta: float) -> void:
var potential_targets = detector.get_overlapping_bodies()
var best_target = _evaluate_targets(potential_targets)
if best_target != current_target:
current_target = best_target
_on_target_switched()
func _evaluate_targets(targets: Array) -> Node2D:
# 1. Priority Stack Logic
# 1st: Enemy heroes hitting an allied hero under tower.
# 2nd: Enemy minions/units closest to the tower.
# 3rd: Enemy heroes.
var heroes_hitting_allies = []
var minions = []
var other_heroes = []
for t in targets:
if t.is_in_group("hero"):
if t.get("is_attacking_ally"): heroes_hitting_allies.append(t)
else: other_heroes.append(t)
elif t.is_in_group("minion"):
minions.append(t)
if not heroes_hitting_allies.is_empty(): return _get_closest(heroes_hitting_allies)
if not minions.is_empty(): return _get_closest(minions)
return _get_closest(other_heroes)
func _get_closest(targets: Array) -> Node2D:
var closest = null
var min_dist = INF
for t in targets:
var d = global_position.distance_to(t.global_position)
if d < min_dist:
min_dist = d
closest = t
return closest
func _on_target_switched() -> void:
if current_target:
print("Tower Aggro: ", current_target.name)
# Visual targeting line/laser update here
## EXPERT NOTE:
## Use Server-Side validation for 'is_attacking_ally'.
## Damage calculation and target switching must be deterministic.
extends Area3D
## Expert MOBA Targeting (Godot 4.6).
## Group-weighted priority selection.
func get_best_target() -> Node3D:
var targets = get_overlapping_bodies()
if targets.is_empty(): return null
targets.sort_custom(func(a, b):
var r_a = _get_rank(a)
var r_b = _get_rank(b)
if r_a != r_b: return r_a < r_b
# Tie-breaker: Closest squared distance
return global_position.distance_squared_to(a.global_position) < \
global_position.distance_squared_to(b.global_position)
)
return targets[0]
func _get_rank(body: Node) -> int:
if body.is_in_group("hero"): return 1
if body.is_in_group("minion"): return 2
if body.is_in_group("tower"): return 3
return 99
## [SKILL NOTICE]: Use 'distance_squared_to' for sorting.
## It avoids expensive square root math, crucial for MOBA loops.