references/fps-weapon-theory.md
# FPS Weapon Theory (load on demand)
> **MANDATORY** when implementing hitscan vs projectile tradeoffs, three-layer recoil, aim assist, net prediction, or weapon balance matrices beyond MANDATORY script reads.
## Hitscan vs projectile
| Mode | Use | Script |
|------|-----|--------|
| Hitscan | Pistols, ARs, snipers — instant feedback | [hitscan_weapon_query.gd](../scripts/hitscan_weapon_query.gd) |
| Projectile | Rockets, grenades — gravity arcs | [server_projectile_instance.gd](../scripts/server_projectile_instance.gd) (visual RID density) |
**WHY raycast not Area3D for bullets:** `PhysicsDirectSpaceState3D.intersect_ray` avoids per-bullet nodes at 60+ fire rate. Always `exclude` the shooter RID or shots hit the barrel instantly.
Muzzle forward: `-transform.basis.z` — not `Transform3D.looking_at()`.
## Three-layer recoil
1. **Visual kick** — camera pivot ([procedural_recoil_handler.gd](../scripts/procedural_recoil_handler.gd))
2. **Pattern offset** — learnable spray ([recoil_system.gd](../scripts/recoil_system.gd) `recoil_pattern` array)
3. **Spread bloom** — accuracy loss while firing; recover on release
**NEVER** apply recoil only to the gun mesh — players feel kick on the camera + bloom.
## Aim assist (controller)
[aim_assist.gd](../scripts/aim_assist.gd) — friction slowdown (≈0.3) within `assist_angle` + subtle screen-space magnetism (≈0.1). Disable or narrow on PC mouse builds.
## Weapon balance decision tree
| Archetype | Fire rate | Damage | Implementation |
|-----------|-----------|--------|----------------|
| SMG | High | Low | Tight vertical pattern |
| AR | Medium | Medium | Hitscan default |
| Shotgun | Burst | Per-pellet | 5–8 pellet spread, <10m effective |
| Sniper | Low | High | Hitscan + tracer visual |
| Rocket | Low | AoE | Projectile + gravity |
Sim asymmetry matrices in [godot-monte-carlo-balancer](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-monte-carlo-balancer/SKILL.md).
## Multiplayer client prediction
```
CLIENT: play VFX/audio/recoil immediately → rpc_id(1, server_validate_shot, transform)
SERVER: authoritative ray → rpc confirm_hit OR rpc_id(sender, client_cancel_cast)
```
Server wins on mismatch — show "no reg" feedback. Do not sync every bullet; send fire events only.
## Polish checklist
- **Audio:** mechanical + shot + delayed tail — never one `AudioStreamPlayer`
- **Impacts:** Decal pool + fade — [bullet_decal_spawner.gd](../scripts/bullet_decal_spawner.gd)
- **Camera:** FOV punch + short shake on fire
- **Viewmodel:** [weapon_bobbing_system.gd](../scripts/weapon_bobbing_system.gd) + `Input.get_last_mouse_velocity()` sway
## Common pitfalls
| Symptom | Fix |
|---------|-----|
| Weak impacts | Triple-layer audio + shake + decal + damage number |
| Guns feel same | Unique `recoil_pattern` per weapon |
| No skill ceiling | Learnable patterns, not pure RNG spread |
| Controller frustration | Aim assist friction + magnetism |
TPS/cover/soft-lock not FPS-rig-specific → [godot-genre-shooter](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-genre-shooter/SKILL.md).
references/migration-notes.md
# Migration notes: godot-genre-shooter-fps
Incremental upgrade for topics this skill covers. Apply **one hop**, stabilize/test, then next. Never skip hops.
If the project is **< 4.0**, follow [godot-version-migration](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-version-migration/SKILL.md) era bridges (legacy → 3→4) until 4.0, then these hops. Official 3→4: [Upgrading from Godot 3 to Godot 4](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.html).
## 4.0 → 4.1
Official: [Upgrading to Godot 4.1](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.1.html)
- `Area3D.priority` type is `int` (was `float`) — audit hitbox/trigger ordering.
- `PhysicsDirectSpaceState3D.collide_shape` returns `Array[Vector3]`.
- `Geometry3D.segment_intersects_convex` takes `Array[Plane]`.
- `MeshInstance3D.create_multiple_convex_collisions` optional `settings`.
- `Node3D.look_at` / `look_at_from_position` gain `use_model_front` — retune weapon/ADS aim helpers.
## 4.1 → 4.2
Official: [Upgrading to Godot 4.2](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.2.html)
- **Mesh format** upgrade — use Project → Tools → Upgrade Mesh Surfaces on weapon/environment meshes.
## 4.2 → 4.3
Official: [Upgrading to Godot 4.3](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.3.html)
- If the genre uses TileMap, migrate to `TileMapLayer` nodes before relying on layer APIs.
- If the genre ships multiplayer, upgrade all peers to 4.3 together (`SceneMultiplayer` protocol).
- `PhysicsShapeQueryParameters3D.motion` is `Vector3` (was `Vector2`) — fix hitscan/sweep queries.
- **Reverse Z** depth — update custom scope/post shaders and decal blood overlays.
- `Skeleton3D.add_bone` returns `int32`; pose update signal rename.
## 4.3 → 4.4
Official: [Upgrading to Godot 4.4](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.4.html)
- CSG uses Manifold — **non-manifold** meshes unsupported; use `MeshInstance3D` for blocking volumes and planes.
- `SoftBody3D.set_point_pinned` gains optional `insert_at`.
## 4.4 → 4.5
Official: [Upgrading to Godot 4.5](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.5.html)
- `Node.get_rpc_config` → `get_node_rpc_config` — audit weapon/fire RPC configs.
- Jolt: `physics/jolt_physics_3d/simulation/areas_detect_static_bodies` removed — Areas always report static overlaps; filter via layers/masks.
- GLTF/BLEND/FBX naming version for non-joint nodes in skeletons — set Import dock Naming Version for old weapon rigs.
## 4.5 → 4.6
Official: [Upgrading to Godot 4.6](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.6.html)
- Retune Environment glow/fog if the genre leans on bloom-heavy looks (muzzle flash, lens dirt).
- New projects default 3D physics engine to **Jolt** — existing projects keep prior setting; verify Jolt differences before shipping.
- `MeshInstance3D.skeleton` default empty; SpringBone enums moved to `SkeletonModifier3D`.
## 4.6 → 4.7
Official: [Upgrading to Godot 4.7](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.7.html)
- Confirm project stretch mode and `AudioStreamPlayer.area_mask` after opening in 4.7.
- `AudioStreamPlayer.area_mask` default is **0** — set mask for room/zone weapon reverb and footstep buses.
- Jolt: `WorldBoundaryShape3D.plane.d` sign convention flipped vs 4.6 — negate if kill planes moved.
- Jolt: `SoftBody3D` default mass is 1 kg for the body — retune gore/ragdoll stiffness/damping.
- Prefer **AreaLight3D** for soft rectangular muzzle/portal lights without GI hacks.
scripts/advanced_fps_controller.gd
extends CharacterBody3D
class_name AdvancedFPSController
## Expert FPS Controller (Godot 4.7).
## Smooth movement with ground/air-aware interpolation and head-bob.
@export var walk_speed: float = 8.0
@export var air_control: float = 0.15 # Air acceleration multiplier
@export var head_bob_freq: float = 2.4
@export var head_bob_amp: float = 0.08
@onready var camera: Camera3D = %Camera3D
var _walk_time: float = 0.0
func _physics_process(delta: float) -> void:
if not is_on_floor():
velocity.y -= 19.6 * delta # Gravity
var input_dir = Input.get_vector("move_left", "move_right", "move_fwd", "move_back")
var direction = (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()
# Expert Pattern: Higher lerp weight on ground for snappiness
var weight = 10.0 if is_on_floor() else 10.0 * air_control
velocity.x = lerp(velocity.x, direction.x * walk_speed, weight * delta)
velocity.z = lerp(velocity.z, direction.z * walk_speed, weight * delta)
move_and_slide()
_apply_head_bob(delta, direction)
func _apply_head_bob(delta: float, direction: Vector3) -> void:
if is_on_floor() and direction.length() > 0.1:
_walk_time += delta * velocity.length()
camera.transform.origin.y = sin(_walk_time * head_bob_freq) * head_bob_amp
else:
camera.transform.origin = camera.transform.origin.lerp(Vector3.ZERO, delta * 5.0)
## [SKILL NOTICE]: Use 'is_on_floor()' to switch between ground/air
## interpolation weights. This prevents 'floaty' movement on the ground.
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/classes/class_characterbody3d.html
# - https://docs.godotengine.org/en/stable/tutorials/physics/physics_introduction.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-input-handling/SKILL.md - move axes and capture before air/ground lerp
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-camera-systems/SKILL.md - head-bob camera local offset composition
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-genre-shooter-fps/SKILL.md
# =============================================================================
scripts/advanced_weapon_controller.gd
# godot-master/scripts/shooter_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.
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/tutorials/physics/ray-casting.html
# - https://docs.godotengine.org/en/stable/tutorials/scripting/resources.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-combat-system/SKILL.md - duck-typed take_damage after hitscan confirm
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-monte-carlo-balancer/SKILL.md - recoil/bloom/TTK bands before shipping stats
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-genre-shooter-fps/SKILL.md
# =============================================================================
scripts/aim_assist.gd
extends Node3D
class_name AimAssist
## Controller friction + magnetism near targets. Tune assist_angle to avoid mouse feel on PC builds.
@export var assist_range: float = 50.0
@export var assist_angle: float = 15.0
@export var friction_strength: float = 0.3
@export var magnetism_strength: float = 0.1
func apply_aim_assist(look_input: Vector2, camera: Camera3D) -> Vector2:
var target := _find_closest_target(camera)
if target == null:
return look_input
var to_target: Vector3 = 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
var friction := 1.0 - (friction_strength * (1.0 - angle / assist_angle))
look_input *= friction
var target_screen_pos := camera.unproject_position(target.global_position)
var screen_center := get_viewport().get_visible_rect().size * 0.5
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: Node3D in get_tree().get_nodes_in_group(&"enemies"):
var to_target: Vector3 = 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:
closest = target
closest_angle = angle
return closest
# ---
# GDSkills research links (agents)
# Docs:
# - https://docs.godotengine.org/en/stable/tutorials/inputs/controllers_gamepads_joysticks.html — analog look + deadzones
# Related:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-genre-shooter-fps/SKILL.md — friction 0.3 + magnetism 0.1 baseline
# ---
scripts/bullet_decal_spawner.gd
# bullet_decal_spawner.gd
extends Node
class_name BulletDecalSpawner
# Spawning Dynamic Bullet Decals
# Correctly projects textures across uneven surfaces using the Decal node.
@export var bullet_hole_texture: Texture2D
func spawn_decal(hit_position: Vector3, hit_normal: Vector3) -> void:
var decal := Decal.new()
decal.texture_albedo = bullet_hole_texture
decal.size = Vector3(0.1, 0.1, 0.1)
get_tree().root.add_child(decal)
decal.global_position = hit_position
# Pattern: Align decal to surface normal.
if hit_normal != Vector3.UP and hit_normal != Vector3.DOWN:
decal.look_at(hit_position + hit_normal, Vector3.UP)
elif hit_normal == Vector3.UP:
decal.rotation_degrees.x = 90
else:
decal.rotation_degrees.x = -90
# Optimization: Decals should have a lifespan.
var timer := get_tree().create_timer(10.0)
timer.timeout.connect(decal.queue_free)
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/tutorials/3d/using_decals.html
# - https://docs.godotengine.org/en/stable/classes/class_decal.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-performance-optimization/SKILL.md - decal fade/pool budgets under fire spam
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-genre-shooter/SKILL.md - shared impact polish patterns
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-genre-shooter-fps/SKILL.md
# =============================================================================
scripts/fps_camera_look.gd
# fps_camera_look.gd
extends Camera3D
class_name FPSCameraLook
# Asynchronous FPS Mouse Look
# Separates camera rotation from the physics tick to ensure zero-latency aiming.
@export var mouse_sensitivity := 0.002
var _rot_x := 0.0
var _rot_y := 0.0
func _unhandled_input(event: InputEvent) -> void:
# Pattern: Capture mouse motion independent of the physics frame.
if event is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
_rot_x -= event.relative.y * mouse_sensitivity
_rot_y -= event.relative.x * mouse_sensitivity
# Clamp pitch to prevent the camera from flipping.
_rot_x = clampf(_rot_x, -PI/2, PI/2)
# Pattern: Reset transform and apply local rotations to avoid precision loss.
transform.basis = Basis()
rotate_object_local(Vector3.UP, _rot_y)
rotate_object_local(Vector3.RIGHT, _rot_x)
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/tutorials/inputs/inputevent.html
# - https://docs.godotengine.org/en/stable/tutorials/inputs/mouse_and_input_coordinates.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-input-handling/SKILL.md - relative mouse and capture modes
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-camera-systems/SKILL.md - yaw/pitch pivot hierarchy
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-genre-shooter-fps/SKILL.md
# =============================================================================
scripts/fps_movement_logic.gd
# fps_movement_logic.gd
extends CharacterBody3D
class_name FPSMovementLogic
# Smooth FPS Movement with Acceleration & Friction
# Handles responsive WASD movement using vector interpolation.
@export var max_speed := 8.0
@export var accel := 10.0
@export var friction := 15.0
@export var gravity := 20.0
func _physics_process(delta: float) -> void:
var input_dir := Input.get_vector(&"move_left", &"move_right", &"move_forward", &"move_back")
# Convert input to direction relative to player rotation.
var direction := (global_transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()
if is_on_floor():
if direction != Vector3.ZERO:
velocity.x = move_toward(velocity.x, direction.x * max_speed, accel * delta)
velocity.z = move_toward(velocity.z, direction.z * max_speed, accel * delta)
else:
velocity.x = move_toward(velocity.x, 0.0, friction * delta)
velocity.z = move_toward(velocity.z, 0.0, friction * delta)
else:
# Gravity is an acceleration: Scale by delta.
velocity.y -= gravity * delta
# Pattern: Delta is applied inside move_and_slide() automatically.
move_and_slide()
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/classes/class_characterbody3d.html
# - https://docs.godotengine.org/en/stable/tutorials/3d/using_transforms.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-input-handling/SKILL.md - Input.get_vector move basis
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-project-foundations/SKILL.md - InputMap action names for walk/sprint
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-genre-shooter-fps/SKILL.md
# =============================================================================
scripts/frame_perfect_input.gd
# frame_perfect_input.gd
extends Node
class_name FramePerfectInput
# Frame-Perfect Input Interception
# Ensures semi-automatic inputs are never missed due to physics/render lag.
var _fire_requested := false
func _unhandled_input(event: InputEvent) -> void:
# Pattern: Intercept event immediately, then process in next physics tick.
if event.is_action_pressed(&"fire"):
_fire_requested = true
func _physics_process(_delta: float) -> void:
if _fire_requested:
_fire_requested = false
perform_shot()
func perform_shot() -> void:
# Trigger actual weapon logic here.
pass
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/tutorials/inputs/inputevent.html
# - https://docs.godotengine.org/en/stable/classes/class_inputeventmousemotion.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-input-handling/SKILL.md - buffered semi-auto fire to avoid dropped shots
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-signal-architecture/SKILL.md - fire intent signals without string connects
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-genre-shooter-fps/SKILL.md
# =============================================================================
scripts/hitscan_weapon_logic.gd
extends Node3D
class_name HitscanWeaponLogic
## Expert Weapon Logic (Godot 4.7).
## Decoupled hitscan logic with signal-based VFX triggering.
signal shot_fired(hit_point: Vector3, hit_normal: Vector3, collider: Object)
@export var range_m: float = 100.0
@export var damage: float = 25.0
func shoot() -> void:
var space = get_world_3d().direct_space_state
var cam = get_viewport().get_camera_3d()
var from = cam.global_position
var to = from + -cam.global_transform.basis.z * range_m
# Raycast query
var query = PhysicsRayQueryParameters3D.create(from, to)
var collision = space.intersect_ray(query)
if collision:
if collision.collider.has_method("take_damage"):
collision.collider.take_damage(damage)
shot_fired.emit(collision.position, collision.normal, collision.collider)
else:
shot_fired.emit(to, Vector3.ZERO, null)
## [SKILL NOTICE]: Use 'Signals' to trigger muzzle flashes and impacts.
## This keeps your mathematical hitscan logic separate from the visual effects.
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/tutorials/physics/ray-casting.html
# - https://docs.godotengine.org/en/stable/classes/class_physicsrayqueryparameters3d.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-raycasting-queries/SKILL.md - exclude player RID and collision masks
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-combat-system/SKILL.md - hit-zone damage multipliers
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-genre-shooter-fps/SKILL.md
# =============================================================================
scripts/hitscan_weapon_query.gd
# hitscan_weapon_query.gd
extends Node3D
class_name HitscanWeaponQuery
# High-Performance Hitscan Query (Nodeless)
# Fires a raycast instantly using the C++ physics server.
@export var damage := 25.0
@export var range := 1000.0
func fire_hitscan(camera: Camera3D, player_rid: RID) -> Dictionary:
var space_state := get_world_3d().direct_space_state
var viewport := get_viewport()
var center_screen := viewport.get_visible_rect().size / 2.0
var origin := camera.project_ray_origin(center_screen)
var end := origin + camera.project_ray_normal(center_screen) * range
var query := PhysicsRayQueryParameters3D.create(origin, end)
# NEVER shoot yourself: Exclude player RID.
query.exclude = [player_rid]
query.collide_with_areas = false
var result := space_state.intersect_ray(query)
if not result.is_empty():
var collider = result.get("collider")
# Duck-typing damage application for decoupled logic.
if collider.has_method(&"take_damage"):
collider.take_damage(damage)
return result
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/classes/class_physicsdirectspacestate3d.html
# - https://docs.godotengine.org/en/stable/tutorials/physics/ray-casting.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-raycasting-queries/SKILL.md - nodeless space-state hitscan
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-multiplayer-networking/SKILL.md - server re-run of the same query shape
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-genre-shooter-fps/SKILL.md
# =============================================================================
scripts/player_anim_bridge.gd
# player_anim_bridge.gd
extends Node
class_name PlayerAnimBridge
# Local Velocity for Animation BlendTrees
# Extracts local lateral movement for strafing blend states.
@export var player: CharacterBody3D
@export var anim_tree: AnimationTree
func _process(_delta: float) -> void:
if not player or not anim_tree: return
# Pattern: Inverse basis transform to get local-space velocity.
var local_vel := player.global_transform.basis.inverse() * player.velocity
var blend_pos := Vector2(local_vel.x, local_vel.z)
# Update BlendTree parameters.
anim_tree.set(&"parameters/movement/blend_position", blend_pos)
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/tutorials/animation/animation_tree.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-animation-tree-mastery/SKILL.md - velocity to blend parameters
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-state-machine-advanced/SKILL.md - StringName locomotion states
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-genre-shooter-fps/SKILL.md
# =============================================================================
scripts/procedural_recoil_handler.gd
extends Node
class_name ProceduralRecoilHandler
## Expert Procedural Recoil (Godot 4.7).
## Framerate-independent camera kick and exponential return.
@export var camera_pivot: Node3D
@export var return_speed: float = 7.0
@export var snap_speed: float = 20.0
var _target_rot: Vector3
var _current_rot: Vector3
func fire_recoil(kick: Vector2) -> void:
# Vector2(Pitch, Yaw)
_target_rot += Vector3(kick.x, kick.y, 0)
func _process(delta: float) -> void:
# Expert Pattern: Exponential smoothing for framerate independence
_target_rot = _target_rot.lerp(Vector3.ZERO, return_speed * delta)
_current_rot = _current_rot.lerp(_target_rot, snap_speed * delta)
camera_pivot.rotation = _current_rot
## [SKILL NOTICE]: Use exponential 'lerp' for recoil return to ensure the
## camera animation feels consistent at 30, 60, or 144 FPS.
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/classes/class_camera3d.html
# - https://docs.godotengine.org/en/stable/tutorials/3d/using_transforms.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-camera-systems/SKILL.md - kick on pivot, not weapon mesh
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-monte-carlo-balancer/SKILL.md - recoil pattern vs TTK feel bands
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-genre-shooter-fps/SKILL.md
# =============================================================================
scripts/recoil_system.gd
extends Node
class_name RecoilSystem
## Three-layer recoil: visual kick, learnable pattern offset, spread bloom. Recover on _process with lerp.
var visual_recoil: Vector2 = Vector2.ZERO
var pattern_offset: Vector2 = Vector2.ZERO
var spread_bloom: float = 0.0
@export var recoil_pattern: Array[Vector2] = []
var pattern_index: int = 0
func apply_recoil(base_recoil: Vector2, max_spread: float) -> void:
visual_recoil.y += base_recoil.y * randf_range(0.8, 1.2)
visual_recoil.x += base_recoil.x * randf_range(-1.0, 1.0)
if pattern_index < recoil_pattern.size():
pattern_offset += recoil_pattern[pattern_index]
pattern_index += 1
spread_bloom = minf(spread_bloom + 0.5, 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 = lerpf(spread_bloom, 0.0, recovery_speed * delta)
if visual_recoil.length() < 0.01:
pattern_index = 0
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)
# ---
# GDSkills research links (agents)
# Related:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-genre-shooter-fps/scripts/procedural_recoil_handler.gd — camera pivot kick
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-monte-carlo-balancer/SKILL.md — spray pattern vs TTK bands
# ---
scripts/server_projectile_instance.gd
# server_projectile_instance.gd
extends Node
class_name ServerProjectileInstance
# Nodeless Projectile Spawning via Servers
# Direct RenderingServer calls for high-volume minigun bullets.
var _bullet_rid: RID
func spawn_visual_bullet(mesh_rid: RID, xform: Transform3D) -> void:
# Pattern: Create instance directly in visual server to bypass SceneTree overhead.
_bullet_rid = RenderingServer.instance_create()
RenderingServer.instance_set_base(_bullet_rid, mesh_rid)
RenderingServer.instance_set_scenario(_bullet_rid, get_world_3d().scenario)
RenderingServer.instance_set_transform(_bullet_rid, xform)
func update_visual(xform: Transform3D) -> void:
if _bullet_rid.is_valid():
RenderingServer.instance_set_transform(_bullet_rid, xform)
func _exit_tree() -> void:
if _bullet_rid.is_valid():
RenderingServer.free_rid(_bullet_rid)
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/tutorials/performance/using_servers.html
# - https://docs.godotengine.org/en/stable/classes/class_renderingserver.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-performance-optimization/SKILL.md - RID visual bullets without nodes
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-multiplayer-networking/SKILL.md - server-authoritative spawn, client visuals only
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-genre-shooter-fps/SKILL.md
# =============================================================================
scripts/weapon_bobbing_system.gd
# weapon_bobbing_system.gd
extends Node3D
class_name WeaponBobbingSystem
# Procedural Weapon Bobbing
# Simulates the realistic movement of holding a gun using sine waves.
@export var bob_frequency := 2.0
@export var bob_amplitude := 0.05
var _time_passed := 0.0
func apply_bobbing(delta: float, is_moving: bool) -> void:
if is_moving:
_time_passed += delta
# Pattern: Procedural offsets for X (sway) and Y (bob).
var offset_y := sin(_time_passed * bob_frequency * 2.0) * bob_amplitude
var offset_x := cos(_time_passed * bob_frequency) * bob_amplitude
transform.origin = Vector3(offset_x, offset_y, 0.0)
else:
# Smoothly return to center when idle.
transform.origin = transform.origin.lerp(Vector3.ZERO, delta * 5.0)
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/tutorials/3d/using_transforms.html
# - https://docs.godotengine.org/en/stable/classes/class_camera3d.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-camera-systems/SKILL.md - sway/bob layered with look and recoil
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-tweening/SKILL.md - optional tween assists for viewmodel settle
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-genre-shooter-fps/SKILL.md
# =============================================================================
scripts/weapon_spread_calc.gd
# weapon_spread_calc.gd
extends RefCounted
class_name WeaponSpreadCalc
# Normal Distribution Bullet Spread
# Bullets cluster near the crosshair using Gaussian distribution.
static func calculate_spread(forward: Vector3, spread_degrees: float) -> Vector3:
# Pattern: randfn() clusters around 0.0 for more realistic clustering.
var dev_x := deg_to_rad(randfn(0.0, spread_degrees))
var dev_y := deg_to_rad(randfn(0.0, spread_degrees))
var spread_basis := Basis()
spread_basis = spread_basis.rotated(Vector3.UP, dev_x)
spread_basis = spread_basis.rotated(Vector3.RIGHT, dev_y)
return spread_basis * forward
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/tutorials/3d/using_transforms.html
# - https://docs.godotengine.org/en/stable/tutorials/scripting/resources.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-monte-carlo-balancer/SKILL.md - bloom vs accuracy win-rate bands
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-combat-system/SKILL.md - spread direction into damage queries
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-genre-shooter-fps/SKILL.md
# =============================================================================
scripts/weapon_state_machine.gd
# weapon_state_machine.gd
extends Node
class_name WeaponStateMachine
# StringName-Optimized Weapon State Machine
# High-performance state transitions for fast firing loops.
# Pattern: Use &StringNames for pointer-level hash comparisons.
var current_state: StringName = &"idle"
func _physics_process(delta: float) -> void:
match current_state:
&"idle":
if Input.is_action_pressed(&"fire"):
transition_to(&"firing")
&"firing":
if not Input.is_action_pressed(&"fire"):
transition_to(&"idle")
&"reloading":
pass
func transition_to(new_state: StringName) -> void:
current_state = new_state
# Handle entry/exit logic here.
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/tutorials/animation/animation_tree.html
# - https://docs.godotengine.org/en/stable/tutorials/scripting/resources.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-state-machine-advanced/SKILL.md - fire/reload/idle StringName transitions
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-signal-architecture/SKILL.md - fired/reloaded signal-object connects
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-genre-shooter-fps/SKILL.md
# =============================================================================
SKILL.md
---
name: godot-genre-shooter-fps
description: "Expert blueprint for First-Person Shooters: fps_camera_look, fps_movement_logic, hitscan_weapon_query, weapon_bobbing_system, procedural recoil, and FPS NEVER rules. Shared TPS/cover theory links to godot-genre-shooter. Keywords: FPS, movement physics, weapon bobbing, camera sway, hitscan, viewmodel, air control."
---
## MANDATORY script reads (start here)
1. [fps_camera_look.gd](scripts/fps_camera_look.gd) — raw `_input` mouse look (yaw/pitch)
2. [fps_movement_logic.gd](scripts/fps_movement_logic.gd) — accel/friction/air control
3. [hitscan_weapon_query.gd](scripts/hitscan_weapon_query.gd) — space-state hitscan
4. [weapon_bobbing_system.gd](scripts/weapon_bobbing_system.gd) — viewmodel bob/sway
Also: [procedural_recoil_handler.gd](scripts/procedural_recoil_handler.gd), [hitscan_weapon_logic.gd](scripts/hitscan_weapon_logic.gd), [advanced_fps_controller.gd](scripts/advanced_fps_controller.gd), [weapon_state_machine.gd](scripts/weapon_state_machine.gd), [frame_perfect_input.gd](scripts/frame_perfect_input.gd), [bullet_decal_spawner.gd](scripts/bullet_decal_spawner.gd), [weapon_spread_calc.gd](scripts/weapon_spread_calc.gd), [server_projectile_instance.gd](scripts/server_projectile_instance.gd), [player_anim_bridge.gd](scripts/player_anim_bridge.gd), [recoil_system.gd](scripts/recoil_system.gd) (pattern + bloom), [aim_assist.gd](scripts/aim_assist.gd) (controller friction/magnetism).
`advanced_weapon_controller.gd` is shared theory — keep FPS feel in the scripts above; genre routing for TPS → sibling skill.
## Core Loop
`Move/Look → Aim → Fire (hitscan) → Recoil recover → Acquire`
## NEVER Do (FPS)
### Gunplay & registration
- **NEVER** hit-detect in `_process` — use `_physics_process` + `intersect_ray`.
- **NEVER** apply recoil only to the gun mesh — kick **camera** + bloom/spread.
- **NEVER** trust client damage — server authority + lag compensation.
- **NEVER** forget `exclude` / `add_exception` for the player RID.
- **NEVER** use `==` on float cooldowns — `is_equal_approx`.
### Input & movement
- **NEVER** poll mouse in `_physics_process` — `_input` / `_unhandled_input` for look.
- **NEVER** accumulate look on a raw `Transform3D` — store yaw/pitch floats.
- **NEVER** multiply velocity by `delta` before `move_and_slide()`.
- **NEVER** use `Transform3D.looking_at` for muzzle forward — use `-transform.basis.z`.
### Polish
- **NEVER** single `AudioStreamPlayer` for gunfire — layer mechanical + shot + tail.
- **NEVER** leave decals forever — fade/pool ([bullet_decal_spawner.gd](scripts/bullet_decal_spawner.gd)).
- **NEVER** hardcode weapon stats — WeaponData Resources.
- **NEVER** use plain Strings for hot weapon states — `StringName`.
## Advanced FPS (keep)
### Viewmodel sway / bob
Procedural sine bob + look sway — [weapon_bobbing_system.gd](scripts/weapon_bobbing_system.gd).
### Step-up
Ray forward/down from `step_height` when `is_on_wall()` after `move_and_slide()`; lift to step top for stairs.
### Tactical lean
Roll camera Z + local X offset from `Input.get_axis("lean_left","lean_right")` with lerp.
## Shared weapon theory
Hitscan vs projectile, spray patterns, and net prediction details that are not FPS-rig specific → [godot-genre-shooter](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-genre-shooter/SKILL.md) + combat/multiplayer complements. Implement FPS fire path via [hitscan_weapon_query.gd](scripts/hitscan_weapon_query.gd) + [procedural_recoil_handler.gd](scripts/procedural_recoil_handler.gd) + [recoil_system.gd](scripts/recoil_system.gd).
> **MANDATORY** for hitscan/projectile tradeoffs, three-layer recoil, aim assist, weapon balance matrices, and client prediction: [fps-weapon-theory.md](references/fps-weapon-theory.md). **Do NOT Load** after MANDATORY script reads unless extending beyond bob/step/lean.
## Reference
> Progressive disclosure: open Official Documentation links only when researching a specific API; load Related Skills when routing to a peer domain — do not preload the whole lattice.
### Official Documentation
- [Ray-casting](https://docs.godotengine.org/en/stable/tutorials/physics/ray-casting.html) — PhysicsDirectSpaceState3D `intersect_ray` patterns for hitscan registration without Area3D ballistics.
- [Physics introduction](https://docs.godotengine.org/en/stable/tutorials/physics/physics_introduction.html) — collision layers/masks, layers for player exclude, and physics tick vs visual frame for gunplay.
- [Using 3D transforms](https://docs.godotengine.org/en/stable/tutorials/3d/using_transforms.html) — basis axes and `-transform.basis.z` forward vectors for aim, recoil, and muzzle direction.
- [Using InputEvent](https://docs.godotengine.org/en/stable/tutorials/inputs/inputevent.html) — `_input` vs `_physics_process` so mouse look stays zero-latency while movement stays physics-synced.
- [Mouse and input coordinates](https://docs.godotengine.org/en/stable/tutorials/inputs/mouse_and_input_coordinates.html) — relative mouse motion, capture, and screen-center projection for aim rays.
- [Controllers, gamepads, and joysticks](https://docs.godotengine.org/en/stable/tutorials/inputs/controllers_gamepads_joysticks.html) — analog look axes and deadzones that aim-assist friction/magnetism build on.
- [Using decals](https://docs.godotengine.org/en/stable/tutorials/3d/using_decals.html) — Decal node projection for bullet impacts instead of Sprite3D/QuadMesh stickers.
- [High-level multiplayer](https://docs.godotengine.org/en/stable/tutorials/networking/high_level_multiplayer.html) — RPC authority and fire-event patterns for server-validated hitscan without syncing every bullet.
- [Using AnimationTree](https://docs.godotengine.org/en/stable/tutorials/animation/animation_tree.html) — blend/state machines for idle/aim/fire/reload without `!` in expressions.
- [Resources](https://docs.godotengine.org/en/stable/tutorials/scripting/resources.html) — WeaponData Resource stats (damage, recoil, spread) instead of hardcoded class constants.
- [Audio streams](https://docs.godotengine.org/en/stable/tutorials/audio/audio_streams.html) — layered mechanical/shot/tail streams for gunfire weight.
- [Optimization using Servers](https://docs.godotengine.org/en/stable/tutorials/performance/using_servers.html) — RenderingServer RID paths for high-volume visual projectiles without SceneTree spam.
### Related Skills
#### Prerequisites
- [godot-project-foundations](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-project-foundations/SKILL.md) — scene tree, InputMap actions, and Resource import basics before FPS controllers and WeaponData wiring.
- [godot-input-handling](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-input-handling/SKILL.md) — mouse capture, action buffering, and look vs move split that frame-perfect fire and camera look depend on.
- [godot-raycasting-queries](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-raycasting-queries/SKILL.md) — physics-synced ray/shape queries and exclude RIDs that hitscan registration builds on.
#### Complements
- [godot-camera-systems](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-camera-systems/SKILL.md) — FOV punch, lean pivots, and shake stacks that compose with procedural recoil and weapon bob.
- [godot-combat-system](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-combat-system/SKILL.md) — duck-typed damage application and hit-zone multipliers shared with hitscan/projectile confirm paths.
- [godot-audio-systems](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-audio-systems/SKILL.md) — bus routing and pooled one-shots for layered gunfire without a single shared AudioStreamPlayer.
- [godot-multiplayer-networking](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-multiplayer-networking/SKILL.md) — authoritative fire RPCs and unreliable movement transfer modes for competitive hit validation.
- [godot-adapt-single-to-multiplayer](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-adapt-single-to-multiplayer/SKILL.md) — client prediction / server reconciliation shells before lag-compensated shot validation.
- [godot-state-machine-advanced](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-state-machine-advanced/SKILL.md) — StringName fire/reload/idle machines without stringly-typed weapon states.
- [godot-animation-tree-mastery](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-animation-tree-mastery/SKILL.md) — AnimationTree parameters and blend spaces driven by local velocity bridges.
- [godot-performance-optimization](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-performance-optimization/SKILL.md) — pooling, Decal budgets, and RenderingServer RID density for bullet visuals and impacts.
- [godot-genre-shooter](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-genre-shooter/SKILL.md) — broader TPS/hybrid shooter architecture when the project is not FPS-first.
#### Downstream / consumers
- [godot-monte-carlo-balancer](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-monte-carlo-balancer/SKILL.md) — simulate weapon asymmetry matrices, TTK bands, and bloom/recoil knobs before shipping balance tables.
- [godot-genre-battle-royale](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-genre-battle-royale/SKILL.md) — large-scale relevancy and lag-compensated combat that reuses FPS hitscan/recoil feel inside BR matches.
#### Master
- [godot-master](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-master/SKILL.md) — library router and mirrored module entry; open when discovering which Domain Skill owns a cross-cutting FPS concern.