改为第一人称控制器
This commit is contained in:
@@ -66,3 +66,42 @@ Ctrl+Shift→仍是 SNEAK、空中→JUMP/FALL 优先于蹲),片段也确认
|
||||
**未做**:蹲下目前不改移动速度、也不改碰撞胶囊高度 —— 潜行时用走速(5 m/s)播 sneak 片段,
|
||||
`speed_scale` 会被 clamp 到上限 1.6。要不要减速/压低碰撞盒等用户定。
|
||||
|
||||
## 重做:第三人称 → 第一人称控制器
|
||||
|
||||
`player.gd` + `player.tscn` 改造。`test_run` 21/21,实机确认 `CameraRig.position.y=1.6`、`Body.visible=false`。
|
||||
|
||||
- **场景**:删掉 `SpringArm`,`Camera` 从 `CameraRig/SpringArm/Camera` 挪到 `CameraRig/Camera`。
|
||||
`CameraRig` 变成「眼睛」,架在 `eye_height` 高,只转 yaw;pitch 在 `Camera` 自身上原地抬头低头。
|
||||
`fov` 82.2 → 75(82 是给第三人称的,第一人称太鱼眼)。
|
||||
- **`player.gd` 新增两个导出**:`eye_height`(1.6,按 1.8 胶囊定,与模型高矮无关)、
|
||||
`hide_own_body`(true,只藏本机那一份,远端照常显示)。不藏的话相机就在模型里/贴头顶。
|
||||
- **删掉 `turn_speed`**:第一人称里身体不再朝移动方向转身,改成始终跟视线一致(`_apply_look()` 里
|
||||
`body.rotation.y = _yaw`),这个平滑参数没用了。
|
||||
- **新增 `_apply_look()`**(3 处调用:鼠标、滑索、_ready):鼠标一动就落节点,视角跟手不等物理帧。
|
||||
- pitch 范围从 -80/45 放到 ±89(第一人称要能抬头低头)。滑索的视角对齐沿用原来的 3.0 慢速率。
|
||||
|
||||
**已知遗留**:`HealthLabel`(y=2.05) 和 `InteractHint`(y=1.75) 是玩家自己位置的 Label3D,
|
||||
第一人称下平视看不到(都在相机所在的竖直平面上,超出 FOV),但抬头会看到自己的血条 ——
|
||||
要干净的话得改成 CanvasLayer 的 HUD(会连带改 `test_zipline` 里断言 `Player/InteractHint` 的那条)。
|
||||
|
||||
**MCP 限制**:`game_manage(op="input_mouse")` 注入的 `InputEventMouseMotion` 不带 `relative` 增量,
|
||||
所以鼠标转视角没法用 MCP 验证(`input_key`/`input_action` 是正常的,用 `H` 键把血条打到 90 验证过)。
|
||||
|
||||
## 方案:面部贴图切换 + 游戏内自绘(只出方案,未写文件)
|
||||
|
||||
用户给了 `mouse_表情测试.png`,问怎么在游戏内切面部贴图、并让玩家自己画。
|
||||
|
||||
**先探了模型**(临时探针读各 mesh 的 surface/UV/材质,跑完删了):面部早就被拆成了独立
|
||||
surface —— `body_mesh s1 "face"`,UV 孤岛 u[0.142,0.356] v[0.038,0.238],自带材质和贴图。
|
||||
所以**不用改 Blender**,直接 `set_surface_override_material(1, ...)`。
|
||||
|
||||
给的方案:面部 shader(把孤岛 UV 重映射到 0~1,`ALBEDO = mix(base_tex, paint_tex, paint.a)`)
|
||||
+ `base_tex` = 预设方形 PNG(切换就换它)+ `paint_tex` = 一个 512² 透明 SubViewport 的**笔画层**。
|
||||
画布只装笔画不装底图,所以橡皮擦掉笔画露出来的是预设脸,语义正确。
|
||||
|
||||
关键设计点(也是给之前那份第三方方案纠错的地方):**绘画 UI 是可见的 `CanvasLayer +
|
||||
SubViewportContainer`,输入天然可达** —— 不是把 SubViewport 当后台渲染目标(那样 `_input` 收不到事件)。
|
||||
|
||||
遗留待用户拍板:现有预设要不要裁成方形(重映射后必须裁,1024² 里裁 x145..365 y32..251);
|
||||
联机时脸要不要 RPC 同步合成后的 PNG。
|
||||
|
||||
|
||||
+44
-32
@@ -1,16 +1,15 @@
|
||||
extends CharacterBody3D
|
||||
## 第三人称控制器:只管「怎么动」—— 输入、移动物理、相机、血条、联机分叉。
|
||||
## 第一人称控制器:只管「怎么动」—— 输入、移动物理、视角、血条、联机分叉。
|
||||
## 表现层(动画)在 Body/AnimationPlayer 上(挂 player_animator.gd),本文件不认识任何片段名。
|
||||
##
|
||||
## 场景结构:
|
||||
## Player (CharacterBody3D) ← 只负责移动与碰撞,自身不旋转
|
||||
## ├─ Collision (CollisionShape3D, CapsuleShape3D)
|
||||
## ├─ Body (Node3D) ← 视觉层,朝移动方向转身;换角色模型时挂在它下面
|
||||
## ├─ CameraRig (Node3D) ← 眼睛,架在 eye_height 高,承担偏航 yaw
|
||||
## │ └─ Camera (Camera3D) ← 承担俯仰 pitch(第一人称,没有弹簧臂)
|
||||
## ├─ Body (Node3D) ← 视觉层,始终朝视线方向;换角色模型时挂在它下面
|
||||
## │ ├─ mouse (Node3D) ← 模型(mouse.glb):MouseRig/Skeleton3D + 各部位 MeshInstance3D
|
||||
## │ └─ AnimationPlayer ← 动作库 + 表现层(挂 player_animator.gd):只读运动、只写动画
|
||||
## ├─ CameraRig (Node3D) ← 承担偏航 yaw,独立于本体旋转
|
||||
## │ └─ SpringArm (SpringArm3D) ← 承担俯仰 pitch,自动避障
|
||||
## │ └─ Camera (Camera3D)
|
||||
## ├─ Sync (MultiplayerSynchronizer) ← 同步 position、Body.rotation、health、动画状态
|
||||
## ├─ HealthLabel (Label3D) ← 头顶血条(节点配置,不是代码建的)
|
||||
## └─ InteractHint (Label3D) ← 靠近滑索时的「按 E」提示,同样在场景里配好
|
||||
@@ -30,6 +29,20 @@ const JUMP_BUFFER_FRAMES: int = 6
|
||||
health = clampi(value, 0, 100)
|
||||
_refresh_health_label()
|
||||
|
||||
@export_group("视角")
|
||||
## 眼睛高度(米)。相机就架在这个高度上 —— 按碰撞胶囊定,跟模型高矮无关。
|
||||
@export var eye_height: float = 1.6
|
||||
## 第一人称下藏掉自己的模型。不藏的话相机要么在模型里、要么贴着头顶,画面糊一片。
|
||||
## 想做成「看得见自己身体」的第一人称,就把这个关掉、并把 eye_height 降到模型头部高度。
|
||||
## 只影响本机这一份 —— 远端玩家的模型照常显示。
|
||||
@export var hide_own_body: bool = true
|
||||
## 鼠标灵敏度(弧度/像素)
|
||||
@export var mouse_sensitivity: float = 0.0025
|
||||
## 最低俯角(向下看)。第一人称得能抬头低头,所以放到接近 ±90°
|
||||
@export var pitch_min_deg: float = -89.0
|
||||
## 最高仰角(向上看)
|
||||
@export var pitch_max_deg: float = 89.0
|
||||
|
||||
@export_group("移动")
|
||||
## 常规移动速度(米/秒)
|
||||
@export var walk_speed: float = 5.0
|
||||
@@ -41,20 +54,12 @@ const JUMP_BUFFER_FRAMES: int = 6
|
||||
@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
|
||||
## 眼睛分两层转:CameraRig 绕竖直轴转(yaw),Camera 原地抬头低头(pitch)。
|
||||
## 分开是为了让 pitch 不掺进移动方向 —— 移动只用 rig 的 basis,低头看地也不会走歪。
|
||||
@onready var camera_rig: Node3D = $CameraRig
|
||||
@onready var spring_arm: SpringArm3D = $CameraRig/SpringArm
|
||||
@onready var camera: Camera3D = $CameraRig/Camera
|
||||
## 头顶血条是 player.tscn 里的 Label3D 节点(3D 世界里的文字,会跟着角色走)
|
||||
@onready var _health_label: Label3D = $HealthLabel
|
||||
## 滑索操作提示,同样在 player.tscn 里配好,默认隐藏
|
||||
@@ -97,9 +102,12 @@ func _ready() -> void:
|
||||
set_physics_process(false)
|
||||
set_process_unhandled_input(false)
|
||||
return
|
||||
camera_rig.position.y = eye_height
|
||||
if hide_own_body:
|
||||
body.visible = false
|
||||
_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
|
||||
_pitch = clampf(camera.rotation.x, deg_to_rad(pitch_min_deg), deg_to_rad(pitch_max_deg))
|
||||
_apply_look()
|
||||
set_mouse_captured(true)
|
||||
# 同步可能在 _ready() 之前就把 health 写进来了,那时血条还没刷新,这里补一次
|
||||
_refresh_health_label()
|
||||
@@ -113,8 +121,7 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||
deg_to_rad(pitch_min_deg),
|
||||
deg_to_rad(pitch_max_deg)
|
||||
)
|
||||
camera_rig.rotation.y = _yaw
|
||||
spring_arm.rotation.x = _pitch
|
||||
_apply_look()
|
||||
elif event.is_action_pressed("jump"):
|
||||
# 缆上按跳 = 主动脱手,额外给一个向上的小跳,方便够到旁边的平台
|
||||
if _zipline != null:
|
||||
@@ -140,6 +147,14 @@ func set_mouse_captured(captured: bool) -> void:
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED if captured else Input.MOUSE_MODE_VISIBLE
|
||||
|
||||
|
||||
## 把 _yaw / _pitch 落到节点上。鼠标一动就调一次(渲染帧),所以视角是跟手的、不等物理帧。
|
||||
## 身体跟着一起转 —— 第一人称里「你看哪,身体就朝哪」,别人看你也是这个朝向。
|
||||
func _apply_look() -> void:
|
||||
camera_rig.rotation.y = _yaw
|
||||
camera.rotation.x = _pitch
|
||||
body.rotation.y = _yaw
|
||||
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
if not _is_local:
|
||||
return
|
||||
@@ -169,18 +184,17 @@ func _physics_process(delta: float) -> void:
|
||||
velocity.y = jump_velocity
|
||||
_jump_buffer = 0
|
||||
|
||||
# --- 相对相机的水平输入 ---
|
||||
# --- 相对视线方向的水平输入 ---
|
||||
# 只用 rig 的 basis(只有 yaw),低头看地时前进方向不会跟着往下扎。
|
||||
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
|
||||
var look_basis := camera_rig.global_transform.basis
|
||||
var wish := look_basis.x * input_dir.x + (-look_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))
|
||||
# 身体朝向由 _apply_look() 跟着视线走 —— 第一人称不再朝移动方向转身
|
||||
|
||||
var speed := sprint_speed if Input.is_action_pressed("sprint") else walk_speed
|
||||
if not wants_move:
|
||||
@@ -280,12 +294,10 @@ func _ride_zipline(delta: float) -> void:
|
||||
_zip_grab_offset = _zip_grab_offset.lerp(Vector3.ZERO, clampf(delta / 0.12, 0.0, 1.0))
|
||||
global_position = _zipline.ride_point(_zip_t) + _zip_grab_offset
|
||||
velocity = tan_dir * _zip_speed
|
||||
var travel_yaw := atan2(-tan_dir.x, -tan_dir.z)
|
||||
body.rotation.y = lerp_angle(body.rotation.y, travel_yaw, clampf(turn_speed * delta, 0.0, 1.0))
|
||||
# 相机也跟着摆到行进方向背后 —— 否则人是横着飞的,看不见前面要去哪。
|
||||
# 速率刻意比转身慢:玩家用鼠标看的视角不会被立刻拽回去。
|
||||
_yaw = lerp_angle(_yaw, travel_yaw, clampf(3.0 * delta, 0.0, 1.0))
|
||||
camera_rig.rotation.y = _yaw
|
||||
# 视线跟着摆到行进方向 —— 否则人是横着飞的,看不见前面要去哪。
|
||||
# 速率刻意放慢:玩家自己用鼠标看的视角不会被一下子拽回去。
|
||||
_yaw = lerp_angle(_yaw, atan2(-tan_dir.x, -tan_dir.z), clampf(3.0 * delta, 0.0, 1.0))
|
||||
_apply_look()
|
||||
# 缆上没有「落地」概念,交给动画器当下落姿态处理(模型里没有专门的滑索片段)
|
||||
animator.set_motion(velocity, false, false)
|
||||
|
||||
|
||||
@@ -24,15 +24,10 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.9, 0)
|
||||
shape = SubResource("CapsuleShape3D_rkbax")
|
||||
|
||||
[node name="CameraRig" type="Node3D" parent="." unique_id=1774966304]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.45, 0)
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.6, 0)
|
||||
|
||||
[node name="SpringArm" type="SpringArm3D" parent="CameraRig" unique_id=917938711]
|
||||
transform = Transform3D(1, 0, 0, 0, 0.9659257, 0.25881964, 0, -0.25881964, 0.9659257, 0, 0, 0)
|
||||
spring_length = 4.0
|
||||
|
||||
[node name="Camera" type="Camera3D" parent="CameraRig/SpringArm" unique_id=52294557]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 4)
|
||||
fov = 82.2
|
||||
[node name="Camera" type="Camera3D" parent="CameraRig" unique_id=52294557]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.6066488, -0.11179829)
|
||||
|
||||
[node name="Body" type="Node3D" parent="." unique_id=394283421]
|
||||
|
||||
|
||||
+1
-1
@@ -87,7 +87,7 @@ E:\GD_Projects\agame\
|
||||
│ ├── player.tscn ← 玩家场景
|
||||
│ │ Player (CharacterBody3D)
|
||||
│ │ ├─ Collision
|
||||
│ │ ├─ CameraRig → SpringArm → Camera ← 远端玩家会被删掉这一支
|
||||
│ │ ├─ CameraRig → Camera ← 第一人称,远端玩家会被删掉这一支
|
||||
│ │ ├─ Body → Mesh ← 视觉层,换角色模型挂这里
|
||||
│ │ ├─ Sync (MultiplayerSynchronizer) ← 负责同步位置和朝向
|
||||
│ │ └─ HealthLabel (Label3D) ← 头顶血条(3D 世界里的文字,跟着角色走)
|
||||
|
||||
Reference in New Issue
Block a user