
Godot Genre Shooter Fps
- 93 installs
- 454 repo stars
- Updated July 28, 2026
- thedivergentai/gd-agentic-skills
Helps with ai & agent building tasks during AI-assisted development.
About
godot-genre-shooter-fps is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- godot-genre-shooter-fps
- AI & Agent Building
- AI-coding skill
Godot Genre Shooter Fps by the numbers
- 93 all-time installs (skills.sh)
- +9 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #4,673 of 16,546 AI & Agent Building 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-shooter-fpsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 93 |
|---|---|
| repo stars | ★ 454 |
| Last updated | July 28, 2026 |
| Repository | thedivergentai/gd-agentic-skills ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
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
Area3Dormove_and_collide()for high-speed ballistics; strictly usePhysicsDirectSpaceState3D.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
AudioStreamPlayerfor gunfire; strictly use Layered Audio (Mechanical + Shot + Reverb Tail). - NEVER instantiate and
free()hundreds of projectile nodes; strictly use Object Pooling or theRenderingServer. - NEVER use
Sprite3DorQuadMeshfor 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
deltabeforemove_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 useis_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 - Professional-grade weapon system with deterministic recoil, bloom, and dual hitscan/projectile modes.
Modular Components
- fps_camera_look.gd - Asynchronous mouse look for zero-latency aiming using raw input.
- hitscan_weapon_query.gd - Nodeless physics raycast pattern for instant hit registration.
- fps_movement_logic.gd - Physics-based movement with acceleration, friction, and gravity scaling.
- weapon_bobbing_system.gd - Procedural bobbing and sway using sine-wave oscillation.
- bullet_decal_spawner.gd - Dynamic surface decal projection for impact effects.
- weapon_spread_calc.gd - Gaussian/Normal distribution logic for bullet clustering.
- server_projectile_instance.gd - High-volume visual bullets using RenderingServer RIDs.
- weapon_state_machine.gd - Optimized state transitions for fire, reload, and idle.
- player_anim_bridge.gd - Local velocity bridge for syncing movement with AnimationTree.
- 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
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 FPS Feel & Movement
Elite patterns for world-class weapon weight, stair navigation, and tactical positioning.
1. Viewmodel Sway (Procedural Weight)
To make weapons feel heavy and responsive, apply a rotational offset based on the mouse's last velocity. Use Input.get_last_mouse_velocity() for jitter-free data.
class_name WeaponSway extends Node3D
@export var sway_amount: float = 0.01
@export var max_sway: float = 0.05
@export var smooth_speed: float = 5.0
func _process(delta: float) -> void:
# Use 0.1s buffered mouse velocity for jitter-free sway
var mouse_vel := Input.get_last_mouse_velocity()
var target_pos := Vector3(
clamp(-mouse_vel.x * sway_amount, -max_sway, max_sway),
clamp(mouse_vel.y * sway_amount, -max_sway, max_sway),
0
)
# Smoothly lerp the viewmodel to the target offset
transform.origin = transform.origin.lerp(target_pos, delta * smooth_speed)2. Step-Up Logic (FPS Navigation)
Standard move_and_slide() can snag on small steps. Use a manual raycast check to detect steps and adjust the player's vertical position for smooth stair climbing.
class_name FPSStepController extends CharacterBody3D
@export var step_height: float = 0.4
@onready var space_state := get_world_3d().direct_space_state
func _physics_process(delta: float) -> void:
move_and_slide()
if is_on_wall():
_check_for_step()
func _check_for_step() -> void:
# Cast a ray forward and down from step_height
var forward_offset := velocity.normalized() * 0.5
var ray_start := global_position + Vector3(0, step_height, 0) + forward_offset
var ray_end := ray_start + Vector3.DOWN * step_height
var query := PhysicsRayQueryParameters3D.create(ray_start, ray_end)
query.exclude = [get_rid()]
var result := space_state.intersect_ray(query)
if result:
# Smoothly or instantly teleport to step top
global_position.y = result.position.y3. Tactical Lean System
Peeking around corners is achieved by rotating the camera around the Z-axis (roll) and offsetting the local X position. Use Input.get_axis() for smooth analog leaning.
class_name LeanSystem extends Camera3D
@export var lean_angle: float = 15.0 # Degrees
@export var lean_offset: float = 0.4 # Meters
@export var lean_speed: float = 8.0
func _process(delta: float) -> void:
var lean_input := Input.get_axis("lean_left", "lean_right")
var target_rot := deg_to_rad(-lean_input * lean_angle)
var target_pos := lean_input * lean_offset
rotation.z = lerp_angle(rotation.z, target_rot, delta * lean_speed)
transform.origin.x = lerp(transform.origin.x, target_pos, delta * lean_speed)Expert Tip: For the Lean system, parent the Camera3D to a "LeanPivot" node at the base of the player's neck to ensure the pivot point feels anatomically correct.
Reference
- Master Skill: godot-master
extends CharacterBody3D
class_name AdvancedFPSController
## Expert FPS Controller (Godot 4.6).
## 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.
# 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.
# 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)
# 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)
# 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()
# 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
extends Node3D
class_name HitscanWeaponLogic
## Expert Weapon Logic (Godot 4.6).
## 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.
# 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
# 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)
extends Node
class_name ProceduralRecoilHandler
## Expert Procedural Recoil (Godot 4.6).
## 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.
# 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)
# 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)
# 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
# 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.