The mouse now orbits the camera around the player a full 360 in yaw (unclamped) with a wide pitch range -- swing it around to look at the avatar's face. The body no longer snaps to the camera yaw; instead it turns smoothly to face its movement direction (lerp_angle at BODY_TURN_SPEED), so controls feel the same: W is still "away from camera", A/D still strafe relative to view. Implementation: CameraPivot is set top_level so it doesn't inherit the body's rotation; _update_camera() places it at the head each frame and points it by _camera_yaw / _camera_pitch. Movement direction is built from a yaw-only camera basis. net_input.look_yaw is now the body's movement facing, so remote players render facing the way they walk. Verified: headless boot clean, 2p multiplayer still connects/moves, avatar smoke 14/14 (source + exported pack), fresh-clone PASS, and eyeballed the orbit in rendered screenshots (behind -> side -> face). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015jtxAiu5dAW4WxCNdGkrkJ
This commit is contained in:
+49
-20
@@ -3,9 +3,12 @@ extends CharacterBody3D
|
|||||||
## The player character controller.
|
## The player character controller.
|
||||||
##
|
##
|
||||||
## A CharacterBody3D with WASD/arrow locomotion, gravity, jump, and a
|
## A CharacterBody3D with WASD/arrow locomotion, gravity, jump, and a
|
||||||
## third-person camera (SpringArm3D behind the player) with mouse-look --
|
## free-orbit third-person camera. The mouse orbits the camera around the
|
||||||
## yaw turns the body, pitch orbits the camera. Reads the spawn point from
|
## player a full 360 (yaw) plus a wide pitch range -- swing it around to see
|
||||||
## the current scene's `PlayerSpawn` Marker3D.
|
## the avatar's face if you want. Movement is camera-relative (W = away from
|
||||||
|
## camera) and the body turns to face the direction it's moving, independent
|
||||||
|
## of where the camera is pointed. Reads the spawn point from the current
|
||||||
|
## scene's `PlayerSpawn` Marker3D.
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -25,6 +28,7 @@ const MOUSE_SENSITIVITY: float = 0.003
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
var _input_direction := Vector2.ZERO
|
var _input_direction := Vector2.ZERO
|
||||||
|
var _camera_yaw: float = 0.0 # free orbit, unclamped (wraps 360)
|
||||||
var _camera_pitch: float = -0.12
|
var _camera_pitch: float = -0.12
|
||||||
var _spawn_position: Vector3 = Vector3(0, 1.5, 0)
|
var _spawn_position: Vector3 = Vector3(0, 1.5, 0)
|
||||||
|
|
||||||
@@ -32,10 +36,15 @@ var _spawn_position: Vector3 = Vector3(0, 1.5, 0)
|
|||||||
## line up with the bottom of the player's collision capsule.
|
## line up with the bottom of the player's collision capsule.
|
||||||
const AVATAR_FEET_OFFSET: float = -0.8
|
const AVATAR_FEET_OFFSET: float = -0.8
|
||||||
|
|
||||||
## Third-person pitch limits (radians): a bit up, a lot down (looking over
|
## Camera pitch limits (radians): from nearly top-down to looking up at the
|
||||||
## the avatar's shoulder toward the ground).
|
## avatar from below. Yaw is deliberately unclamped -- full 360 orbit.
|
||||||
const PITCH_MIN: float = -1.1
|
const PITCH_MIN: float = -1.35
|
||||||
const PITCH_MAX: float = 0.45
|
const PITCH_MAX: float = 1.2
|
||||||
|
|
||||||
|
## Where the camera pivot sits relative to the player origin, and how quickly
|
||||||
|
## the body swings to face its movement direction.
|
||||||
|
const CAMERA_PIVOT_OFFSET: Vector3 = Vector3(0.0, 1.2, 0.0)
|
||||||
|
const BODY_TURN_SPEED: float = 12.0
|
||||||
|
|
||||||
@onready var _camera: Camera3D = $CameraPivot/SpringArm3D/Camera3D
|
@onready var _camera: Camera3D = $CameraPivot/SpringArm3D/Camera3D
|
||||||
@onready var _camera_pivot: Node3D = $CameraPivot
|
@onready var _camera_pivot: Node3D = $CameraPivot
|
||||||
@@ -79,8 +88,12 @@ func _ready() -> void:
|
|||||||
var arm := $CameraPivot/SpringArm3D as SpringArm3D
|
var arm := $CameraPivot/SpringArm3D as SpringArm3D
|
||||||
arm.add_excluded_object(self)
|
arm.add_excluded_object(self)
|
||||||
|
|
||||||
# Start the camera arm tilted down a little so the avatar is framed.
|
# The camera pivot orbits the player freely, so it can't be a child that
|
||||||
_camera_pivot.rotation.x = _camera_pitch
|
# inherits the body's yaw -- detach it and drive its world transform
|
||||||
|
# ourselves each frame.
|
||||||
|
_camera_pivot.top_level = true
|
||||||
|
_camera_yaw = rotation.y
|
||||||
|
_update_camera()
|
||||||
|
|
||||||
# Capture the mouse for look. Headless/test runs have no real window to
|
# Capture the mouse for look. Headless/test runs have no real window to
|
||||||
# capture input for, so guard against that rather than error.
|
# capture input for, so guard against that rather than error.
|
||||||
@@ -159,12 +172,21 @@ func _clear_input_direction(key: InputEventKey) -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _handle_mouse_look(event: InputEventMouseMotion) -> void:
|
func _handle_mouse_look(event: InputEventMouseMotion) -> void:
|
||||||
# Yaw rotates the whole body (so movement direction turns with the
|
# Mouse orbits the camera only -- the body is left alone (it turns to face
|
||||||
# look direction); pitch orbits the camera arm around the player, so the
|
# movement in _physics_process). Yaw wraps freely; pitch is clamped.
|
||||||
# capsule itself never tips over.
|
_camera_yaw = wrapf(_camera_yaw - event.relative.x * MOUSE_SENSITIVITY, -PI, PI)
|
||||||
rotation.y -= event.relative.x * MOUSE_SENSITIVITY
|
|
||||||
_camera_pitch = clampf(_camera_pitch - event.relative.y * MOUSE_SENSITIVITY, PITCH_MIN, PITCH_MAX)
|
_camera_pitch = clampf(_camera_pitch - event.relative.y * MOUSE_SENSITIVITY, PITCH_MIN, PITCH_MAX)
|
||||||
_camera_pivot.rotation.x = _camera_pitch
|
_update_camera()
|
||||||
|
|
||||||
|
|
||||||
|
## Place the free-orbit camera pivot at the player's head and point it per the
|
||||||
|
## current yaw/pitch. Called from _ready, on mouse-look, and every frame (the
|
||||||
|
## player moves under it).
|
||||||
|
func _update_camera() -> void:
|
||||||
|
if not is_instance_valid(_camera_pivot):
|
||||||
|
return
|
||||||
|
_camera_pivot.global_position = global_position + CAMERA_PIVOT_OFFSET
|
||||||
|
_camera_pivot.global_rotation = Vector3(_camera_pitch, _camera_yaw, 0.0)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -189,17 +211,24 @@ func _physics_process(delta: float) -> void:
|
|||||||
velocity.y = JUMP_VELOCITY
|
velocity.y = JUMP_VELOCITY
|
||||||
jumped = true
|
jumped = true
|
||||||
|
|
||||||
# Build movement direction from input. Godot's local "forward" is -Z, not
|
# Movement is camera-relative: W (input.y = +1) heads away from the camera,
|
||||||
# +Z, so W (input.y = +1) must use -basis.z -- using +basis.z here would
|
# A/D strafe. A node looks down its local -Z, so forward = -cam_basis.z.
|
||||||
# move the player backward when pressing forward (confirmed by reading
|
var cam_basis := Basis.from_euler(Vector3(0.0, _camera_yaw, 0.0))
|
||||||
# Camera3D/Node3D's own convention: a node looks down its local -Z).
|
var direction := (_input_direction.x * cam_basis.x -
|
||||||
var direction := (_input_direction.x * transform.basis.x -
|
_input_direction.y * cam_basis.z).normalized()
|
||||||
_input_direction.y * transform.basis.z).normalized()
|
|
||||||
velocity.x = direction.x * WALK_SPEED
|
velocity.x = direction.x * WALK_SPEED
|
||||||
velocity.z = direction.z * WALK_SPEED
|
velocity.z = direction.z * WALK_SPEED
|
||||||
|
|
||||||
|
# Turn the body to face where it's moving (independent of the camera).
|
||||||
|
if direction.length_squared() > 0.01:
|
||||||
|
var target_yaw := atan2(-direction.x, -direction.z)
|
||||||
|
rotation.y = lerp_angle(rotation.y, target_yaw, clampf(BODY_TURN_SPEED * delta, 0.0, 1.0))
|
||||||
|
|
||||||
move_and_slide()
|
move_and_slide()
|
||||||
|
|
||||||
|
# Keep the orbit camera on the player as it moves.
|
||||||
|
_update_camera()
|
||||||
|
|
||||||
if _limb_anim != null:
|
if _limb_anim != null:
|
||||||
_limb_anim.set("speed", Vector2(velocity.x, velocity.z).length())
|
_limb_anim.set("speed", Vector2(velocity.x, velocity.z).length())
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user