WIP: add Camera3D, fix jump/forward-direction bugs, add mouse capture, build game.tscn composition
This commit is contained in:
@@ -0,0 +1,10 @@
|
|||||||
|
[gd_scene load_steps=3 format=3 uid="uid://game_scene_pc2"]
|
||||||
|
|
||||||
|
[ext_resource type="PackedScene" uid="uid://c49a36fc7db_test_level" path="res://scenes/worlds/test_level.tscn" id="1_level"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://player_scene_001" path="res://scenes/player.tscn" id="2_player"]
|
||||||
|
|
||||||
|
[node name="Game" type="Node3D"]
|
||||||
|
|
||||||
|
[node name="Level" parent="." instance=ExtResource("1_level")]
|
||||||
|
|
||||||
|
[node name="Player" parent="." instance=ExtResource("2_player")]
|
||||||
@@ -18,3 +18,7 @@ shape = SubResource("CapsuleShape1")
|
|||||||
|
|
||||||
[node name="CapsuleMesh" type="MeshInstance3D" parent="."]
|
[node name="CapsuleMesh" type="MeshInstance3D" parent="."]
|
||||||
mesh = SubResource("CapsuleMesh1")
|
mesh = SubResource("CapsuleMesh1")
|
||||||
|
|
||||||
|
[node name="Camera3D" type="Camera3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.6, 0)
|
||||||
|
current = true
|
||||||
|
|||||||
+41
-12
@@ -2,9 +2,9 @@ class_name Player
|
|||||||
extends CharacterBody3D
|
extends CharacterBody3D
|
||||||
## The player character controller.
|
## The player character controller.
|
||||||
##
|
##
|
||||||
## A simple CharacterBody3D with WASD/arrow locomotion, gravity, and a
|
## A simple CharacterBody3D with WASD/arrow locomotion, gravity, jump, and a
|
||||||
## first-person Camera3D. Reads the spawn point from the current scene's
|
## first-person Camera3D with mouse-look. Reads the spawn point from the
|
||||||
## `PlayerSpawn` Marker3D (see docs/architecture/pc2_test_level.md §3).
|
## 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.
|
## No networking, no chunk code, no avatar UI — PC2 is local-only.
|
||||||
|
|
||||||
@@ -14,19 +14,29 @@ extends CharacterBody3D
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const WALK_SPEED: float = 5.0
|
const WALK_SPEED: float = 5.0
|
||||||
const JUMP_VELOCITY: float = -4.5
|
const JUMP_VELOCITY: float = 4.5
|
||||||
const GRAVITY: float = 9.8
|
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
|
# Input state
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
var _input_direction := Vector2.ZERO
|
var _input_direction := Vector2.ZERO
|
||||||
|
var _camera_pitch: float = 0.0
|
||||||
|
|
||||||
|
@onready var _camera: Camera3D = $Camera3D
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# _ready — spawn at PlayerSpawn marker
|
# _ready — spawn at PlayerSpawn marker, capture the mouse for look
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
@@ -52,6 +62,11 @@ func _ready() -> void:
|
|||||||
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)
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
if DisplayServer.get_name() != "headless":
|
||||||
|
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# _unhandled_input — capture keyboard / mouse for movement
|
# _unhandled_input — capture keyboard / mouse for movement
|
||||||
@@ -60,11 +75,18 @@ func _ready() -> void:
|
|||||||
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:
|
if key.pressed and not key.echo:
|
||||||
_update_input_direction(key)
|
_update_input_direction(key)
|
||||||
else:
|
elif not key.pressed:
|
||||||
_clear_input_direction(key)
|
_clear_input_direction(key)
|
||||||
elif event is InputEventMouseMotion:
|
# Release the mouse so the player can get back to a menu/cursor.
|
||||||
|
if key.pressed and key.keycode == KEY_ESCAPE:
|
||||||
|
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||||
|
elif event is InputEventMouseButton and event.pressed:
|
||||||
|
# Clicking back into the viewport re-captures the mouse.
|
||||||
|
if Input.mouse_mode == Input.MOUSE_MODE_VISIBLE and DisplayServer.get_name() != "headless":
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
@@ -93,7 +115,12 @@ func _clear_input_direction(key: InputEventKey) -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _handle_mouse_look(event: InputEventMouseMotion) -> void:
|
func _handle_mouse_look(event: InputEventMouseMotion) -> void:
|
||||||
rotation.y -= event.relative.x * 0.002
|
# 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.
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -108,10 +135,12 @@ func _physics_process(delta: float) -> void:
|
|||||||
# Jump.
|
# Jump.
|
||||||
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
|
||||||
return
|
|
||||||
|
|
||||||
# Build movement direction from input.
|
# Build movement direction from input. Godot's local "forward" is -Z, not
|
||||||
var direction := (_input_direction.x * transform.basis.x +
|
# +Z, so W (input.y = +1) must use -basis.z -- using +basis.z here would
|
||||||
|
# move the player backward when pressing forward (confirmed by reading
|
||||||
|
# Camera3D/Node3D's own convention: a node looks down its local -Z).
|
||||||
|
var direction := (_input_direction.x * transform.basis.x -
|
||||||
_input_direction.y * transform.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
|
||||||
|
|||||||
@@ -18,13 +18,15 @@ func _initialize() -> void:
|
|||||||
|
|
||||||
func _run() -> void:
|
func _run() -> void:
|
||||||
await process_frame
|
await process_frame
|
||||||
print("SMOKE: loading test level scene")
|
print("SMOKE: loading game scene")
|
||||||
|
|
||||||
# Load the test level scene (which includes the player). Use the same
|
# Load scenes/game.tscn -- the real PC2 composition point (Level + Player
|
||||||
# SceneTree API as the real game so `current_scene` is populated before
|
# as siblings, per docs/architecture/pc2_test_level.md §4), not the level
|
||||||
# the scene is added and its _ready() callbacks fire. That ordering is
|
# scene directly. Use the same SceneTree API as the real game so
|
||||||
# what the player's spawn lookup (get_tree().current_scene) depends on.
|
# `current_scene` is populated before the scene is added and its
|
||||||
var packed: PackedScene = load("res://scenes/worlds/test_level.tscn")
|
# _ready() callbacks fire. That ordering is what the player's spawn
|
||||||
|
# lookup (get_tree().current_scene) depends on.
|
||||||
|
var packed: PackedScene = load("res://scenes/game.tscn")
|
||||||
var result := change_scene_to_packed(packed)
|
var result := change_scene_to_packed(packed)
|
||||||
if result != OK:
|
if result != OK:
|
||||||
printerr("SMOKE: failed to change scene, error ", result)
|
printerr("SMOKE: failed to change scene, error ", result)
|
||||||
@@ -32,11 +34,11 @@ func _run() -> void:
|
|||||||
return
|
return
|
||||||
|
|
||||||
await scene_changed
|
await scene_changed
|
||||||
var level: Node3D = current_scene as Node3D
|
var game: Node3D = current_scene as Node3D
|
||||||
print("SMOKE: level ready")
|
print("SMOKE: game scene ready")
|
||||||
|
|
||||||
# 1. Verify the player node exists in the scene tree.
|
# 1. Verify the player node exists in the scene tree.
|
||||||
var player: Node = level.get_node("Player")
|
var player: Node = game.get_node("Player")
|
||||||
_check("Player node exists in test level", player != null)
|
_check("Player node exists in test level", player != null)
|
||||||
_check("Player is a CharacterBody3D", player is CharacterBody3D)
|
_check("Player is a CharacterBody3D", player is CharacterBody3D)
|
||||||
|
|
||||||
@@ -52,13 +54,12 @@ func _run() -> void:
|
|||||||
mesh is MeshInstance3D and (mesh as MeshInstance3D).mesh != null)
|
mesh is MeshInstance3D and (mesh as MeshInstance3D).mesh != null)
|
||||||
|
|
||||||
# 3. Verify the player spawned at the PlayerSpawn marker position.
|
# 3. Verify the player spawned at the PlayerSpawn marker position.
|
||||||
var spawn_marker: Marker3D = level.get_node("PlayerSpawn")
|
var spawn_marker: Marker3D = game.get_node("Level/PlayerSpawn")
|
||||||
_check("PlayerSpawn marker exists", spawn_marker != null)
|
_check("PlayerSpawn marker exists", spawn_marker != null)
|
||||||
|
|
||||||
# The player's _ready() should have moved it to the spawn marker's
|
# The player's _ready() should have moved it to the spawn marker's
|
||||||
# global position. Since both are direct children of the level root
|
# global position. Compare global positions directly (not local) since
|
||||||
# (and the level root is at the origin), their global positions equal
|
# Player and Level sit at different depths under Game.
|
||||||
# their local positions.
|
|
||||||
var expected_pos: Vector3 = spawn_marker.global_position
|
var expected_pos: Vector3 = spawn_marker.global_position
|
||||||
_check("Player spawned at PlayerSpawn position",
|
_check("Player spawned at PlayerSpawn position",
|
||||||
player.global_position == expected_pos)
|
player.global_position == expected_pos)
|
||||||
@@ -71,6 +72,13 @@ func _run() -> void:
|
|||||||
_check("Player has WALK_SPEED constant", constants.has("WALK_SPEED"))
|
_check("Player has WALK_SPEED constant", constants.has("WALK_SPEED"))
|
||||||
_check("Player has GRAVITY constant", constants.has("GRAVITY"))
|
_check("Player has GRAVITY constant", constants.has("GRAVITY"))
|
||||||
_check("Player has JUMP_VELOCITY constant", constants.has("JUMP_VELOCITY"))
|
_check("Player has JUMP_VELOCITY constant", constants.has("JUMP_VELOCITY"))
|
||||||
|
_check("JUMP_VELOCITY is positive (upward)", float(constants.get("JUMP_VELOCITY", -1.0)) > 0.0)
|
||||||
|
|
||||||
|
# 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")
|
||||||
|
_check("Player has a Camera3D", camera is Camera3D)
|
||||||
|
_check("Camera3D is current", camera is Camera3D and (camera as Camera3D).current)
|
||||||
|
|
||||||
print("\n=== SMOKE RESULT: %d/%d passed ===" % [_passed, _passed + _failed])
|
print("\n=== SMOKE RESULT: %d/%d passed ===" % [_passed, _passed + _failed])
|
||||||
quit(0 if _failed == 0 else 1)
|
quit(0 if _failed == 0 else 1)
|
||||||
|
|||||||
Reference in New Issue
Block a user