diff --git a/.workbuddy-ai/memory/2026-10-02.md b/.workbuddy-ai/memory/2026-10-02.md index cdd7c9a..63630eb 100644 --- a/.workbuddy-ai/memory/2026-10-02.md +++ b/.workbuddy-ai/memory/2026-10-02.md @@ -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。 + diff --git a/character/player/player.gd b/character/player/player.gd index aebeb83..a321b67 100644 --- a/character/player/player.gd +++ b/character/player/player.gd @@ -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) diff --git a/character/player/player.tscn b/character/player/player.tscn index 44a4cf6..fbc1f5f 100644 --- a/character/player/player.tscn +++ b/character/player/player.tscn @@ -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] diff --git a/docs/联机框架使用指南.md b/docs/联机框架使用指南.md index bb8c041..8e6de0b 100644 --- a/docs/联机框架使用指南.md +++ b/docs/联机框架使用指南.md @@ -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 世界里的文字,跟着角色走)