Files
agame/player/player.gd
T
2026-09-22 17:45:29 +08:00

199 lines
7.4 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
extends CharacterBody3D
## 第三人称控制器(胶囊体占位)
##
## 场景结构:
## Player (CharacterBody3D) ← 只负责移动与碰撞,自身不旋转
## ├─ Collision (CollisionShape3D, CapsuleShape3D)
## ├─ Body (Node3D) ← 视觉层,朝移动方向转身;换角色模型时挂在它下面
## │ └─ Mesh (MeshInstance3D)
## ├─ CameraRig (Node3D) ← 承担偏航 yaw,独立于本体旋转
## │ └─ SpringArm (SpringArm3D) ← 承担俯仰 pitch,自动避障
## │ └─ Camera (Camera3D)
## ├─ Sync (MultiplayerSynchronizer) ← 同步 position 与 Body.rotation
## └─ Hud (CanvasLayer) ← 本地玩家的血条(节点配置,不是代码建的)
## └─ HealthLabel (Label)
##
## 操作:WASD/方向键移动,鼠标转视角,Shift 疾跑,空格跳跃,Esc 释放/锁定鼠标
## 跳跃输入缓冲帧数(输入事件在渲染帧到达,物理帧读取,避免漏跳)
const JUMP_BUFFER_FRAMES: int = 6
@export_group("属性")
## 血量
@export var health: int = 100:
set(value):
health = clampi(value, 0, 100)
_refresh_health_label()
@export_group("移动")
## 常规移动速度(米/秒)
@export var walk_speed: float = 5.0
## 疾跑速度(米/秒)
@export var sprint_speed: float = 8.5
## 地面加速度(米/秒²)
@export var acceleration: float = 45.0
## 空中加速度(米/秒²)
@export var air_acceleration: float = 12.0
## 起跳初速度(米/秒)
@export var jump_velocity: float = 5.0
## 视觉层转身平滑速度
@export var turn_speed: float = 10.0
@export_group("相机")
## 鼠标灵敏度(弧度/像素)
@export var mouse_sensitivity: float = 0.0025
## 最低俯角(向下看)
@export var pitch_min_deg: float = -80.0
## 最高仰角(向上看)
@export var pitch_max_deg: float = 45.0
@onready var body: Node3D = $Body
@onready var camera_rig: Node3D = $CameraRig
@onready var spring_arm: SpringArm3D = $CameraRig/SpringArm
## 血条是 player.tscn 里的节点,不是代码建的 UI
@onready var _hud: CanvasLayer = $Hud
@onready var _health_label: Label = $Hud/HealthLabel
var _gravity: float = float(ProjectSettings.get_setting("physics/3d/default_gravity", 9.8))
var _yaw: float = 0.0
var _pitch: float = 0.0
var _jump_buffer: int = 0
## 本节点是否由本机玩家操控。由世界节点在生成时设置 authority 决定,见 world/main.gd。
var _is_local: bool = true
func _ready() -> void:
_report_identity()
_is_local = is_multiplayer_authority()
if not _is_local:
# 远端玩家只是一具「回放身体」:
# 拆掉相机 —— 否则每多一个玩家就多一台 Camera3D 互相抢渲染;
# 关掉血条 —— 否则 N 份血条叠在同一位置,显示的血量可能是别人的;
# 关掉输入与物理 —— 否则与权威端双重模拟,表现为抖动、漂移、穿地。
# 它的 position 与 Body.rotation 由子节点 Sync 同步过来。
camera_rig.queue_free()
_hud.visible = false
set_physics_process(false)
set_process_unhandled_input(false)
return
_yaw = camera_rig.rotation.y
_pitch = clampf(spring_arm.rotation.x, deg_to_rad(pitch_min_deg), deg_to_rad(pitch_max_deg))
spring_arm.rotation.x = _pitch
set_mouse_captured(true)
# 同步可能在 _ready() 之前就把 health 写进来了,那时血条还没刷新,这里补一次
_refresh_health_label()
func _unhandled_input(event: InputEvent) -> void:
if event is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
_yaw -= event.relative.x * mouse_sensitivity
_pitch = clampf(
_pitch - event.relative.y * mouse_sensitivity,
deg_to_rad(pitch_min_deg),
deg_to_rad(pitch_max_deg)
)
camera_rig.rotation.y = _yaw
spring_arm.rotation.x = _pitch
elif event.is_action_pressed("jump"):
_jump_buffer = JUMP_BUFFER_FRAMES
elif event.is_action_pressed("ui_cancel"):
set_mouse_captured(Input.mouse_mode != Input.MOUSE_MODE_CAPTURED)
elif event is InputEventMouseButton and event.pressed and Input.mouse_mode != Input.MOUSE_MODE_CAPTURED:
set_mouse_captured(true)
elif event is InputEventKey and event.pressed and not event.echo and event.keycode == KEY_H:
# 只有权威端能改被同步的属性,别人改的会被覆盖回去
if is_multiplayer_authority():
health -= 10
func set_mouse_captured(captured: bool) -> void:
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED if captured else Input.MOUSE_MODE_VISIBLE
func _physics_process(delta: float) -> void:
if not _is_local:
return
# --- 重力 ---
if is_on_floor():
if velocity.y < 0.0:
velocity.y = 0.0
else:
velocity.y -= _gravity * delta
# --- 跳跃(读取输入缓冲)---
if _jump_buffer > 0:
_jump_buffer -= 1
if is_on_floor():
velocity.y = jump_velocity
_jump_buffer = 0
# --- 相对相机的水平输入 ---
var input_dir := Input.get_vector("move_left", "move_right", "move_back", "move_forward")
var cam_basis := camera_rig.global_transform.basis
var wish := cam_basis.x * input_dir.x + (-cam_basis.z) * input_dir.y
wish.y = 0.0
var wants_move := wish.length() > 0.001
if wants_move:
wish = wish.normalized()
# 只转视觉层,不动本体,也不影响相机(Godot 前进方向为 -Z)
var target_yaw := atan2(-wish.x, -wish.z)
body.rotation.y = lerp_angle(body.rotation.y, target_yaw, clampf(turn_speed * delta, 0.0, 1.0))
var speed := sprint_speed if Input.is_action_pressed("sprint") else walk_speed
if not wants_move:
speed = 0.0
var accel := acceleration if is_on_floor() else air_acceleration
var horizontal := Vector2(velocity.x, velocity.z)
horizontal = horizontal.move_toward(Vector2(wish.x, wish.z) * speed, accel * delta)
velocity.x = horizontal.x
velocity.z = horizontal.y
move_and_slide()
func _refresh_health_label() -> void:
# 血条节点在 player.tscn 里(Hud/HealthLabel)。
# 同步可能在 _ready() 之前就把 health 写进来,那时 _health_label 还是 null,直接跳过。
if _health_label == null:
return
_health_label.text = "HP %d / 100" % health
## 关卡 0 的临时自检:把「我这一端」和「这个节点」分层打印出来。
## 联机框架调通后可以整段删掉(连同 _ready() 里的那一行调用)。
func _report_identity() -> void:
var me: int = multiplayer.get_unique_id()
var authority: int = get_multiplayer_authority()
# 传输层类型:以后换成 Steam 时这里会变成 SteamMultiplayerPeer
var transport := "无"
if multiplayer.multiplayer_peer != null:
transport = multiplayer.multiplayer_peer.get_class()
# 同步配置:字段为空或未绑定是「远端玩家僵在原地」的头号原因,所以顺手打出来
var sync := get_node_or_null("Sync") as MultiplayerSynchronizer
var fields := "未绑定"
if sync != null and sync.replication_config != null:
fields = str(sync.replication_config.get_properties())
print("""┌─ [节点自检] %s
│ 进程级 我是 peer %d | 服务器?%s | 传输 %s | 我知道的 peer:%s
│ 节点级 全路径 %s
│ 节点名 %s | 归属 peer %d | 归我管?%s
│ 同步 Sync 字段 %s
└─ 结论 %s""" % [
name,
me,
multiplayer.is_server(),
transport,
multiplayer.get_peers(),
get_path(),
name,
authority,
is_multiplayer_authority(),
fields,
"这是【我操控】的角色" if is_multiplayer_authority() else "这是【别人】的角色,我只负责回放",
])