Claude
Skills
Sign in
โ† Back

godot-genre-shooter-fps

Included with Lifetime
$97 forever

Expert blueprint for First-Person Shooters (Doom, Quake, Battlefield, Overwatch) focusing on physics-based movement, acceleration/friction, camera sway, weapon bobbing, and high-precision hit registration. Use when building tight, responsive FPS combat with advanced camera mechanics. Keywords FPS, movement physics, weapon bobbing, camera sway, hitscan, ground detection, air control.

Generalscripts

What this skill does


# Genre: Shooter (FPS/TPS)

Gunplay feel, responsive combat, and competitive balance define shooters.

## NEVER Do (Expert Anti-Patterns)

### Gunplay & Hit Registration
- NEVER use `_process()` for hit detection; strictly use `_physics_process()` to maintain frame-rate independent accuracy.
- NEVER apply recoil to the physical weapon model; strictly apply it to **Camera Rotation (kick)** and **Weapon Bloom (spread)**.
- NEVER trust the client for hit registration in multiplayer; strictly use **Server-Authoritative** validation with lag compensation.
- NEVER synchronize every bullet over the network; strictly use **Client-Side Prediction** and send only initial "Fire" events.
- NEVER use `Area3D` or `move_and_collide()` for high-speed ballistics; strictly use `PhysicsDirectSpaceState3D.intersect_ray()` for 100x better performance.
- NEVER forget to exclude the player's own RID from hitscan raycasts; otherwise, shots will collide instantly with the barrel.
- NEVER use exact floating-point equality (==) for weapon cooldowns or timers; strictly use `is_equal_approx()`.

### Performance & Polish
- NEVER use a single `AudioStreamPlayer` for gunfire; strictly use **Layered Audio** (Mechanical + Shot + Reverb Tail).
- NEVER instantiate and `free()` hundreds of projectile nodes; strictly use **Object Pooling** or the `RenderingServer`.
- NEVER use `Sprite3D` or `QuadMesh` for bullet impacts; strictly use the **Decal** node for surface-conforming texture projection.
- NEVER leave decals in the scene indefinitely; strictly implement a fade-out and cleanup cycle.
- NEVER use `Transform3D.looking_at()` for forward shooting vectors; strictly extract the direction from `-transform.basis.z`.
- NEVER multiply velocity by `delta` before `move_and_slide()`; the method internalizes the timestep automatically.

### Input & Architecture
- NEVER poll mouse motion inside `_physics_process()`; strictly use `_input()` for zero-latency camera look.
- NEVER accumulate mouse rotation directly onto a `Transform3D`; strictly store **Yaw/Pitch variables** to avoid gimbal lock.
- NEVER hardcode weapon statistics (Damage, Recoil) inside logic; strictly use **Resource-based WeaponData** for balancing.
- NEVER tightly couple damage logic to specific classes; strictly use **Duck-Typing** (`has_method("take_damage")`) for environment interactivity.
- NEVER use standard Strings for high-frequency state identifiers; strictly use `StringName` (e.g., `&"reloading"`).
- NEVER use the `!` (NOT) operator in AnimationTree expressions; strictly use `is_firing == false`.
- NEVER connect weapon signals via string-based calls; strictly use **Signal-Object syntax** (`fired.connect`).

---

## ๐Ÿ›  Expert Components (scripts/)

### Original Expert Patterns
- [advanced_weapon_controller.gd](scripts/advanced_weapon_controller.gd) - Professional-grade weapon system with deterministic recoil, bloom, and dual hitscan/projectile modes.

### Modular Components
- [fps_camera_look.gd](scripts/fps_camera_look.gd) - Asynchronous mouse look for zero-latency aiming using raw input.
- [hitscan_weapon_query.gd](scripts/hitscan_weapon_query.gd) - Nodeless physics raycast pattern for instant hit registration.
- [fps_movement_logic.gd](scripts/fps_movement_logic.gd) - Physics-based movement with acceleration, friction, and gravity scaling.
- [weapon_bobbing_system.gd](scripts/weapon_bobbing_system.gd) - Procedural bobbing and sway using sine-wave oscillation.
- [bullet_decal_spawner.gd](scripts/bullet_decal_spawner.gd) - Dynamic surface decal projection for impact effects.
- [weapon_spread_calc.gd](scripts/weapon_spread_calc.gd) - Gaussian/Normal distribution logic for bullet clustering.
- [server_projectile_instance.gd](scripts/server_projectile_instance.gd) - High-volume visual bullets using RenderingServer RIDs.
- [weapon_state_machine.gd](scripts/weapon_state_machine.gd) - Optimized state transitions for fire, reload, and idle.
- [player_anim_bridge.gd](scripts/player_anim_bridge.gd) - Local velocity bridge for syncing movement with AnimationTree.
- [frame_perfect_input.gd](scripts/frame_perfect_input.gd) - Buffered semi-automatic input handling to prevent dropped shots.

---

## Core Loop

`Engage โ†’ Aim โ†’ Fire โ†’ Kill Confirm โ†’ Acquire Next`

---

## Weapon System Architecture

```gdscript
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)

```gdscript
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)

```gdscript
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:

```gdscript
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

fun

Related in General