Roblox-style camera: Classic + Shift Lock + scroll zoom + first person
checkpoint / checkpoint (push) Successful in 1m46s
checkpoint / checkpoint (push) Successful in 1m46s
Matches Roblox's default controls so the feel doesn't stray: Classic (default): mouse cursor is free; hold LMB/RMB and drag to orbit the camera (RMB also turns the body to face the camera while held). The camera stays where you leave it -- no auto-recenter. Body auto-rotates toward its movement direction otherwise. Scroll wheel zooms between 0.4 and 9.0 m; at/below 0.6 m it switches to first person (mouse locks, camera at eye height, own avatar hidden so the head doesn't fill the view). Shift Lock (toggle Left Shift): mouse locks to a centre crosshair, camera shifts 0.85 m to the right shoulder, body facing is slaved to the camera, and WASD strafes / backpedals. Movement stays camera-relative throughout. net_input.look_yaw is still the body yaw, so remote players face the right way in every mode. Verified headless (boot, 2p multiplayer, avatar smoke source+pack, fresh-clone) and eyeballed all three modes in rendered screenshots (classic behind / shift-lock over-shoulder+crosshair / first person). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015jtxAiu5dAW4WxCNdGkrkJ
This commit is contained in:
+173
-83
@@ -2,17 +2,23 @@ class_name Player
|
|||||||
extends CharacterBody3D
|
extends CharacterBody3D
|
||||||
## The player character controller.
|
## The player character controller.
|
||||||
##
|
##
|
||||||
## A CharacterBody3D with WASD/arrow locomotion, gravity, jump, and a
|
## Roblox-style third-person controller:
|
||||||
## free-orbit third-person camera. The mouse orbits the camera around the
|
##
|
||||||
## player a full 360 (yaw) plus a wide pitch range -- swing it around to see
|
## * Classic camera (default): the mouse cursor is free; hold a mouse button
|
||||||
## the avatar's face if you want. Movement is camera-relative (W = away from
|
## and drag to orbit the camera 360 in yaw (pitch clamped). Right button
|
||||||
## camera) and the body turns to face the direction it's moving, independent
|
## also turns the character to face the camera while held. Scroll wheel
|
||||||
## of where the camera is pointed. Reads the spawn point from the current
|
## zooms; zoom all the way in -> first person.
|
||||||
## scene's `PlayerSpawn` Marker3D.
|
## * Shift Lock (toggle Left Shift): mouse locks to a centre crosshair, the
|
||||||
|
## camera moves over the right shoulder, and the character's facing is
|
||||||
|
## locked to the camera -- WASD strafes / backpedals.
|
||||||
|
##
|
||||||
|
## Movement is always camera-relative (W = away from the camera). Outside the
|
||||||
|
## camera-facing modes the body auto-rotates toward its movement direction.
|
||||||
|
## Spawn point comes from the current scene's `PlayerSpawn` Marker3D.
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Movement constants
|
# Movement / camera constants
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const WALK_SPEED: float = 5.0
|
const WALK_SPEED: float = 5.0
|
||||||
@@ -22,51 +28,64 @@ const GRAVITY: float = 9.8
|
|||||||
## Mouse-look sensitivity (radians per pixel of relative motion).
|
## Mouse-look sensitivity (radians per pixel of relative motion).
|
||||||
const MOUSE_SENSITIVITY: float = 0.003
|
const MOUSE_SENSITIVITY: float = 0.003
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Input state
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
var _input_direction := Vector2.ZERO
|
|
||||||
var _camera_yaw: float = 0.0 # free orbit, unclamped (wraps 360)
|
|
||||||
var _camera_pitch: float = -0.12
|
|
||||||
var _spawn_position: Vector3 = Vector3(0, 1.5, 0)
|
|
||||||
|
|
||||||
## Vertical offset applied to the assembled avatar so its feet (origin y=0)
|
## Vertical offset applied to the assembled avatar so its feet (origin y=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
|
||||||
|
|
||||||
## Camera pitch limits (radians): from nearly top-down to looking up at the
|
## Camera pitch limits (radians): near top-down to looking up from below.
|
||||||
## avatar from below. Yaw is deliberately unclamped -- full 360 orbit.
|
## Yaw is deliberately unclamped -- full 360 orbit.
|
||||||
const PITCH_MIN: float = -1.35
|
const PITCH_MIN: float = -1.35
|
||||||
const PITCH_MAX: float = 1.2
|
const PITCH_MAX: float = 1.2
|
||||||
|
|
||||||
## Where the camera pivot sits relative to the player origin, and how quickly
|
## Camera pivot height above the player origin, third-person vs first-person.
|
||||||
## the body swings to face its movement direction.
|
const PIVOT_Y_TP: float = 1.2
|
||||||
const CAMERA_PIVOT_OFFSET: Vector3 = Vector3(0.0, 1.2, 0.0)
|
const PIVOT_Y_FP: float = 0.65
|
||||||
|
|
||||||
|
## Zoom (spring length) range, scroll step, and the distance at or below which
|
||||||
|
## we switch to first person.
|
||||||
|
const ZOOM_MIN: float = 0.4
|
||||||
|
const ZOOM_MAX: float = 9.0
|
||||||
|
const ZOOM_STEP: float = 0.6
|
||||||
|
const FIRST_PERSON_ZOOM: float = 0.6
|
||||||
|
const ZOOM_DEFAULT: float = 3.5
|
||||||
|
|
||||||
|
## How far right the camera sits in Shift Lock (metres).
|
||||||
|
const SHOULDER_OFFSET: float = 0.85
|
||||||
|
|
||||||
|
## How fast the body swings toward its target facing.
|
||||||
const BODY_TURN_SPEED: float = 12.0
|
const BODY_TURN_SPEED: float = 12.0
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# State
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
var _input_direction := Vector2.ZERO
|
||||||
|
var _camera_yaw: float = 0.0 # unclamped, wraps
|
||||||
|
var _camera_pitch: float = -0.12
|
||||||
|
var _zoom: float = ZOOM_DEFAULT
|
||||||
|
var _shift_lock: bool = false
|
||||||
|
var _mouse_look_held: bool = false # LMB or RMB held (classic drag)
|
||||||
|
var _rmb_held: bool = false # RMB specifically (turns the body)
|
||||||
|
var _spawn_position: Vector3 = Vector3(0, 1.5, 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
|
||||||
|
@onready var _spring_arm: SpringArm3D = $CameraPivot/SpringArm3D
|
||||||
@onready var _capsule_mesh: MeshInstance3D = $CapsuleMesh
|
@onready var _capsule_mesh: MeshInstance3D = $CapsuleMesh
|
||||||
|
|
||||||
var _avatar_view: Node3D = null
|
var _avatar_view: Node3D = null
|
||||||
var _limb_anim: Node = null
|
var _limb_anim: Node = null
|
||||||
|
var _crosshair: CanvasLayer = null
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# _ready — spawn at PlayerSpawn marker, capture the mouse for look
|
# _ready
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
# Per §3 of pc2_test_level.md: look up PlayerSpawn from the current
|
# Per §3 of pc2_test_level.md: look up PlayerSpawn from the current scene
|
||||||
# scene root. The lookup walks the full subtree (recursive=true) so
|
# root (recursive), guarding the null current_scene of headless runs.
|
||||||
# it finds the marker whether it is a direct child of the level or
|
|
||||||
# nested deeper.
|
|
||||||
#
|
|
||||||
# current_scene is null in headless --script mode and in any scene
|
|
||||||
# before the main scene is fully swapped in, so guard it rather than
|
|
||||||
# blindly dereferencing.
|
|
||||||
var current_scene: Node = get_tree().current_scene
|
var current_scene: Node = get_tree().current_scene
|
||||||
var spawn: Node = null
|
var spawn: Node = null
|
||||||
if current_scene != null:
|
if current_scene != null:
|
||||||
@@ -74,39 +93,52 @@ func _ready() -> void:
|
|||||||
|
|
||||||
if spawn is Node3D:
|
if spawn is Node3D:
|
||||||
global_position = (spawn as Node3D).global_position
|
global_position = (spawn as Node3D).global_position
|
||||||
# Align player forward with spawn yaw (optional polish).
|
rotation.y = (spawn as Node3D).rotation.y
|
||||||
var spawn_yaw := (spawn as Node3D).rotation.y
|
|
||||||
rotation.y = spawn_yaw
|
|
||||||
else:
|
else:
|
||||||
push_warning("player.gd: no PlayerSpawn marker found; spawning at origin")
|
push_warning("player.gd: no PlayerSpawn marker found; spawning at origin")
|
||||||
global_position = Vector3(0, 1.0, 0)
|
global_position = Vector3(0, 1.0, 0)
|
||||||
_spawn_position = global_position
|
_spawn_position = global_position
|
||||||
|
|
||||||
# The spring arm must not collide with the player's own capsule (they're
|
# The spring arm must not collide with the player's own capsule (same
|
||||||
# on the same layer), or the camera gets yanked right up against the
|
# layer) or the camera gets yanked against the player's head.
|
||||||
# player's head.
|
_spring_arm.add_excluded_object(self)
|
||||||
var arm := $CameraPivot/SpringArm3D as SpringArm3D
|
|
||||||
arm.add_excluded_object(self)
|
|
||||||
|
|
||||||
# The camera pivot orbits the player freely, so it can't be a child that
|
# The camera pivot orbits the player freely -- it can't inherit the body's
|
||||||
# inherits the body's yaw -- detach it and drive its world transform
|
# yaw, so detach it and drive its world transform ourselves each frame.
|
||||||
# ourselves each frame.
|
|
||||||
_camera_pivot.top_level = true
|
_camera_pivot.top_level = true
|
||||||
_camera_yaw = rotation.y
|
_camera_yaw = rotation.y
|
||||||
|
|
||||||
|
_build_crosshair()
|
||||||
|
_refresh_mouse_mode()
|
||||||
_update_camera()
|
_update_camera()
|
||||||
|
|
||||||
# Capture the mouse for look. Headless/test runs have no real window to
|
|
||||||
# capture input for, so guard against that rather than error.
|
|
||||||
if DisplayServer.get_name() != "headless":
|
|
||||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
|
||||||
|
|
||||||
_apply_avatar()
|
_apply_avatar()
|
||||||
|
|
||||||
|
|
||||||
|
func _build_crosshair() -> void:
|
||||||
|
if DisplayServer.get_name() == "headless":
|
||||||
|
return
|
||||||
|
_crosshair = CanvasLayer.new()
|
||||||
|
_crosshair.layer = 64
|
||||||
|
var l := Label.new()
|
||||||
|
l.text = "+"
|
||||||
|
l.add_theme_font_size_override("font_size", 22)
|
||||||
|
l.add_theme_color_override("font_color", Color(1, 1, 1, 0.85))
|
||||||
|
l.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.8))
|
||||||
|
l.add_theme_constant_override("outline_size", 4)
|
||||||
|
l.set_anchors_preset(Control.PRESET_CENTER)
|
||||||
|
l.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
l.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
|
l.offset_left = -12
|
||||||
|
l.offset_right = 12
|
||||||
|
l.offset_top = -16
|
||||||
|
l.offset_bottom = 16
|
||||||
|
l.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
_crosshair.add_child(l)
|
||||||
|
add_child(_crosshair)
|
||||||
|
_crosshair.visible = false
|
||||||
|
|
||||||
|
|
||||||
## Show the player's assembled avatar and hide the placeholder capsule.
|
## Show the player's assembled avatar and hide the placeholder capsule.
|
||||||
## AvatarSystem.build_current_avatar() is self-sufficient now (loads the
|
|
||||||
## catalog + falls back to a default loadout), so this only no-ops if the
|
|
||||||
## avatar assets themselves are missing.
|
|
||||||
func _apply_avatar() -> void:
|
func _apply_avatar() -> void:
|
||||||
var avatars := get_node_or_null("/root/AvatarSystem")
|
var avatars := get_node_or_null("/root/AvatarSystem")
|
||||||
if avatars == null or not avatars.has_method("build_current_avatar"):
|
if avatars == null or not avatars.has_method("build_current_avatar"):
|
||||||
@@ -121,8 +153,7 @@ func _apply_avatar() -> void:
|
|||||||
if _capsule_mesh:
|
if _capsule_mesh:
|
||||||
_capsule_mesh.visible = false
|
_capsule_mesh.visible = false
|
||||||
|
|
||||||
# Procedural walk cycle -- no clips yet, just swing the limb bones with
|
# Procedural walk cycle -- no clips yet, just swing the limb bones with speed.
|
||||||
# ground speed.
|
|
||||||
var skel := avatar.find_child("Skeleton3D", true, false)
|
var skel := avatar.find_child("Skeleton3D", true, false)
|
||||||
if skel is Skeleton3D:
|
if skel is Skeleton3D:
|
||||||
_limb_anim = preload("res://scripts/avatar/limb_animator.gd").new()
|
_limb_anim = preload("res://scripts/avatar/limb_animator.gd").new()
|
||||||
@@ -131,18 +162,71 @@ func _apply_avatar() -> void:
|
|||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# _unhandled_input — capture keyboard / mouse for movement
|
# Input
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func _is_first_person() -> bool:
|
||||||
|
return _zoom <= FIRST_PERSON_ZOOM
|
||||||
|
|
||||||
|
|
||||||
|
## Mouse should be captured (hidden, locked) whenever the camera is in an
|
||||||
|
## aim-style mode or the player is mid-drag.
|
||||||
|
func _want_mouse_captured() -> bool:
|
||||||
|
return _shift_lock or _is_first_person() or _mouse_look_held
|
||||||
|
|
||||||
|
|
||||||
|
## Body facing is slaved to the camera in Shift Lock, first person, or while
|
||||||
|
## the right mouse button is held.
|
||||||
|
func _want_face_camera() -> bool:
|
||||||
|
return _shift_lock or _is_first_person() or _rmb_held
|
||||||
|
|
||||||
|
|
||||||
|
func _refresh_mouse_mode() -> void:
|
||||||
|
if DisplayServer.get_name() == "headless":
|
||||||
|
return
|
||||||
|
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED if _want_mouse_captured() \
|
||||||
|
else Input.MOUSE_MODE_VISIBLE
|
||||||
|
if _crosshair != null:
|
||||||
|
_crosshair.visible = _shift_lock or _is_first_person()
|
||||||
|
|
||||||
|
|
||||||
|
func _set_zoom(v: float) -> void:
|
||||||
|
var was_fp := _is_first_person()
|
||||||
|
_zoom = clampf(v, ZOOM_MIN, ZOOM_MAX)
|
||||||
|
if _is_first_person() != was_fp:
|
||||||
|
_refresh_mouse_mode()
|
||||||
|
_update_camera()
|
||||||
|
|
||||||
|
|
||||||
func _unhandled_input(event: InputEvent) -> void:
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
if event is InputEventKey:
|
if event is InputEventKey:
|
||||||
var key := event as InputEventKey
|
var key := event as InputEventKey
|
||||||
if key.pressed and not key.echo:
|
if key.pressed and not key.echo:
|
||||||
|
if key.keycode == KEY_SHIFT:
|
||||||
|
_shift_lock = not _shift_lock
|
||||||
|
_refresh_mouse_mode()
|
||||||
|
_update_camera()
|
||||||
|
else:
|
||||||
_update_input_direction(key)
|
_update_input_direction(key)
|
||||||
elif not key.pressed:
|
elif not key.pressed:
|
||||||
_clear_input_direction(key)
|
_clear_input_direction(key)
|
||||||
# ESC (leave the world) is handled by game.gd, the scene root that
|
# ESC (leave the world) is handled by game.gd, which owns the connection.
|
||||||
# owns the network connection, so the teardown is a single path.
|
|
||||||
|
elif event is InputEventMouseButton:
|
||||||
|
var mb := event as InputEventMouseButton
|
||||||
|
match mb.button_index:
|
||||||
|
MOUSE_BUTTON_WHEEL_UP:
|
||||||
|
if mb.pressed:
|
||||||
|
_set_zoom(_zoom - ZOOM_STEP)
|
||||||
|
MOUSE_BUTTON_WHEEL_DOWN:
|
||||||
|
if mb.pressed:
|
||||||
|
_set_zoom(_zoom + ZOOM_STEP)
|
||||||
|
MOUSE_BUTTON_LEFT, MOUSE_BUTTON_RIGHT:
|
||||||
|
_mouse_look_held = Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT) \
|
||||||
|
or Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT)
|
||||||
|
_rmb_held = Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT)
|
||||||
|
_refresh_mouse_mode()
|
||||||
|
|
||||||
elif event is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
|
elif event is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
|
||||||
_handle_mouse_look(event as InputEventMouseMotion)
|
_handle_mouse_look(event as InputEventMouseMotion)
|
||||||
|
|
||||||
@@ -172,67 +256,73 @@ func _clear_input_direction(key: InputEventKey) -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _handle_mouse_look(event: InputEventMouseMotion) -> void:
|
func _handle_mouse_look(event: InputEventMouseMotion) -> void:
|
||||||
# Mouse orbits the camera only -- the body is left alone (it turns to face
|
|
||||||
# movement in _physics_process). Yaw wraps freely; pitch is clamped.
|
|
||||||
_camera_yaw = wrapf(_camera_yaw - event.relative.x * MOUSE_SENSITIVITY, -PI, PI)
|
_camera_yaw = wrapf(_camera_yaw - event.relative.x * MOUSE_SENSITIVITY, -PI, PI)
|
||||||
_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)
|
||||||
_update_camera()
|
_update_camera()
|
||||||
|
|
||||||
|
|
||||||
## Place the free-orbit camera pivot at the player's head and point it per the
|
## Place the orbit pivot at the player's head and point it per yaw/pitch.
|
||||||
## current yaw/pitch. Called from _ready, on mouse-look, and every frame (the
|
## Applies the first-person / shoulder framing. Called from _ready, on input,
|
||||||
## player moves under it).
|
## and every physics frame (the player moves under it).
|
||||||
func _update_camera() -> void:
|
func _update_camera() -> void:
|
||||||
if not is_instance_valid(_camera_pivot):
|
if not is_instance_valid(_camera_pivot):
|
||||||
return
|
return
|
||||||
_camera_pivot.global_position = global_position + CAMERA_PIVOT_OFFSET
|
var fp := _is_first_person()
|
||||||
|
_camera_pivot.global_position = global_position \
|
||||||
|
+ Vector3(0.0, PIVOT_Y_FP if fp else PIVOT_Y_TP, 0.0)
|
||||||
_camera_pivot.global_rotation = Vector3(_camera_pitch, _camera_yaw, 0.0)
|
_camera_pivot.global_rotation = Vector3(_camera_pitch, _camera_yaw, 0.0)
|
||||||
|
|
||||||
|
_spring_arm.spring_length = 0.0 if fp else _zoom
|
||||||
|
_spring_arm.position.x = SHOULDER_OFFSET if (_shift_lock and not fp) else 0.0
|
||||||
|
|
||||||
|
# Hide our own avatar in first person so the head doesn't fill the view.
|
||||||
|
if _avatar_view != null and is_instance_valid(_avatar_view):
|
||||||
|
_avatar_view.visible = not fp
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Physics — gravity, jump, move_and_slide
|
# Physics
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
## The input frame this player applied last physics tick, in the shape the
|
## Input frame forwarded to the server by scripts/game.gd each physics tick:
|
||||||
## network layer sends: world-space move direction (already yaw-transformed and
|
## world-space move direction (yaw-transformed, normalized), jump edge, body yaw.
|
||||||
## normalized), jump edge, body yaw. scripts/game.gd reads this each frame in
|
|
||||||
## networked mode and forwards it to the server.
|
|
||||||
var net_input: Dictionary = {"move_x": 0.0, "move_z": 0.0, "jump": false, "look_yaw": 0.0}
|
var net_input: Dictionary = {"move_x": 0.0, "move_z": 0.0, "jump": false, "look_yaw": 0.0}
|
||||||
|
|
||||||
|
|
||||||
func _physics_process(delta: float) -> void:
|
func _physics_process(delta: float) -> void:
|
||||||
# Apply gravity.
|
|
||||||
if not is_on_floor():
|
if not is_on_floor():
|
||||||
velocity.y -= GRAVITY * delta
|
velocity.y -= GRAVITY * delta
|
||||||
|
|
||||||
# Jump.
|
|
||||||
var jumped := false
|
var jumped := false
|
||||||
if is_on_floor() and Input.is_action_just_pressed("ui_accept"):
|
if is_on_floor() and Input.is_action_just_pressed("ui_accept"):
|
||||||
velocity.y = JUMP_VELOCITY
|
velocity.y = JUMP_VELOCITY
|
||||||
jumped = true
|
jumped = true
|
||||||
|
|
||||||
# Movement is camera-relative: W (input.y = +1) heads away from the camera,
|
# Camera-relative movement. A node looks down its local -Z, so forward is
|
||||||
# A/D strafe. A node looks down its local -Z, so forward = -cam_basis.z.
|
# -cam_basis.z and W (input.y = +1) heads away from the camera.
|
||||||
var cam_basis := Basis.from_euler(Vector3(0.0, _camera_yaw, 0.0))
|
var cam_basis := Basis.from_euler(Vector3(0.0, _camera_yaw, 0.0))
|
||||||
var direction := (_input_direction.x * cam_basis.x -
|
var direction := (_input_direction.x * cam_basis.x
|
||||||
_input_direction.y * cam_basis.z).normalized()
|
- _input_direction.y * cam_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).
|
# Body facing: locked to the camera in aim modes, otherwise turns toward
|
||||||
if direction.length_squared() > 0.01:
|
# the movement direction.
|
||||||
|
if _want_face_camera():
|
||||||
|
rotation.y = _camera_yaw
|
||||||
|
elif direction.length_squared() > 0.01:
|
||||||
var target_yaw := atan2(-direction.x, -direction.z)
|
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))
|
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()
|
_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())
|
||||||
|
|
||||||
# Safety net -- never fall forever if something goes wrong.
|
# Safety net -- never fall forever.
|
||||||
if global_position.y < -10.0:
|
if global_position.y < -10.0:
|
||||||
global_position = _spawn_position
|
global_position = _spawn_position
|
||||||
velocity = Vector3.ZERO
|
velocity = Vector3.ZERO
|
||||||
@@ -243,8 +333,8 @@ func _physics_process(delta: float) -> void:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
## Server-authoritative correction: called by scripts/game.gd when the local
|
## Server-authoritative correction from scripts/game.gd when the local
|
||||||
## predicted position has drifted too far from the server's.
|
## predicted position has drifted too far.
|
||||||
func apply_server_correction(pos: Vector3) -> void:
|
func apply_server_correction(pos: Vector3) -> void:
|
||||||
global_position = pos
|
global_position = pos
|
||||||
velocity.x = 0.0
|
velocity.x = 0.0
|
||||||
|
|||||||
Reference in New Issue
Block a user