Skip to main content
Part of the make-game skill. The skill links here when your agent needs the detail.
The combat and interaction layer on top of 3d-fps.md: hitscan and projectile shooting, the data-driven weapon system, raycast interaction, and the HUD.

Shooting: hitscan vs projectile

Hitscan (raycast) for rifles/pistols, projectiles for rockets/grenades. Most weapons should be hitscan - instant, cheap, reliable.

Hitscan

Uses the player’s ShootRay (RayCast3D under Camera3D, target (0,0,-100), mask 6 = Environment + Enemy):
force_raycast_update() matters: the ray otherwise reports last frame’s state and fast turns produce misses. Cooldown ticks down in _process and re-arms can_shoot.

Projectile

CharacterBody3D moved with move_and_collide (not RigidBody - you want deterministic flight):
Spawn from the camera, not the weapon model, so shots go where the crosshair points:

Weapon system (data-driven)

WeaponData is a Resource; one .tres (or in-code instance) per weapon:
WeaponManager (Node3D child of Player) owns weapons: Array[WeaponData], current index, magazine/reserve ammo, and these signals for the HUD: weapon_changed, ammo_changed(current, reserve), reload_started(time), reload_finished. Core rules learned the hard way:
  • Automatic vs semi-auto is one branch: automatic polls Input.is_action_pressed("shoot"), semi-auto uses is_action_just_pressed.
  • Fire path: refuse if not can_shoot or is_reloading; if magazine empty, auto-start reload instead of firing; decrement ammo and emit ammo_changed BEFORE resolving the hit.
  • Spread: randomize shoot_ray.target_position x/y within +-spread before the raycast, then RESET it to (0,0,-100) after - a forgotten reset makes every later weapon inherit the spread.
  • Switching: clear WeaponHolder children (queue_free), instantiate the new weapon_model_scene, reset is_reloading/can_shoot/shoot_timer. Guard against re-equipping the same index.
  • Reload:
The is_reloading guard is not optional - without it players stack reload timers. Partial reloads take mini(needed, reserve), never a full magazine. Weapon switching by number keys: loop weapons.size() checking Input.is_action_just_pressed("weapon_%d" % (i + 1)).

Interaction system

Duck-typed interface - any node is interactable if it implements:
The player polls its InteractionRay every physics frame and caches look_target. Check the collider AND its parent for the methods (the collider is often a StaticBody3D child of the actual object):

Door pattern

Node3D root that rotates; mesh + StaticBody3D child positioned at (0.5, 1.4, 0) so the hinge is the parent origin, not the door center. _process lerps rotation.y toward target_rotation. Open away from the player by checking which side they are on:
Prompt reflects state: “Locked” / “Open” / “Close”. The door body’s collision_layer is 6 (Environment + Interactable) so it both blocks movement and answers the interaction ray.

Pickup pattern

RigidBody3D with freeze = true (physics off, still hit by rays), idle bob (base_y + sin(bob_time) * 0.05) and slow rotate, randomized starting phase (bob_time = randf() * TAU) so a row of pickups does not bob in lockstep. interact() applies the effect and queue_free()s.

HUD

Wiring pattern: the HUD finds the player via the “player” group in _process (retry until found - spawn order is not guaranteed), then connects weapon_manager.ammo_changed. Interaction prompt shows "[E] " + player.look_target.get_interaction_prompt() while look_target is set, hides otherwise. Damage feedback: on hit, set the overlay to Color(1, 0, 0, 0.3) and fade alpha down at 2.0/s in _process. Cheap and effective.

Order of work

  1. Hitscan fire on the base controller (fixed 25 damage, no weapon data).
  2. Hit effects + damage overlay - shooting must feel real before it is configurable.
  3. WeaponData + WeaponManager, migrate the hardcoded fire into it, add ammo/reload + HUD ammo label.
  4. Second and third weapons (one automatic with spread, one projectile) to prove the data path.
  5. Interaction ray + door + pickup + prompt label.

Source: library/skills/make-game/references/3d-fps-systems.md, rendered from summer-engine@4.3.0 on npm.