Third-person camera + fix the avatar customize preview
checkpoint / checkpoint (push) Successful in 1m34s
checkpoint / checkpoint (push) Successful in 1m34s
You couldn't see your own character because the camera was first-person (inside the avatar's head), and the customize screen's 3D preview rendered nothing. - player.tscn: camera now hangs off a SpringArm3D on a CameraPivot, 4 m behind + above the player, pulled in on collision. player.gd: yaw still turns the body, pitch orbits the arm (starts tilted down to frame the avatar). - avatar_customization_ui.gd: the preview SubViewport had own_world_3d=false, so it shared the Control-only main world -- no scenario, nothing rendered. Set own_world_3d=true, add a WorldEnvironment with ambient fill + a key/fill light pair, make the camera current, tweak framing. - smoke_test_player: assert the third-person SpringArm rig (18/18). - smoke_test_enter_world: wait longer for the scene change -- a real POST /join spawns a dedicated server, which is slow (9/9). Client-side change -- rebuild the exe to get it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015jtxAiu5dAW4WxCNdGkrkJ
This commit is contained in:
+10
-3
@@ -1,4 +1,4 @@
|
||||
[gd_scene load_steps=3 format=3 uid="uid://player_scene_001"]
|
||||
[gd_scene load_steps=4 format=3 uid="uid://player_scene_001"]
|
||||
|
||||
[ext_resource type="Script" path="res://scripts/player.gd" id="1_player"]
|
||||
|
||||
@@ -19,6 +19,13 @@ shape = SubResource("CapsuleShape1")
|
||||
[node name="CapsuleMesh" type="MeshInstance3D" parent="."]
|
||||
mesh = SubResource("CapsuleMesh1")
|
||||
|
||||
[node name="Camera3D" type="Camera3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.6, 0)
|
||||
[node name="CameraPivot" type="Node3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.7, 0)
|
||||
|
||||
[node name="SpringArm3D" type="SpringArm3D" parent="CameraPivot"]
|
||||
transform = Transform3D(-1, 0, -8.7422777e-08, 0, 1, 0, 8.7422777e-08, 0, -1, 0, 0, 0)
|
||||
spring_length = 4.0
|
||||
margin = 0.2
|
||||
|
||||
[node name="Camera3D" type="Camera3D" parent="CameraPivot/SpringArm3D"]
|
||||
current = true
|
||||
|
||||
@@ -165,14 +165,31 @@ func _build_ui() -> void:
|
||||
_preview_viewport.name = "PreviewViewport"
|
||||
_preview_viewport.size = Vector2i(464, 560)
|
||||
_preview_viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS
|
||||
_preview_viewport.transparent_bg = true
|
||||
# Its own 3D world -- a SubViewport that shares the (Control-only) main
|
||||
# world has no scenario/environment set up and renders nothing.
|
||||
_preview_viewport.own_world_3d = true
|
||||
_preview_container.add_child(_preview_viewport)
|
||||
|
||||
# Environment: ambient fill so the model reads even from a bad light angle.
|
||||
var world_env := WorldEnvironment.new()
|
||||
world_env.name = "PreviewEnv"
|
||||
var env := Environment.new()
|
||||
env.background_mode = Environment.BG_COLOR
|
||||
env.background_color = Color(0.10, 0.11, 0.14)
|
||||
env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
|
||||
env.ambient_light_color = Color(0.6, 0.6, 0.65)
|
||||
env.ambient_light_energy = 1.0
|
||||
world_env.environment = env
|
||||
_preview_viewport.add_child(world_env)
|
||||
|
||||
# Camera for the preview.
|
||||
_preview_camera = Camera3D.new()
|
||||
_preview_camera.name = "PreviewCamera"
|
||||
# look_at needs a global transform, which only exists once in the tree.
|
||||
_preview_camera.look_at_from_position(
|
||||
Vector3(0, 1.0, 3.0), Vector3(0, 1.0, 0), Vector3.UP)
|
||||
Vector3(0, 1.0, 3.2), Vector3(0, 0.9, 0), Vector3.UP)
|
||||
_preview_camera.current = true
|
||||
_preview_viewport.add_child(_preview_camera)
|
||||
|
||||
# Avatar root inside the viewport.
|
||||
@@ -180,11 +197,17 @@ func _build_ui() -> void:
|
||||
_preview_root.name = "AvatarPreviewRoot"
|
||||
_preview_viewport.add_child(_preview_root)
|
||||
|
||||
# A light so the avatar is visible.
|
||||
var light := DirectionalLight3D.new()
|
||||
light.name = "PreviewLight"
|
||||
light.rotation_degrees = Vector3(-45, 30, 0)
|
||||
_preview_viewport.add_child(light)
|
||||
# Key + fill lights so the avatar is clearly visible.
|
||||
var key := DirectionalLight3D.new()
|
||||
key.name = "PreviewKeyLight"
|
||||
key.rotation_degrees = Vector3(-35, 35, 0)
|
||||
key.light_energy = 1.2
|
||||
_preview_viewport.add_child(key)
|
||||
var fill := DirectionalLight3D.new()
|
||||
fill.name = "PreviewFillLight"
|
||||
fill.rotation_degrees = Vector3(-20, -140, 0)
|
||||
fill.light_energy = 0.4
|
||||
_preview_viewport.add_child(fill)
|
||||
|
||||
# --- Right: controls ---
|
||||
var controls_panel := PanelContainer.new()
|
||||
|
||||
+21
-17
@@ -2,11 +2,10 @@ class_name Player
|
||||
extends CharacterBody3D
|
||||
## The player character controller.
|
||||
##
|
||||
## A simple CharacterBody3D with WASD/arrow locomotion, gravity, jump, and a
|
||||
## first-person Camera3D with mouse-look. Reads the spawn point from the
|
||||
## current scene's `PlayerSpawn` Marker3D (see docs/architecture/pc2_test_level.md §3).
|
||||
##
|
||||
## No networking, no chunk code, no avatar UI — PC2 is local-only.
|
||||
## A CharacterBody3D with WASD/arrow locomotion, gravity, jump, and a
|
||||
## third-person camera (SpringArm3D behind the player) with mouse-look --
|
||||
## yaw turns the body, pitch orbits the camera. Reads the spawn point from
|
||||
## the current scene's `PlayerSpawn` Marker3D.
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -20,23 +19,25 @@ const GRAVITY: float = 9.8
|
||||
## Mouse-look sensitivity (radians per pixel of relative motion).
|
||||
const MOUSE_SENSITIVITY: float = 0.003
|
||||
|
||||
## Camera pitch is clamped so the player can't flip past looking straight
|
||||
## up/down (which would otherwise let mouse-look invert on itself).
|
||||
const PITCH_LIMIT: float = deg_to_rad(89.0)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Input state
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
var _input_direction := Vector2.ZERO
|
||||
var _camera_pitch: float = 0.0
|
||||
var _camera_pitch: float = -0.25
|
||||
|
||||
## Vertical offset applied to the assembled avatar so its feet (origin y=0)
|
||||
## line up with the bottom of the player's collision capsule.
|
||||
const AVATAR_FEET_OFFSET: float = -0.8
|
||||
|
||||
@onready var _camera: Camera3D = $Camera3D
|
||||
## Third-person pitch limits (radians): a bit up, a lot down (looking over
|
||||
## the avatar's shoulder toward the ground).
|
||||
const PITCH_MIN: float = -1.1
|
||||
const PITCH_MAX: float = 0.45
|
||||
|
||||
@onready var _camera: Camera3D = $CameraPivot/SpringArm3D/Camera3D
|
||||
@onready var _camera_pivot: Node3D = $CameraPivot
|
||||
@onready var _capsule_mesh: MeshInstance3D = $CapsuleMesh
|
||||
|
||||
var _avatar_view: Node3D = null
|
||||
@@ -69,8 +70,11 @@ func _ready() -> void:
|
||||
push_warning("player.gd: no PlayerSpawn marker found; spawning at origin")
|
||||
global_position = Vector3(0, 1.0, 0)
|
||||
|
||||
# Capture the mouse for FPS-style look. Headless/test runs have no real
|
||||
# window to capture input for, so guard against that rather than error.
|
||||
# Start the camera arm tilted down a little so the avatar is framed.
|
||||
_camera_pivot.rotation.x = _camera_pitch
|
||||
|
||||
# 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
|
||||
|
||||
@@ -143,11 +147,11 @@ func _clear_input_direction(key: InputEventKey) -> void:
|
||||
|
||||
func _handle_mouse_look(event: InputEventMouseMotion) -> void:
|
||||
# Yaw rotates the whole body (so movement direction turns with the
|
||||
# look direction); pitch only tilts the camera, so the capsule itself
|
||||
# never tips over.
|
||||
# look direction); pitch orbits the camera arm around the player, so the
|
||||
# capsule itself never tips over.
|
||||
rotation.y -= event.relative.x * MOUSE_SENSITIVITY
|
||||
_camera_pitch = clampf(_camera_pitch - event.relative.y * MOUSE_SENSITIVITY, -PITCH_LIMIT, PITCH_LIMIT)
|
||||
_camera.rotation.x = _camera_pitch
|
||||
_camera_pitch = clampf(_camera_pitch - event.relative.y * MOUSE_SENSITIVITY, PITCH_MIN, PITCH_MAX)
|
||||
_camera_pivot.rotation.x = _camera_pitch
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -81,7 +81,7 @@ func _run() -> void:
|
||||
return
|
||||
|
||||
enter_button.pressed.emit()
|
||||
await _wait_for_scene_change(instance, 60)
|
||||
await _wait_for_scene_change(instance, 600) # generous -- a real POST /join may spawn a dedicated server (slow)
|
||||
|
||||
var game_scene := current_scene
|
||||
if not _check("game.tscn became current_scene after Enter World", game_scene != null and game_scene != instance):
|
||||
|
||||
@@ -76,9 +76,15 @@ func _run() -> void:
|
||||
|
||||
# 5. Verify the player has a Camera3D so the game is actually viewable
|
||||
# from the player's perspective, not just a walking capsule with no view.
|
||||
var camera: Node = player.get_node_or_null("Camera3D")
|
||||
var camera: Node = player.find_child("Camera3D", true, false)
|
||||
_check("Player has a Camera3D", camera is Camera3D)
|
||||
_check("Camera3D is current", camera is Camera3D and (camera as Camera3D).current)
|
||||
# Third-person rig: the camera hangs off a SpringArm3D (so it pulls in
|
||||
# when a wall is behind the player) with real length.
|
||||
var arm: Node = player.find_child("SpringArm3D", true, false)
|
||||
_check("Camera is on a SpringArm3D (third-person)", arm is SpringArm3D)
|
||||
_check("SpringArm has real length", arm is SpringArm3D and (arm as SpringArm3D).spring_length >= 2.0)
|
||||
_check("Camera is a descendant of the SpringArm", camera != null and arm != null and arm.is_ancestor_of(camera))
|
||||
|
||||
print("\n=== SMOKE RESULT: %d/%d passed ===" % [_passed, _passed + _failed])
|
||||
quit(0 if _failed == 0 else 1)
|
||||
|
||||
Reference in New Issue
Block a user