Claude
Skills
Sign in
Back

godot-genre-shooter

Included with Lifetime
$97 forever

Expert blueprint for FPS/TPS shooter games (Call of Duty, Counter-Strike, Apex Legends, Fortnite) covering weapon systems, recoil patterns, hitscan vs projectile, aim assist, multiplayer prediction, and gunplay feel. Use when building competitive shooters, battle royales, or tactical FPS games requiring responsive combat. Keywords hitscan, recoil pattern, aim assist, client prediction, weapon archetype, projectile physics, hit registration.

Generalscripts

What this skill does


# Genre: Shooter (FPS/TPS)

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

## Available Scripts

### [advanced_weapon_controller.gd](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 `Area3D` overlap 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 `AudioStreamPlayer` for 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 the `PhysicsServer3D` API for stability.
- NEVER use `Sprite3D` for 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 `CollisionShape3D` non-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](scripts/advanced_weapon_controller.gd) - Professional-grade weapon system with deterministic recoil, bloom, and dual firing modes.

### Modular Components
- [shooter_patterns.gd](scripts/shooter_patterns.gd) - Reusable patterns: Server-bypassing hitscan, random spread, and ShapeCast3D explosions.

---

## 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

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)

```gdscript
class_name AimAssist
extends Node3D

@export var assist_range := 50.0
@export var assist_angle := 15.0

Related in General