
Godot Genre Rts
- 140 installs
- 454 repo stars
- Updated July 28, 2026
- thedivergentai/gd-agentic-skills
Use godot-genre-rts for development tasks
About
godot-genre-rts: A skill for development. This provides functionality for development workflows.
- godot-genre-rts
Godot Genre Rts by the numbers
- 140 all-time installs (skills.sh)
- +9 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #2,605 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-rtsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 140 |
|---|---|
| repo stars | ★ 454 |
| Last updated | July 28, 2026 |
| Repository | thedivergentai/gd-agentic-skills ↗ |
What it does
Use godot-genre-rts for development tasks
Files
Genre: Real-Time Strategy (RTS)
Expert blueprint for RTS games balancing strategy, micromanagement, and performance.
NEVER Do (Expert Anti-Patterns)
Unit Logic & Pathfinding
- NEVER allow pathfinding "Jitter" when moving group units; strictly stagger path queries and enable RVO Avoidance only when units are in motion to save CPU cycles.
- NEVER update RVO avoidance every frame for all units; strictly use Avoidance Threading (Project Settings) and replace static units with
NavigationObstacle. - NEVER let units get stuck in infinite path loops; strictly implement a timeout and IDLE state if a destination is unreachable.
- NEVER use
_process()on hundreds of individual units; strictly use a central UnitManager or_physics_processonly when required. - NEVER calculate unit visibility manually for Fog of War; strictly use a Shader-based mask (SubViewport + ColorRect) for GPU efficiency.
- NEVER process unit AI or pathfinding synchronously for mass groups; strictly offload to `WorkerThreadPool` and stagger path updates.
- NEVER use high-poly visual meshes as NavMesh source geometry; strictly use simplified Collision Shapes for baking.
Interaction & Commands
- NEVER forget Command Queuing (Shift-Click); strictly store an
Array[Command]and implement a "Force Move/Attack" bypass. - NEVER create excessive micromanagement; strictly automate low-level tasks like auto-aggro range and auto-return for resource gathering.
- NEVER use exact floating-point equality (==) for grid or timers; strictly use
is_equal_approx()for deterministic triggers. - NEVER rely on the visual SceneTree for selection data; strictly maintain a Typed Selection Set of
RefCountedorResourceobjects for deterministic serialization and netcode. - NEVER forget Command Queuing; strictly implement a Command Pattern using serializable
DictionaryorJSONstates for save-game and multiplayer playback. - NEVER forget to duplicate_deep() globally shared Resources; otherwise, modifying one unit's data (e.g., stats) affects all.
Performance & Simulation
- NEVER render thousands of units using separate
MeshInstance3Dnodes; strictly use `MultiMeshInstance` with `INSTANCE_CUSTOM` data to drive unique GPU-side state animations (walking/attacking/color). - NEVER calculate transforms for mass units on the main thread; strictly use `WorkerThreadPool` to push buffers to
RenderingServer.multimesh_set_buffer(). - NEVER update every unit's navigation path in the same frame; strictly use random timers to stagger updates.
- NEVER use standard Strings for high-frequency AI state identifiers; strictly use StringName (&"harvesting") for pointer-speed comparisons.
- NEVER allow simulation coordinates to exceed 8192 units without float-precision management; strictly use world-origin shifts.
- NEVER use
CSGShape3Dfor building placement ghosts; strictly use optimized staticArrayMeshgeometry.
---
🛠 Expert Components (scripts/)
Original Expert Patterns
- selection_manager_marquee_2d.gd - Professional-grade unit selection system with drag-box, unit filtering, and shift-add support.
Modular Components
- rts_army_manager.gd - Multithreaded AI update system for managing mass units on background cores.
- selection_manager_raycast_3d.gd - Optimized 3D selection using direct PhysicsServer raycasting.
- rts_path_query_pool.gd - Pooled Navigation query system to prevent memory allocations.
- navigation_mask_helper.gd - Bitmask utilities for dynamic navigation layers and avoidance.
- rts_targeting_logic.gd - Distance-squared performance optimization for mass enemy filtering.
- rts_group_commander.gd - SceneTree group broadcasting pattern for decoupled mass units.
- rts_unit_stat_duplicator.gd - Pattern for deep duplicating unit data for isolation.
- rts_unit.gd - Comprehensive unit controller with state management and navigation integration.
- building_grid_astar.gd - High-speed grid-based pathfinding for building placement.
- fog_of_war_tile_mask.gd - Efficient Fog of War clearing using the TileMapLayer API and Vector2i.
- rendering_ghost_spawner.gd - Optimized placement ghosts using RenderingServer RIDs.
---
Core Loop
1. Gather: Units collect resources (Gold, Wood, etc.). 2. Build: Construct base buildings to unlock tech/units. 3. Train: Produce an army of diverse units. 4. Command: Micromanage units in real-time battles. 5. Expand: Secure map control and resources.
Skill Chain
| Phase | Skills | Purpose |
|---|---|---|
| 1. Controls | godot-input-handling, camera-rts | Selection box, camera panning/zoom |
| 2. Units | navigation-server, state-machines | Pathfinding, avoidance, states (Idle/Move/Attack) |
| 3. Systems | fog-of-war, building-system | Map visibility, grid placement |
| 4. AI | behavior-trees, utility-ai | Enemy commander logic |
| 5. Polish | ui-minimap, godot-particles | Strategic overview, battle feedback |
Architecture Overview
1. Selection Manager (Singleton or Commander Node)
Handles mouse input for selecting units.
# selection_manager.gd
extends Node2D
var selected_units: Array[Unit] = []
var drag_start: Vector2
var is_dragging: bool = false
@onready var selection_box: Panel = $SelectionBox
func _unhandled_input(event):
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
if event.pressed:
start_selection(event.position)
else:
end_selection(event.position)
elif event is InputEventMouseMotion and is_dragging:
update_selection_box(event.position)
func end_selection(end_pos: Vector2):
is_dragging = false
selection_box.visible = false
var rect = Rect2(drag_start, end_pos - drag_start).abs()
if Input.is_key_pressed(KEY_SHIFT):
# Add to selection
pass
else:
deselect_all()
# Query physics server for units in rect
var query = PhysicsShapeQueryParameters2D.new()
var shape = RectangleShape2D.new()
shape.size = rect.size
query.shape = shape
query.transform = Transform2D(0, rect.get_center())
# ... execute query and add units to selected_units
for unit in selected_units:
unit.set_selected(true)
func issue_command(target_position: Vector2):
for unit in selected_units:
unit.move_to(target_position)2. Unit Controller (State Machine)
Units need robust state management to handle commands and auto-attacks.
# unit.gd
extends CharacterBody2D
class_name Unit
enum State { IDLE, MOVE, ATTACK, HOLD }
var state: State = State.IDLE
var command_queue: Array[Command] = []
@onready var nav_agent: NavigationAgent2D = $NavigationAgent2D
func move_to(target: Vector2):
nav_agent.target_position = target
state = State.MOVE
func _physics_process(delta):
if state == State.MOVE:
if nav_agent.is_navigation_finished():
state = State.IDLE
return
var next_pos = nav_agent.get_next_path_position()
var direction = global_position.direction_to(next_pos)
velocity = direction * speed
move_and_slide()
### 3. Group Movement & Flocking
Instead of moving all units directly to a single point (clumping), use **Relative Offsets**:
- Calculate the **Center of Mass** for the selected group.
- On click, calculate each unit's **Relative Offset** from the center.
- Issue `target_position + unit_offset` to each unit to maintain formation.3. Fog of War
A system to hide unvisited areas. Usually implemented with a texture and a shader.
- Grid Approach: 2D array of "visibility" values.
- Viewport Texture: A
SubViewportdrawing white circles for units on a black background. This texture is then used as a mask in a shader on a full-screenColorRectoverlay.
shader_type canvas_item;
uniform sampler2D visibility_texture;
uniform vec4 fog_color : source_color;
void fragment() {
float visibility = texture(visibility_texture, UV).r;
COLOR = mix(fog_color, vec4(0,0,0,0), visibility);
}Key Mechanics Implementation
Command Queue
Allow players to chain commands (Shift-Click).
- Implementation: Store commands in an
Array. When one finishes, pop the next. - Visuals: Draw lines showing the queued path.
Resource Gathering
- Nodes:
ResourceNode(Tree/GoldMine) andDropoffPoint(TownCenter). - Logic:
1. Move to Resource. 2. Work (Timer). 3. Move to Dropoff. 4. Deposit (Global Economy update). 5. Repeat.
Common Pitfalls
1. Pathfinding Jitter: Units pushing each other endlessly. Fix: Use RVO (Reciprocal Velocity Obstacles) built into Godot's NavigationAgent2D (properties avoidance_enabled, radius). 2. Too Much Micro: Automate mundane tasks (auto-attack nearby, auto-gather behavior). 3. Performance: Too many nodes. Fix: Use MultiMeshInstance2D for rendering thousands of units if needed, and run logic on a Server node rather than individual scripts for mass units.
Godot-Specific Tips
- Avoidance:
NavigationAgent2Dhas built-in RVO avoidance. Make sure to callset_velocity()and use thevelocity_computedsignal for the actual movement! - Server Architecture: For 100+ units, don't use
_processon every unit. Have a centralUnitManageriterate through active units to save function call overhead. - Groups: Use Groups heavily (
Units,Buildings,Resources) for easy selection filters.
---
🚀 Elite Technical Implementations (Batch 09)
1. Center-of-Mass Formation Movement Pattern
To prevent CPU bottlenecks when moving hundreds of units, avoid querying individual paths. Instead, calculate the "Center of Mass" of the selection and perform a single NavigationServer3D (or 2D) path query.
class_name RTSFormationManager extends Node
## Moves a group of units in formation to a target destination using a single path query.
static func move_group_to_target(units: Array[CharacterBody3D], target_position: Vector3, map_rid: RID) -> void:
if units.is_empty():
return
# 1. Calculate the Center of Mass (Average Position)
var center_of_mass := Vector3.ZERO
for unit in units:
center_of_mass += unit.global_position
center_of_mass /= units.size()
# 2. Query NavigationServer for the optimized central path
var central_path: PackedVector3Array = NavigationServer3D.map_get_path(
map_rid,
center_of_mass,
target_position,
true
)
if central_path.is_empty():
return
var final_center_destination: Vector3 = central_path[central_path.size() - 1]
# 3. Distribute commands with relative offsets to maintain formation
for unit in units:
var offset: Vector3 = unit.global_position - center_of_mass
var unit_destination: Vector3 = final_center_destination + offset
if unit.has_method("set_movement_target"):
unit.set_movement_target(unit_destination)2. MultiMeshInstance Rendering for Massive Armies
Standard nodes fail when unit counts reach thousands. Use MultiMeshInstance3D to draw millions of objects in a single draw call via the GPU.
- Architectural Tip: Pre-allocate the maximum expected units and toggle visibility via
visible_instance_count. - Performance: Note that individual frustum culling is disabled for MultiMesh instances; the entire group is either drawn or not.
class_name RTSMassiveUnitRenderer extends MultiMeshInstance3D
@export var max_units: int = 10000
@export var unit_mesh: Mesh
func _ready() -> void:
multimesh = MultiMesh.new()
multimesh.transform_format = MultiMesh.TRANSFORM_3D
multimesh.use_colors = true
multimesh.instance_count = max_units
multimesh.mesh = unit_mesh
multimesh.visible_instance_count = 0
## Sync logical unit transforms to GPU instances
func synchronize_rendering(active_units: Array[Transform3D]) -> void:
var count: int = min(active_units.size(), max_units)
multimesh.visible_instance_count = count
for i in range(count):
multimesh.set_instance_transform(i, active_units[i])3. SubViewport Fog-of-War System
Avoid complex geometry. Use a SubViewport as a dynamic render target to generate a vision mask (White = Vision, Black = Fog).
Mask Generator Logic:
class_name FogOfWarManager extends SubViewport
@export var terrain_material: ShaderMaterial
@export var world_bounds: Vector2 = Vector2(1024, 1024)
func _ready() -> void:
disable_3d = true
render_target_update_mode = SubViewport.UPDATE_ALWAYS
await RenderingServer.frame_post_draw
var fow_texture: ViewportTexture = get_texture()
terrain_material.set_shader_parameter("fow_mask", fow_texture)
terrain_material.set_shader_parameter("world_bounds", world_bounds)Projection Shader (Spatial):
shader_type spatial;
uniform sampler2D fow_mask : hint_default_black, filter_linear;
uniform vec2 world_bounds;
uniform vec3 fog_color : source_color = vec3(0.1, 0.1, 0.15);
void fragment() {
// Map World X/Z to 2D UV Coordinates
vec2 fow_uv = (NODE_POSITION_WORLD.xz / world_bounds) + vec2(0.5);
float visibility = texture(fow_mask, fow_uv).r;
ALBEDO = mix(fog_color, ALBEDO, visibility);
}- Master Skill: godot-master
# building_grid_astar.gd
extends Node
class_name BuildingGridAStar
# Grid-Based AStar for Base Building
# Instant grid pathfinding for placing structures and unit grid-navigation.
var astar_grid := AStarGrid2D.new()
func init_grid(size: Vector2i, cell_size: Vector2) -> void:
astar_grid.region = Rect2i(Vector2i.ZERO, size)
astar_grid.cell_size = cell_size
astar_grid.diagonal_mode = AStarGrid2D.DIAGONAL_MODE_NEVER
astar_grid.update()
func mark_occupied(cell: Vector2i, occupied: bool) -> void:
# Pattern: AStarGrid2D is much faster than Node-based AStar for grid RTS.
astar_grid.set_point_solid(cell, occupied)
func is_cell_valid(cell: Vector2i) -> bool:
return not astar_grid.is_point_solid(cell)
extends CharacterBody3D
class_name CrowdNavigationUnit
## Expert Crowd Pathfinding (Godot 4.6).
## Optimized RVO avoidance with arrival dampening.
@onready var nav_agent: NavigationAgent3D = $NavigationAgent3D
func _ready() -> void:
nav_agent.avoidance_enabled = true
# Expert Pattern: Increase distance to prevent "dancing" at target
nav_agent.target_desired_distance = 1.5
nav_agent.velocity_computed.connect(_move_unit)
func set_move_target(target: Vector3) -> void:
nav_agent.target_position = target
func _physics_process(_delta: float) -> void:
if nav_agent.is_navigation_finished(): return
var next_path_pos = nav_agent.get_next_path_position()
var new_vel = global_position.direction_to(next_path_pos) * 5.0
# Send preferred velocity to NavigationServer for RVO processing
nav_agent.set_velocity(new_vel)
func _move_unit(safe_vel: Vector3) -> void:
velocity = safe_vel
move_and_slide()
## [SKILL NOTICE]: Set 'target_desired_distance' > 1.0 for crowds.
## This prevents units from oscillating indefinitely when they reach a dense target.
# fog_of_war_tile_mask.gd
extends Node2D
class_name FogOfWarTileMask
# TileMapLayer Fog of War Masking
# Efficiently clears Fog of War using the 2D TileMapLayer grid.
@export var fog_layer: TileMapLayer
func reveal_circular_area(world_pos: Vector2, radius_cells: int) -> void:
if not fog_layer: return
# Pattern: Map world position to discrete 2i grid coordinates.
var center := fog_layer.local_to_map(fog_layer.to_local(world_pos))
for x in range(-radius_cells, radius_cells + 1):
for y in range(-radius_cells, radius_cells + 1):
if Vector2(x, y).length() <= radius_cells:
# pass -1 to layer to "clear" the fog cell.
fog_layer.set_cell(center + Vector2i(x, y), -1)
extends Node
## Expert RTS Economy (Godot 4.6).
## Global Singleton for decoupled resource management.
signal budget_updated(res_id: String, new_total: int)
var _bank: Dictionary = {"gold": 1000, "iron": 500}
func try_spend(costs: Dictionary) -> bool:
# 1. Verification Pass
for id in costs:
if _bank.get(id, 0) < costs[id]: return false
# 2. Deduction Pass
for id in costs:
_bank[id] -= costs[id]
budget_updated.emit(id, _bank[id])
return true
func add_funds(id: String, amount: int) -> void:
_bank[id] = _bank.get(id, 0) + amount
budget_updated.emit(id, _bank[id])
## [SKILL NOTICE]: Use a 'Dictionary'-based cost system within an 'Autoload'.
## This allows flexible multi-resource checkouts (e.g., Gold + Iron) for units.
# navigation_mask_helper.gd
extends RefCounted
class_name NavigationMaskHelper
# Navigation Layer Bitmasking for Avoidance
# Dynamically alters agent layers to ignore hazard regions or locked gates.
static func toggle_navigation_layer(agent: NavigationAgent3D, layer_index: int, enabled: bool) -> void:
# Pattern: Bitwise masking for efficient layer management.
if enabled:
agent.navigation_layers |= (1 << layer_index)
else:
agent.navigation_layers &= ~(1 << layer_index)
static func set_swimming_capability(agent: NavigationAgent3D, can_swim: bool) -> void:
# Example: Layer 2 is water.
toggle_navigation_layer(agent, 2, can_swim)
# rendering_ghost_spawner.gd
extends Node
class_name RenderingGhostSpawner
# Server-Side Rendering for Building Ghosts
# Bypasses SceneTree overhead for ghost visuals using RenderingServer.
var _ghost_rid: RID
func create_placement_ghost(mesh_rid: RID, scenario: RID) -> void:
# Pattern: Directly instantiate in visual server to avoid costly Node lifecycle.
_ghost_rid = RenderingServer.instance_create()
RenderingServer.instance_set_base(_ghost_rid, mesh_rid)
RenderingServer.instance_set_scenario(_ghost_rid, scenario)
# Optional: Apply ghost shader/material.
# RenderingServer.instance_set_surface_override_material(...)
func update_ghost_transform(xform: Transform3D) -> void:
if _ghost_rid.is_valid():
RenderingServer.instance_set_transform(_ghost_rid, xform)
func destroy_ghost() -> void:
if _ghost_rid.is_valid():
RenderingServer.free_rid(_ghost_rid)
# rts_army_manager.gd
extends Node
class_name RTSArmyManager
# High-Performance Multithreaded AI Updates
# Offloads thousands of unit state calculations to background worker threads.
var _units: Array[Node] = []
func _physics_process(_delta: float) -> void:
if _units.is_empty(): return
# Pattern: Use WorkerThreadPool to distribute heavy AI/Targeting across all cores.
var task_id := WorkerThreadPool.add_group_task(_process_unit_ai, _units.size())
# IMPORTANT: Wait for completion if subsequent logic (like movement) depends on results.
WorkerThreadPool.wait_for_group_task_completion(task_id)
func _process_unit_ai(index: int) -> void:
var unit := _units[index]
# Execute expensive logic here: Targeting, LOS checks, or state evaluation.
pass
func register_unit(unit: Node) -> void:
_units.append(unit)
func unregister_unit(unit: Node) -> void:
_units.erase(unit)
# rts_group_commander.gd
extends Node
class_name RTSGroupCommander
# Decoupled Group Broadcasting for Mass Commands
# Instantly alerts all units in a control group without hardcoded array iteration.
func order_move_group(group_id: StringName, target_pos: Vector3) -> void:
# Pattern: Use call_group_flags with DEFERRED and UNIQUE for safety and efficiency.
var flags := SceneTree.GROUP_CALL_DEFERRED | SceneTree.GROUP_CALL_UNIQUE
get_tree().call_group_flags(
flags,
group_id, # Target group (e.g., &"group_1")
&"move_to", # Target method in unit script
target_pos # Arg
)
# rts_path_query_pool.gd
extends Node3D
class_name RTSPathQueryPool
# Object-Pooled Navigation Path Queries
# Reuses query objects to prevent constant heap allocation during mass move orders.
var _query_params := NavigationPathQueryParameters3D.new()
var _query_result := NavigationPathQueryResult3D.new()
func get_optimized_path(start: Vector3, target: Vector3, layers: int = 1) -> PackedVector3Array:
# Pattern: Configure pre-allocated query objects instead of creating new ones.
_query_params.start_position = start
_query_params.target_position = target
_query_params.navigation_layers = layers
_query_params.path_postprocessing = NavigationPathQueryParameters3D.PATH_POSTPROCESSING_CORRIDORFUNNEL
var map_rid := get_world_3d().get_navigation_map()
NavigationServer3D.query_path(map_rid, _query_params, _query_result)
return _query_result.path
extends Control
class_name RTSSelectionOverlay
## Expert Box Selection (Godot 4.6).
## Draws a 2D box and projects 3D units to screen space for selection.
@export var camera: Camera3D
@export var box_color = Color(0, 1, 0, 0.2)
var _start_pos = Vector2.ZERO
var _is_dragging = false
func _unhandled_input(event: InputEvent) -> void:
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
if event.pressed:
_start_pos = event.position
_is_dragging = true
else:
_is_dragging = false
_select_units(Rect2(_start_pos, event.position - _start_pos).abs())
queue_redraw()
if event is InputEventMouseMotion and _is_dragging:
queue_redraw()
func _draw() -> void:
if _is_dragging:
draw_rect(Rect2(_start_pos, get_local_mouse_position() - _start_pos), box_color, true)
func _select_units(rect: Rect2) -> void:
var selected = []
for unit in get_tree().get_nodes_in_group("units"):
if camera.is_position_behind(unit.global_position): continue
var screen_pos = camera.unproject_position(unit.global_position)
if rect.has_point(screen_pos):
selected.append(unit)
unit.set_selected(true)
## [SKILL NOTICE]: Project 3D positions to 2D for box selection. It is much
## more performant than scaling a 3D Area3D or frustum-casting every frame.
# rts_targeting_logic.gd
extends RefCounted
class_name RTSTargetingLogic
# Fast Distance Squared Checking
# Bypasses expensive square-root math when filtering thousands of potential targets.
static func find_nearest_target(origin: Vector3, targets: Array[Node3D]) -> Node3D:
var best_target: Node3D = null
var min_dist_sq := INF
for target in targets:
# Pattern: Use distance_squared_to to save CPU cycles in mass loops.
var d_sq := origin.distance_squared_to(target.global_position)
if d_sq < min_dist_sq:
min_dist_sq = d_sq
best_target = target
return best_target
# rts_unit_stat_duplicator.gd
extends Node
class_name RTSUnitStatDuplicator
# Deep Duplication of Unit Stats
# Ensures unique health/armor per unit instead of sharing a global Resource.
@export var base_stats: Resource
var active_stats: Resource
func _ready() -> void:
if base_stats:
# Pattern: Deep duplication to isolate dictionaries/arrays within the resource.
active_stats = base_stats.duplicate(true)
func modify_stat(stat_name: StringName, amount: float) -> void:
if active_stats and stat_name in active_stats:
active_stats.set(stat_name, active_stats.get(stat_name) + amount)
# skills/genre-rts/scripts/rts_unit.gd
extends CharacterBody2D
## RTS Unit Entity (Expert Pattern)
## Handles state machine logic (Idle, Move, Attack) and pathfinding.
## Integrates with NavigationAgent2D for RVO avoidance.
class_name RTSUnit
enum State { IDLE, MOVE, ATTACK, HOLD }
@export var speed: float = 150.0
@export var attack_range: float = 40.0
@export var damage: int = 5
@export var nav_agent: NavigationAgent2D
var state: State = State.IDLE
var current_target: Node2D
var _is_selected: bool = false
@onready var selection_visual: Sprite2D = $SelectionCircle
func _ready() -> void:
# Setup RVO
nav_agent.velocity_computed.connect(_on_velocity_computed)
nav_agent.avoidance_enabled = true
nav_agent.radius = 20.0
if selection_visual:
selection_visual.visible = false
func move_to(target_pos: Vector2) -> void:
nav_agent.target_position = target_pos
state = State.MOVE
func set_selected(selected: bool) -> void:
_is_selected = selected
if selection_visual:
selection_visual.visible = selected
func _physics_process(delta: float) -> void:
if state == State.MOVE:
if nav_agent.is_navigation_finished():
state = State.IDLE
velocity = Vector2.ZERO
return
var next = nav_agent.get_next_path_position()
var dir = global_position.direction_to(next)
# Trigger avoidance calc
nav_agent.set_velocity(dir * speed)
elif state == State.ATTACK:
# Attack logic here
pass
func _on_velocity_computed(safe_velocity: Vector2) -> void:
# Callback from RVO avoidance
velocity = safe_velocity
move_and_slide()
## EXPERT USAGE:
## Attach NavigationAgent2D CHILD. Assign it to 'nav_agent'.
## Connect 'velocity_computed' signal in inspector or _ready (done above).
# godot-master/scripts/rts_rts_selection_manager.gd
extends Node2D
## RTS Selection Manager (Expert Pattern)
## Handles 2D/3D unit selection via drag box and single clicks.
## Supports Shift-Add and broadcasting commands to selected units.
class_name RTSSelectionManager
signal selection_changed(selected_units: Array)
@export var selection_box_color: Color = Color(0, 1, 0, 0.3)
@export var selection_border_color: Color = Color(0, 1, 0, 1)
var selected_units: Array = []
var drag_start: Vector2 = Vector2.ZERO
var is_dragging: bool = false
var _box_rect: Rect2
func _unhandled_input(event: InputEvent) -> void:
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_LEFT:
if event.pressed:
_start_drag(event.position)
else:
_end_drag(event.position)
elif event.button_index == MOUSE_BUTTON_RIGHT and event.pressed:
_issue_move_command(get_global_mouse_position()) # Or raycast for 3D
elif event is InputEventMouseMotion and is_dragging:
queue_redraw()
func _start_drag(pos: Vector2) -> void:
drag_start = pos
is_dragging = true
func _end_drag(pos: Vector2) -> void:
is_dragging = false
queue_redraw()
var drag_rect = Rect2(drag_start, pos - drag_start).abs()
if not Input.is_key_pressed(KEY_SHIFT):
_deselect_all()
if drag_rect.size.length_squared() < 100:
# Single Click
_select_at_point(pos)
else:
# Box Select
_select_in_rect(drag_rect)
selection_changed.emit(selected_units)
func _select_at_point(pos: Vector2) -> void:
# 2D Implementation (Physics Query)
var space = get_world_2d().direct_space_state
var query = PhysicsPointQueryParameters2D.new()
query.position = get_global_mouse_position() # Use global for query
query.collide_with_areas = true # Assuming units have areas or bodies
var results = space.intersect_point(query)
if not results.is_empty():
var unit = results[0].collider
if unit.has_method("set_selected"):
_add_to_selection(unit)
func _select_in_rect(rect: Rect2) -> void:
# Need to convert Viewport Screen Rect to World Rect if camera moves
# For now assuming simple 2D screen-space match or using `get_global_mouse_position` logic
# Better: Query physics with a Shape
var space = get_world_2d().direct_space_state
var query = PhysicsShapeQueryParameters2D.new()
var shape = RectangleShape2D.new()
shape.size = rect.size
query.shape = shape
query.transform = Transform2D(0, (drag_start + (rect.size / 2)) + get_canvas_transform().origin ) # Rough approximation, requires camera awareness
# Actually, easiest is to iterate all "selectable" group nodes and check if inside rect
# This avoids complex shape transform math for screen-to-world rect
var candidates = get_tree().get_nodes_in_group("selectable")
for unit in candidates:
# Check if unit screen position is inside drag_rect
var screen_pos = unit.get_global_transform_with_canvas().origin
if rect.has_point(screen_pos):
_add_to_selection(unit)
func _add_to_selection(unit: Node) -> void:
if unit not in selected_units:
selected_units.append(unit)
unit.set_selected(true)
func _deselect_all() -> void:
for unit in selected_units:
if is_instance_valid(unit):
unit.set_selected(false)
selected_units.clear()
func _issue_move_command(target: Vector2) -> void:
for unit in selected_units:
if unit.has_method("move_to"):
unit.move_to(target)
func _draw() -> void:
if is_dragging:
var mouse = get_local_mouse_position()
var rect = Rect2(drag_start, mouse - drag_start)
draw_rect(rect, selection_box_color, true)
draw_rect(rect, selection_border_color, false, 2.0)
## EXPERT USAGE:
## Add units to "selectable" group. Implement set_selected(bool) and move_to(pos) on units.
## For 3D, replace PhysicsPointQueryParameters2D with Raycast logic.
# rts_selection_manager.gd
extends Node3D
class_name RTSSelectionManager
# Fast Physics-Server Raycasting for Unit Selection
# Bypasses SceneTree overhead by querying the C++ PhysicsServer3D directly.
func select_unit_at_mouse(camera: Camera3D, mouse_pos: Vector2) -> Object:
var space_state := get_world_3d().direct_space_state
# Standard ray projection from camera.
var origin := camera.project_ray_origin(mouse_pos)
var normal := camera.project_ray_normal(mouse_pos)
var query := PhysicsRayQueryParameters3D.create(origin, origin + normal * 1000.0)
query.collide_with_areas = false
query.collide_with_bodies = true
# Pattern: Direct server lookup is faster than Area3D detection for mass units.
var result := space_state.intersect_ray(query)
return result.get("collider") if not result.is_empty() else null