Part of the
make-game skill. The skill links here when your agent needs the detail.3d-third-person-systems.md.
Minimum playable base (build this first)
Level (CSG floor + light + WorldEnvironment) + player capsule with camera-relative WASD, orbit camera on a SpringArm3D, and jump. Prove “run around, camera orbits, no wall clipping” before combat exists.Player scene tree
damage()), “targeteables” (grenade lock-on).
Input: move_up/down/left/right, jump (Space, hold for air boost), attack
(LMB / RT), aim (RMB / LT), swap_weapons (Tab), pause, camera_left/right/up/
down (QERF keyboard fallback + right stick). The player script can
auto-register missing actions at runtime via InputMap.add_action so the
scene works when dropped into a bare project.
Controller: the decisions that matter
Tuning baseline: move_speed 8.0, acceleration 4.0, rotation_speed 12.0, jump_initial_impulse 12.0, jump_additional_force 4.5 (held-jump air boost), attack_impulse 10.0, gravity -30.0 (deliberately NOT project default; project gravity 16 is too floaty for 3D platforming - tune independently). Camera-relative input with diagonal correction:CharacterRotationRoot (never the body) toward that
direction at rotation_speed, preserving model scale.
Velocity: split Y off before lerping so gravity is never interpolated:
jump_additional_force * delta while jump is
held and still rising (variable height). Track _is_on_floor_buffer to
detect the landing frame for the landing sound.
Unstick hack after move_and_slide(): if velocity is nonzero but position
did not change (wedged in geometry), nudge
global_position += get_wall_normal() * 0.1.
damage(impact_point, force) on the player: force.y forced positive (always
tossed upward), clamped by max_throwback_force 15, drops up to 5 coins that
scatter for re-collection.
Camera system
Two pivots, one camera.CameraThirdPersonPivot sits at the end of the
SpringArm3D (behind and above). CameraOverShoulderPivot is a plain Node3D
offset right and closer (about x 1.0, y 0.5, z -2.0). Aiming switches pivots
via set_pivot(); each _process the camera copies the active pivot’s
global transform and zeroes rotation.z.
- Orbit uses accumulated euler angles from mouse (
_unhandled_input, only while mouse captured) plus joystick strength added in_process; inputs are consumed and zeroed every frame. Tilt clamped -60..60 deg. Neverlook_at()per frame - it jitters. - SpringArm3D handles wall avoidance for free, but MUST exclude the player:
_camera_spring_arm.add_excluded_object(anchor.get_rid())and_camera_raycast.add_exception_rid(anchor.get_rid())insetup(). - Ground-height tracking: camera Y lerps toward the player’s
_ground_height(from GroundShapeCast), not the player’s Y - so jumps do not bounce the camera. The player computes_ground_heightas the max collision point of the downward ShapeCast, clamped to its own Y when falling below it. - Aim target: CameraRayCast collision point (or the ray end if no hit) is
what bullets fly toward and what the grenade locks onto -
get_aim_target()/get_aim_collider().
Character skin (AnimationTree)
If the visual is a Summer humanoid package, do not use the generic setup below. Instantiate itscharacter.tscn under CharacterRotationRoot, preserve the package’s
normalized -Z forward axis, then install the character-movement skill’s
default third-person controller (summer_docs_read id skill/character-movement/references/default-third-person-controller.gd) unchanged for the standard
playable controller (that skill has the byte-exact install steps and the
verification probe). Do not hand-write an equivalent controller. Never rebuild package clips
into a second AnimationTree.
The generic setup below is only for non-package character skins that already expose
compatible animations.
BlendTree root: a StateMachine (idle / move / jump / fall, where move is a
BlendSpace1D walk-run blended by normalized speed) plus a PunchOneShot
(AnimationNodeOneShot) layered on top so punches play over any state.
jump() on jump press, fall() when
not is_on_floor() and velocity.y < 0, set_moving(xz_speed > stopping_speed)
plus set_moving_speed(inverse_lerp(0, move_speed, xz_speed)) on the floor.
Footsteps: a stepped signal emitted from method-call keyframes in the
walk/run animations; player plays the step sound with randfn(1.2, 0.2)
pitch. Free audio sync, no timers.
animation_tree.active = true in _ready() or nothing plays. Imported
animations often default to non-looping: keep a _force_loop export array
and set loop_mode = Animation.LOOP_LINEAR on those at runtime.
Level assembly
Check for real assets first (asset-strategy: assets.search, then
summer_import_asset): a ready location or kit beats CSG. Keep CSG for the
first-motion blockout, invisible glue (death planes, triggers) and pieces no
asset covers — do not ship a grey-box level when a real one is available.
- WorldEnvironment: sky background (ProceduralSkyMaterial), ACES tonemap
(
tonemap_mode = 2), glow enabled. Without WorldEnvironment everything is flat and dark. - DirectionalLight3D angled ~45 deg,
shadow_enabled = true(off by default; without shadows objects float). - Geometry: CSGCombiner3D with
use_collision = true, CSGBox3D/ CSGCylinder3D children; boolean subtraction (operation 2) for tunnels. Materials withuv1_triplanar = trueso textures map on any face. - DeathPlane: big Area3D at y -20;
body_enteredon the Player callsreset_position()(respawn at_start_positioncaptured in_ready). - Jump pad: Area3D setting
body.velocity = Vector3.UP * body.jump_initial_impulse + transform.basis * Vector3.UP * impulse_strength(basis-relative so rotated pads launch at angles) + squash-and-elastic rebound tween on the mesh. - Project: 1920x1080, stretch canvas_items / keep_height.
- Pause menu node needs
process_mode = ALWAYSand must flip mouse mode between CAPTURED (gameplay) and VISIBLE (menu).
Build order
- Project settings + input actions + layer names.
- Player scene tree above; movement + jump with a placeholder capsule.
- CameraController: spring arm orbit, then over-shoulder aim pivot.
- CharacterSkin AnimationTree once movement feels right.
- Level: imported location/props (
asset-strategy) where they fit; CSG floor/platforms only as blockout or for missing pieces; light, environment, death plane, jump pad. - Combat + enemies + coins (
3d-third-person-systems.md).
Traps
- Parenting the camera rigidly to the player - it inherits every rotation and nauseates. Follow via code with offset + lerp.
- Skipping SpringArm3D (camera clips walls) or forgetting to exclude the player’s RID from it (camera collides with the player).
look_at()every frame for camera orstate_machine.travel()every frame for animation - both jitter/reset; only act on change.- Scaling the CharacterBody3D itself - scale the mesh under CharacterRotationRoot. Physics on scaled bodies misbehaves.
- 1 unit = 1 meter. Characters are 1-2 units tall; extreme scales break physics and lighting.
- Toggling CollisionShape3D
disableddirectly inside physics callbacks - alwaysset_deferred("disabled", ...). - Low gravity. -30 for the player feels right; project-default 16 is floaty. Tune player gravity independently of the physics setting.
Source:
library/skills/make-game/references/3d-third-person.md, rendered from summer-engine@4.3.0 on npm.
