Part of the
make-game skill. The skill links here when your agent needs the detail.3d-third-person.md: melee, bullets,
grenade with arc preview, two enemy archetypes, coins, destructible boxes,
and the HUD.
Shared damage interface
Everything hittable (player, enemies, boxes) implements:impact_point is
global_position - body.global_position (direction from body to impact),
force is the knockback vector. Enemies additionally join “targeteables”
for grenade lock-on. One interface, three weapons, all content - keep it.
Weapons
Tab toggles DEFAULT and GRENADE (aweapon_switched signal drives the HUD
icons). DEFAULT: left-click = melee from the hip, aim (RMB) + left-click =
ranged shot with over-shoulder camera + crosshair. GRENADE: left-click
throws with a live arc preview.
Melee (animation-gated hitbox)
The MeleeAttackArea’s CollisionShape3D starts disabled. The “Attack” animation (0.3-0.5s) has method-call tracks:activate() at the impact
frame (~0.15s), deactivate() at the end. The area’s body_entered damages
“damageables” with force pointing away from the player. Player side:
attack() plays the animation, fires the skin’s punch OneShot, and sets
velocity = rotation_root.basis * Vector3.BACK * attack_impulse (forward
lunge). Movement is locked while the attack animation plays.
Animation-gated hitboxes beat timer-gated ones: the active window is
authored visually next to the swing frames.
Bullet (shared by player AND enemies)
Node3D moved manually in_process - no physics body:
shooter reference prevents self-hits, which is what lets one bullet
scene serve both sides. Distance limit expressed as time
(distance / speed); visual scale decays via an exported Curve so bullets
fade with range. Player shots originate at global_position + Vector3.UP
and fly toward camera_controller.get_aim_target().
Fast projectiles tunnel through thin walls: keep speeds moderate (player
10, enemies 6) or fatten the Area3D shape.
Grenade launcher (arc preview + lock-on)
While GRENADE is equipped the launcher is visible and every physics frame: camera tilt lerps throw distance between min 7 and max 16; a ShapeCast3D finds the landing point; if the cast hits a “targeteables” body, the landing snaps to that enemy (lock-on, SnapMesh indicator); initial velocity decomposes into verticalsqrt(2 * gravity * peak_height) and horizontal
distance / time_to_land; the preview trail is a triangle-strip mesh built
with SurfaceTool sampling the parabola at 0.05s steps.
The grenade itself is a CharacterBody3D using move_and_collide (NOT
move_and_slide) so it can bounce:
not body is Player -
self-immunity), spawn explosion visuals, hide, await the sound, free. On
throw, add a collision exception between grenade and thrower via
PhysicsServer3D.body_add_collision_exception.
Enemies
Both are RigidBody3D with a PlayerDetectionArea (big sphere Area3D), a reaction label (”!” / ”?” animations on detect/lose), and the same death choreography: play defeat sound, set_alive = false, DISCONNECT the
detection signals (dead enemies must not react), enable the death collider,
enable gravity so the body drops, wait 2s, spawn a smoke puff, await puff.full, burst coins out of the smoke, queue_free().
The smoke puff emits full from an animation keyframe at peak opacity -
awaiting it makes coins appear to burst out of the cloud instead of
clipping through a half-formed puff.
Bee bot (flying turret)
RigidBody3D withgravity_scale = 0 (floats). While a target is set:
target.global_position + Vector3.UP. damage() applies an impulse
(clamped length 3) then runs the death choreography; gravity_scale flips to
1.0 so it falls out of the air.
Beetle bot (NavigationAgent3D chaser)
RigidBody3D with rotation axes locked (rigid until death). Chase loop:look_at the target with Y flattened to its own height. On death, unlock
all angular axes so the corpse tumbles.
Navigation requirements: the level MUST have a baked NavigationRegion3D or
the beetle stands still forever; the target must be reachable on the
navmesh; always zero the Y of path directions.
Enemy skins: AnimationTree state machines (Idle/Walk/Attack/Shake/PowerOff)
with a secondary-action timer firing random idle shakes every 3-8s;
power_off() stops that timer.
Coins
RigidBody3D collectible, either placed in the level or scattered byspawn() (random impulse: direction rotated randomly around UP, range 2-4,
height 1-3). Collection is DELAYED: layer 3 collision is off for
coin_delay (0.5s default, 1.5s when the player drops them) so coins from
a kill do not vacuum back instantly.
Magnet: when the player enters the coin’s detection area (mask = Player
layer 4), freeze physics and tween to the player:
_follow lerps from the captured start to the LIVE player position, so the
coin homes even while the player runs. Collect: random-pitch sound,
player.collect_coin(), hide, await audio, free. Coin model: spin
rotation.y += 1.5 * delta, bob sin(t) * 0.04.
Destructible boxes
damage() on the box: spawn 5 coins with spawn(), instantiate a
pre-fractured debris scene, disable own collision (deferred), play sound,
await it, free. Debris scene = 6 hidden frozen RigidBody3D pieces; on ready,
shuffle and launch 3 random ones with apply_force(rand_vector * 500),
leave the rest hidden. Half-count random debris looks as good as full
simulation at a fraction of the cost.
HUD
update_coins_amount, restarts
its display timer per change, slides back out on timeout. Weapon icons:
inactive at alpha 0.2, tween to full white on select; switch_to() guards
against reselecting the current one. Use unique names (%) so the deep
player script reaches UI without brittle paths.
Order of work
- Melee: attack animation + gated hitbox + a destructible box to punch.
- Coins with magnet + HUD counter (the reward loop).
- Aim mode: over-shoulder pivot + crosshair + bullet.
- Bee bot (reuses your bullet), then beetle bot (navmesh required).
- Grenade launcher last - arc preview, lock-on, explosion.
Source:
library/skills/make-game/references/3d-third-person-systems.md, rendered from summer-engine@4.3.0 on npm.
