
Godot Genre Shooter
- 140 installs
- 454 repo stars
- Updated July 28, 2026
- thedivergentai/gd-agentic-skills
Use godot-genre-shooter for development tasks
About
godot-genre-shooter: A skill for development. This provides functionality for development workflows.
- godot-genre-shooter
Godot Genre Shooter 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-shooterAdd 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-shooter for development tasks
Files
Genre: Shooter (FPS/TPS)
Gunplay feel, responsive combat, and competitive balance define shooters.
Available Scripts
advanced_weapon_controller.gd
Expert pattern for recoil, bloom, and dual hitscan/projectile systems with object pooling notes.
Core Loop
Engage → Aim → Fire → Kill Confirm → Acquire Next
NEVER Do (Expert Anti-Patterns)
Gunplay & Hit Registration
- NEVER use
_process()for hit detection; strictly use `_physics_process()` to maintain frame-rate independent accuracy (aiming/firing are physics events). - NEVER apply recoil solely to the weapon model transform; strictly apply it to Camera Rotation (kick) and Weapon Bloom (spread).
- NEVER use
Area3Doverlap for high-speed hit detection; strictly use `PhysicsDirectSpaceState3D.intersect_ray()` for 100x better performance. - NEVER trust the client for hit registration in multiplayer; strictly use Server-Authoritative validation using lag compensation (rewinding).
- NEVER synchronize every bullet over the network; strictly use Client-Side Prediction for visual tracers and only send the initial "Fire" event.
- NEVER forget to exclude the player's own RID from hitscan raycasts; strictly use `add_exception()` to prevent shots colliding with the weapon barrel.
- NEVER use exact floating-point equality (==) for bullet damage or health; strictly use `is_equal_approx()` to mitigate precision loss.
Performance & Architecture
- NEVER hardcode weapon statistics (Damage, Recoil) inside logic; strictly use Resource-based WeaponData for rapid balancing.
- NEVER use a single
AudioStreamPlayerfor gunfire; strictly use Layered Audio (Mechanical + Shot + Reverb Tail) for punchy feedback. - NEVER instantiate and
free()hundreds of projectile nodes; strictly use Object Pooling or thePhysicsServer3DAPI for stability. - NEVER use
Sprite3Dfor bullet impacts on surfaces; strictly use the Decal node for conforming, perspective-correct projection. - NEVER use absolute pixel positioning for crosshairs; strictly rely on Anchors & RectCenter to ensure accuracy across resolutions.
- NEVER scale
CollisionShape3Dnon-uniformly; strictly scale the Internal Shape Resource to maintain valid physics calculations. - NEVER use TCP for multiplayer shooter synchronization; strictly use ENet (UDP) with unreliable transfer modes to avoid latency spikes.
---
🛠 Expert Components (scripts/)
Original Expert Patterns
- advanced_weapon_controller.gd - Professional-grade weapon system with deterministic recoil, bloom, and dual firing modes.
Modular Components
- shooter_patterns.gd - Reusable patterns: Server-bypassing hitscan, random spread, and ShapeCast3D explosions.
---
Weapon System Architecture
class_name Weapon
extends Node3D
@export_group("Stats")
@export var damage: int = 20
@export var fire_rate: float = 0.1 # Seconds between shots
@export var magazine_size: int = 30
@export var reload_time: float = 2.0
@export var range: float = 100.0
@export_group("Recoil")
@export var base_recoil: Vector2 = Vector2(0.5, 2.0) # X, Y degrees
@export var recoil_recovery_speed: float = 5.0
@export var max_spread: float = 5.0
@export_group("Type")
@export var is_hitscan: bool = true
@export var projectile_scene: PackedScene
var current_ammo: int
var can_fire: bool = true
var current_recoil: Vector2 = Vector2.ZERO
var current_spread: float = 0.0
signal fired
signal reloaded
signal ammo_changed(current: int, max: int)---
Hitscan vs Projectile
Hitscan (Instant Hit)
func fire_hitscan() -> void:
if not can_fire or current_ammo <= 0:
return
current_ammo -= 1
ammo_changed.emit(current_ammo, magazine_size)
var camera := get_viewport().get_camera_3d()
var ray_origin := camera.global_position
var ray_direction := -camera.global_basis.z
# Apply spread
ray_direction = apply_spread(ray_direction)
var space := get_world_3d().direct_space_state
var query := PhysicsRayQueryParameters3D.create(
ray_origin,
ray_origin + ray_direction * range
)
query.collision_mask = collision_mask
var result := space.intersect_ray(query)
if result:
var hit_point: Vector3 = result.position
var hit_normal: Vector3 = result.normal
var hit_object: Object = result.collider
spawn_impact_effect(hit_point, hit_normal)
if hit_object.has_method("take_damage"):
var hit_zone := determine_hit_zone(result)
var final_damage := calculate_damage(damage, hit_zone)
hit_object.take_damage(final_damage, hit_zone)
apply_recoil()
start_fire_cooldown()
fired.emit()
func determine_hit_zone(result: Dictionary) -> String:
# Use collision shape name or bone detection for hitboxes
if "headshot" in result.collider.name.to_lower():
return "head"
elif "chest" in result.collider.name.to_lower():
return "chest"
return "body"
func calculate_damage(base: int, zone: String) -> int:
match zone:
"head": return int(base * 2.5)
"chest": return int(base * 1.0)
_: return int(base * 0.8)Projectile (Physical Bullet)
class_name Projectile
extends CharacterBody3D
@export var speed := 100.0
@export var damage := 20
@export var gravity_affected := true
@export var lifetime := 5.0
var direction: Vector3
var shooter: Node3D
func _ready() -> void:
await get_tree().create_timer(lifetime).timeout
queue_free()
func _physics_process(delta: float) -> void:
if gravity_affected:
velocity.y -= 9.8 * delta
velocity = direction * speed
var collision := move_and_collide(velocity * delta)
if collision:
var collider := collision.get_collider()
if collider != shooter and collider.has_method("take_damage"):
collider.take_damage(damage)
spawn_impact(collision.get_position(), collision.get_normal())
queue_free()---
Recoil System
Three types of recoil working together:
class_name RecoilSystem
extends Node
var visual_recoil: Vector2 = Vector2.ZERO # Camera kick
var pattern_offset: Vector2 = Vector2.ZERO # Deterministic pattern
var spread_bloom: float = 0.0 # Accuracy loss
@export var recoil_pattern: Array[Vector2] # Predefined spray pattern
var pattern_index: int = 0
func apply_recoil(weapon: Weapon) -> void:
# 1. Visual recoil - camera kick
visual_recoil.y += weapon.base_recoil.y * randf_range(0.8, 1.2)
visual_recoil.x += weapon.base_recoil.x * randf_range(-1.0, 1.0)
# 2. Pattern recoil - learnable spray
if pattern_index < recoil_pattern.size():
pattern_offset += recoil_pattern[pattern_index]
pattern_index += 1
# 3. Spread bloom - reduced accuracy
spread_bloom = min(spread_bloom + 0.5, weapon.max_spread)
func recover_recoil(delta: float, recovery_speed: float) -> void:
visual_recoil = visual_recoil.lerp(Vector2.ZERO, recovery_speed * delta)
pattern_offset = pattern_offset.lerp(Vector2.ZERO, recovery_speed * delta)
spread_bloom = lerp(spread_bloom, 0.0, recovery_speed * delta)
if visual_recoil.length() < 0.01:
pattern_index = 0 # Reset pattern
func get_spread_direction(base_direction: Vector3) -> Vector3:
var spread_angle := deg_to_rad(spread_bloom)
var random_offset := Vector2(
randf_range(-spread_angle, spread_angle),
randf_range(-spread_angle, spread_angle)
)
return base_direction.rotated(Vector3.UP, random_offset.x).rotated(Vector3.RIGHT, random_offset.y)---
Aim Assist (Controller Support)
class_name AimAssist
extends Node3D
@export var assist_range := 50.0
@export var assist_angle := 15.0 # Degrees
@export var friction_strength := 0.3 # Slowdown near targets
@export var magnetism_strength := 0.1 # Pull toward targets
func apply_aim_assist(look_input: Vector2, camera: Camera3D) -> Vector2:
var target := find_closest_target(camera)
if not target:
return look_input
var to_target := target.global_position - camera.global_position
var camera_forward := -camera.global_basis.z
var angle := rad_to_deg(camera_forward.angle_to(to_target.normalized()))
if angle > assist_angle:
return look_input
# Friction - slow movement near targets
var friction := 1.0 - (friction_strength * (1.0 - angle / assist_angle))
look_input *= friction
# Magnetism - subtle pull toward target
var target_screen_pos := camera.unproject_position(target.global_position)
var screen_center := get_viewport().get_visible_rect().size / 2
var pull_direction := (target_screen_pos - screen_center).normalized()
look_input += pull_direction * magnetism_strength * (1.0 - angle / assist_angle)
return look_input
func find_closest_target(camera: Camera3D) -> Node3D:
var closest: Node3D = null
var closest_angle := assist_angle
for target in get_tree().get_nodes_in_group("enemies"):
var to_target := target.global_position - camera.global_position
var angle := rad_to_deg((-camera.global_basis.z).angle_to(to_target.normalized()))
if angle < closest_angle and to_target.length() < assist_range:
if has_line_of_sight(camera.global_position, target.global_position):
closest = target
closest_angle = angle
return closest---
Weapon Feel Polish
Camera Effects
func on_weapon_fired() -> void:
# Screen shake
camera_shake(0.1, 0.05)
# FOV punch
camera.fov += 2.0
await get_tree().create_timer(0.05).timeout
camera.fov -= 2.0
# Muzzle flash
muzzle_flash.visible = true
await get_tree().create_timer(0.02).timeout
muzzle_flash.visible = false
func on_weapon_reloaded() -> void:
# Lock controls during reload
can_fire = false
can_aim = false
play_animation("reload")
await get_tree().create_timer(reload_time).timeout
current_ammo = magazine_size
can_fire = true
can_aim = trueAudio Layering
@export var fire_sounds: Array[AudioStream] # Random selection
@export var tail_sound: AudioStream # Reverb/echo
@export var mechanical_sound: AudioStream # Gun mechanism
func play_fire_audio() -> void:
# Main shot
var shot := fire_sounds.pick_random()
fire_audio_player.stream = shot
fire_audio_player.play()
# Mechanical click
mechanical_player.play()
# Tail (delayed reverb)
await get_tree().create_timer(0.1).timeout
tail_player.play()---
Weapon Selection Decision Tree
When designing weapon balance:
- High fire rate (SMG) = Low damage per shot, rewards tracking aim
- Low fire rate (Sniper) = High damage, rewards precision
- Shotguns = Spread pattern (5-8 pellets), effective range <10m
- ARs = Jack-of-all-trades, medium everything
Technical implementation:
- Pistol/AR: Hitscan (instant feedback)
- Rocket/Grenade: Projectile with gravity
- S niper: Hitscan with tracer visual
Multiplayer Client Prediction Pattern
# CLIENT: Instant feedback, no waiting for server
func fire_client() -> void:
play_effects_immediate() # Muzzle flash, recoil, audio
local_hitscan_visual() # Visual blood splatter only
rpc_id(1, "server_validate_shot", camera.global_transform)
# SERVER: Authoritative damage
@rpc("any_peer")
func server_validate_shot(shooter_transform: Transform3D) -> void:
var hit = perform_server_hitscan(shooter_transform)
if hit and is_valid_shot(hit):
rpc("confirm_hit", hit.victim_id, hit.damage)
# EDGE CASE: What if client's visual hit doesn't match server?
# SOLUTION: Server wins. Client shows "no reg" indicator if mismatch.Common Pitfalls & Expert Fixes
- Weak bullet impact → Triple-layer audio (shot+tail+mechanical) + screen shake + blood VFX + damage number
- Guns feel identical → Unique recoil patterns (SMG: tight vertical, AK: strong horizontal kick)
- No skill ceiling → Learnable spray patterns (CS:GO style), not pure RNG spread
- Controller aim frustration → Friction (0.3 slowdown near targets) + subtle 0.1 magnetism
---
Godot-Specific Tips
1. Raycasts: Use PhysicsRayQueryParameters3D with proper layer masks 2. Projectiles: CharacterBody3D or RigidBody3D depending on physics needs 3. Audio: Multiple AudioStreamPlayer3D for layered gun sounds 4. Animations: AnimationTree for weapon state machines (idle, aim, fire, reload)
Advanced Shooter Meta-Systems
Elite implementation of tactical positioning, realistic ballistics, and environmental interaction.
1. Spray-Pattern Editor (Recoil Math)
Utilize Godot's custom Resource system to define reusable data containers. By creating a script that inherits from Resource and using the @export annotation on an Array[Vector2], you can manually define weapon recoil patterns that are editable directly in the Godot Inspector.
class_name WeaponRecoilPattern extends Resource
@export var spray_points: Array[Vector2] = []
@export var horizontal_variance: float = 0.1
@export var vertical_variance: float = 0.1
func get_recoil_at(shot_index: int) -> Vector2:
if spray_points.is_empty(): return Vector2.ZERO
var base := spray_points[shot_index % spray_points.size()]
return base + Vector2(randf() * horizontal_variance, randf() * vertical_variance)2. Lag-Compensation (Server Rewinding)
To implement authoritative hit validation, store a history of player positions in a ring buffer. When a shot is fired, the server temporarily "rewinds" target colliders to their historical positions based on the client's timestamp, performs the raycast, and restores them.
class_name LagCompensator extends Node
var _history: Array[Dictionary] = [] # { "time": int, "transform": Transform3D }
const MAX_BACKTRACK_MS = 200
func _physics_process(_delta: float) -> void:
_history.append({"time": Time.get_ticks_msec(), "transform": owner.global_transform})
if _history.size() > 60: # ~1 second at 60fps
_history.pop_front()
func backtrack_to(timestamp: int) -> void:
var best_match = _history[0]
for entry in _history:
if abs(entry.time - timestamp) < abs(best_match.time - timestamp):
best_match = entry
owner.global_transform = best_match.transform3. ShapeCast3D-Explosion
To query an explosion volume without node overhead, use PhysicsDirectSpaceState3D.intersect_shape(). This returns an array of dictionaries representing all colliders within the sphere volume in a single call.
class_name ExplosionQuery extends Node3D
func execute_explosion(radius: float) -> Array[Dictionary]:
var space_state := get_world_3d().direct_space_state
var sphere := SphereShape3D.new()
sphere.radius = radius
var query := PhysicsShapeQueryParameters3D.new()
query.shape = sphere
query.transform = global_transform
query.collision_mask = 1 # Environment/Players
return space_state.intersect_shape(query, 32)Architectural Tip: When implementing Lag Compensation, always perform the raycast in a single frame and immediately restore transforms to prevent physics glitches in other server calculations.
Reference
- Master Skill: godot-master
# skills/genre-shooter/scripts/advanced_weapon_controller.gd
extends Node3D
## Advanced Weapon Controller
## Procedural Recoil, Bloom, and Hybrid Hitscan/Projectile Logic.
class_name AdvancedWeaponController
signal weapon_fired(current_ammo: int)
@export_group("Stats")
@export var fire_rate: float = 0.1
@export var max_ammo: int = 30
@export var damage: float = 25.0
@export var is_hitscan: bool = true
@export var projectile_scene: PackedScene
@export var projectile_speed: float = 50.0
@export_group("Recoil & Spread")
@export var recoil_kick: Vector2 = Vector2(0.5, 2.0) # Horizontal, Vertical (deg)
@export var recoil_recovery: float = 10.0 # deg/sec
@export var max_recoil_x: float = 5.0
@export var max_recoil_y: float = 10.0
@export var spread_per_shot: float = 0.5
@export var max_spread: float = 5.0
# Dependencies
@onready var camera: Camera3D = get_viewport().get_camera_3d()
# State
var current_ammo: int
var _fire_timer: float = 0.0
var _current_recoil: Vector2 = Vector2.ZERO
var _current_spread: float = 0.0
var _trigger_held: bool = false
func _ready() -> void:
current_ammo = max_ammo
func _process(delta: float) -> void:
_fire_timer -= delta
# Recoil Recovery
_current_recoil = _current_recoil.move_toward(Vector2.ZERO, recoil_recovery * delta)
_current_spread = move_toward(_current_spread, 0.0, recoil_recovery * delta)
# Apply visual rotation to camera (or weapon model)
if camera:
# Note: In real FPS, apply this as a separate offset/rotation to avoid drifting the actual view permanently
# For this snippet, we'll assume a 'recoil_container' or similar approach is best,
# but here is the logic for the offsets:
pass
func trigger_down() -> void:
_trigger_held = true
if _fire_timer <= 0:
_fire()
func trigger_up() -> void:
_trigger_held = false
func _fire() -> void:
if current_ammo <= 0: return # Play dry fire sound
current_ammo -= 1
_fire_timer = fire_rate
# calculate spread
var spread_angle = deg_to_rad(_current_spread)
var spread_vector = Vector3(randf_range(-spread_angle, spread_angle), randf_range(-spread_angle, spread_angle), 0)
if is_hitscan and camera:
var forward = -camera.global_transform.basis.z
# Apply spread rotation
var aim_dir = forward + camera.global_transform.basis * spread_vector
aim_dir = aim_dir.normalized()
# Raycast
var space = get_world_3d().direct_space_state
var query = PhysicsRayQueryParameters3D.create(camera.global_position, camera.global_position + aim_dir * 1000.0)
var result = space.intersect_ray(query)
if result:
if result.collider.has_method("take_damage"):
result.collider.take_damage(damage)
elif projectile_scene:
var proj = projectile_scene.instantiate()
get_tree().root.add_child(proj)
proj.global_transform = camera.global_transform
# Apply spread to projectile
proj.rotation.x += randf_range(-deg_to_rad(_current_spread), deg_to_rad(_current_spread))
proj.rotation.y += randf_range(-deg_to_rad(_current_spread), deg_to_rad(_current_spread))
# Apply Recoil kick
_current_recoil.x = clamp(_current_recoil.x + randf_range(-recoil_kick.x, recoil_kick.x), -max_recoil_x, max_recoil_x)
_current_recoil.y = clamp(_current_recoil.y + recoil_kick.y, 0, max_recoil_y) # Kick up
_current_spread = clamp(_current_spread + spread_per_shot, 0, max_spread)
weapon_fired.emit(current_ammo)
# Auto-fire logic
if _trigger_held and fire_rate > 0:
await get_tree().create_timer(fire_rate).timeout
if _trigger_held: _fire()
## EXPERT USAGE:
## Call trigger_down()/target_up() from Input.
## Bind 'current_recoil' to a CameraGL/SpringArm offset script for visual shake.
extends Node3D
class_name CoverValidatorRays
## Expert Cover Detection (Godot 4.6).
## Uses a cluster of RayCasts to detect cover height and peeking.
@onready var ray_low: RayCast3D = $RayLow
@onready var ray_high: RayCast3D = $RayHigh
@onready var ray_left: RayCast3D = $RayLeft
@onready var ray_right: RayCast3D = $RayRight
enum CoverState { NONE, HALF, FULL }
func get_cover_state() -> CoverState:
if not ray_low.is_colliding(): return CoverState.NONE
if ray_high.is_colliding(): return CoverState.FULL
return CoverState.HALF
func can_peek_side() -> int:
# Returns -1 (Left), 1 (Right), or 0 (None)
if not ray_left.is_colliding(): return -1
if not ray_right.is_colliding(): return 1
return 0
## [SKILL NOTICE]: Cluster multiple RayCast3D nodes to detect
## environmental context (like cover height) in a single physics frame.
# skills/genre-shooter/scripts/projectile.gd
extends CharacterBody3D
## Projectile Expert Pattern
## Physical bullet with gravity, bounce, and lifetime management.
class_name Projectile
@export var speed: float = 50.0
@export var damage: int = 25
@export var gravity_scale: float = 1.0
@export var max_lifetime: float = 5.0
@export var bounces: int = 0
var _velocity: Vector3 = Vector3.ZERO
var _timer: float = 0.0
func setup(direction: Vector3, start_pos: Vector3) -> void:
look_at_from_position(start_pos, start_pos + direction)
_velocity = direction * speed
func _physics_process(delta: float) -> void:
_timer += delta
if _timer >= max_lifetime:
queue_free()
return
# Gravity
_velocity.y -= 9.8 * gravity_scale * delta
velocity = _velocity
var collision = move_and_collide(velocity * delta)
if collision:
_handle_collision(collision)
func _handle_collision(collision: KinematicCollision3D) -> void:
var collider = collision.get_collider()
if collider.has_method("take_damage"):
collider.take_damage(damage)
queue_free()
elif bounces > 0:
bounces -= 1
_velocity = _velocity.bounce(collision.get_normal())
# Reflect visual
look_at(global_position + _velocity)
else:
# Spawn impact effect
queue_free()
## EXPERT USAGE:
## Instantiate from WeaponController. Call setup().
# shooter_patterns.gd
extends Node
# 1. Server-Bypassing Hitscan (PhysicsDirectSpaceState3D)
# EXPERT NOTE: Direct raycasting is faster than standard ray nodes for high-frequency fire.
func fire_hitscan(origin: Vector3, direction: Vector3, shooter_rid: RID) -> void:
var space_state := get_world_3d().direct_space_state
var query := PhysicsRayQueryParameters3D.create(origin, origin + direction * 100)
query.exclude = [shooter_rid]
var result := space_state.intersect_ray(query)
if result and result.collider.has_method(&"take_damage"):
result.collider.call(&"take_damage", 10)
# 2. Applying Random Spread (Normal Distribution)
# EXPERT NOTE: randfn generates numbers clustered around the mean (0.0) for natural spread.
func calculate_spread(forward_vector: Vector3, spread_factor: float) -> Vector3:
var deviation := Vector3(randfn(0.0, spread_factor), randfn(0.0, spread_factor), 0)
return (forward_vector + deviation).normalized()
# 3. ShapeCast3D for AoE Explosions
# EXPERT NOTE: Efficient sphere casting to detect multiple targets for explosions.
func detonate_rocket(shape_rid: RID, pos: Transform3D) -> void:
var space_state := get_world_3d().direct_space_state
var query := PhysicsShapeQueryParameters3D.new()
query.shape_rid = shape_rid
query.transform = pos
var results := space_state.intersect_shape(query)
for hit in results:
if hit.collider.has_method(&"apply_impulse"):
hit.collider.apply_impulse(Vector3.UP * 10)
# 4. Spawning Bullet Hole Decals
# EXPERT NOTE: Use RenderingServer for low-overhead decal placement.
func spawn_decal(pos: Vector3, normal: Vector3) -> void:
var decal_rid := RenderingServer.decal_create()
RenderingServer.instance_set_transform(decal_rid, Transform3D(Basis(), pos))
# Additional setup: set texture, size, and world scenario
# 5. Projectile Server Instantiation (Bypassing Nodes)
# EXPERT NOTE: Create physics bodies directly in the server for bullet hell performance.
func spawn_server_bullet(transform: Transform3D) -> RID:
var body_rid := PhysicsServer3D.body_create()
PhysicsServer3D.body_set_mode(body_rid, PhysicsServer3D.BODY_MODE_KINEMATIC)
PhysicsServer3D.body_set_space(body_rid, get_world_3d().space)
PhysicsServer3D.body_set_state(body_rid, PhysicsServer3D.BODY_STATE_TRANSFORM, transform)
return body_rid
# 6. Recoil Interpolation via Tweens
# EXPERT NOTE: Procedural recoil that smoothly returns to center.
func apply_recoil(camera: Camera3D, kickback: float) -> void:
var tween := create_tween()
tween.tween_property(camera, "rotation:x", camera.rotation.x + kickback, 0.05)
tween.tween_property(camera, "rotation:x", camera.rotation.x, 0.1)
# 7. AI Taking Cover (NavigationServer3D)
# EXPERT NOTE: Inform the navigation agent of its position manually for server-side AI.
func move_ai_to_cover(agent_rid: RID, cover_pos: Vector3, global_pos: Vector3) -> void:
NavigationServer3D.agent_set_position(agent_rid, global_pos)
# Target setting happens in background gen
NavigationServer3D.agent_set_velocity(agent_rid, (cover_pos - global_pos).normalized() * 5.0)
# 8. Server-Authoritative Firing
# EXPERT NOTE: Clients request; server validates and executes the shot.
@rpc("any_peer", "call_local", "reliable")
func request_fire(target_vector: Vector3) -> void:
if multiplayer.is_server():
var sender_id := multiplayer.get_remote_sender_id()
# Validate ammo, cooldown, and line-of-sight here
print("Server validated shot for player: ", sender_id)
# 9. Dynamic Weapon Crosshair Binding
# EXPERT NOTE: Use unbind(1) to connect signals with extra arguments to simple UI functions.
func connect_ui_effects(weapon_node: Node, crosshair_node: Node) -> void:
if weapon_node.has_signal(&"fired"):
weapon_node.connect(&"fired", crosshair_node.call.bind(&"expand").unbind(1))
# 10. Low-Latency Input Buffering
# EXPERT NOTE: Flush buffered events to ensure frame-perfect reaction to reload/fire.
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed(&"reload"):
Input.flush_buffered_events()
# Trigger reload logic
extends Node
class_name SoftLockAimAssist
## Expert Aim Assist (Godot 4.6).
## Uses Dot Product to find targets and Slerp for smooth tracking.
@export var assist_strength: float = 4.0
@export var threshold: float = 0.97 # Cone of influence
@onready var camera: Camera3D = get_viewport().get_camera_3d()
func _physics_process(delta: float) -> void:
var target = _find_best_target()
if target:
_apply_soft_lock(target, delta)
func _find_best_target() -> Node3D:
var best_target: Node3D = null
var best_dot: float = -1.0
var forward = -camera.global_transform.basis.z
for enemy in get_tree().get_nodes_in_group("enemies"):
var dir = camera.global_position.direction_to(enemy.global_position)
var dot = forward.dot(dir)
if dot > threshold and dot > best_dot:
best_dot = dot
best_target = enemy
return best_target
func _apply_soft_lock(target: Node3D, delta: float) -> void:
var target_basis = Basis.looking_at(target.global_position - camera.global_position)
var current_q = camera.global_transform.basis.get_rotation_quaternion()
var target_q = target_basis.get_rotation_quaternion()
# Expert Pattern: Spherical linear interpolation for smooth rotation
var result_q = current_q.slerp(target_q, assist_strength * delta)
camera.global_transform.basis = Basis(result_q)
## [SKILL NOTICE]: Use 'dot product' to filter proximity to screen center,
## and 'slerp' for smooth aim-assist pull. Avoid hard-snapping rotation.
extends SpringArm3D
class_name TPSCameraSpringArm
## Expert TPS Camera (Godot 4.6).
## Collision-aware SpringArm with dynamic shoulder swapping.
@export var shoulder_offset: float = 0.6
@onready var camera: Camera3D = get_child(0)
func _ready() -> void:
# Add the player to exclusion to prevent self-collision
add_excluded_object(get_parent().get_rid())
func swap_shoulder(to_right: bool) -> void:
var target = shoulder_offset if to_right else -shoulder_offset
# Expert Pattern: Use 'h_offset' to shift view without moving the collision ray
var tween = create_tween()
tween.tween_property(camera, "h_offset", target, 0.25).set_trans(Tween.TRANS_SINE)
## [SKILL NOTICE]: Use 'h_offset' on the Camera3D child of a SpringArm3D
## for shoulder swapping. This keeps the collision ray centered on the player.