From 414fcafe1df0a553637801b04a4dfd7be122f50c Mon Sep 17 00:00:00 2001 From: yvmou Date: Tue, 22 Sep 2026 17:51:52 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=80=E9=87=8F=E6=98=BE=E7=A4=BA=E5=9C=A8?= =?UTF-8?q?=E7=8E=A9=E5=AE=B6=E5=A4=B4=E9=A1=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .workbuddy/memory/2026-09-22.md | 12 +++++++ docs/联机实战教程-由浅入深.md | 55 ++++++++++++++++++++++----------- docs/联机框架使用指南.md | 3 +- player/player.gd | 13 +++----- player/player.tscn | 13 ++++---- 5 files changed, 61 insertions(+), 35 deletions(-) diff --git a/.workbuddy/memory/2026-09-22.md b/.workbuddy/memory/2026-09-22.md index 1053426..2ddfec4 100644 --- a/.workbuddy/memory/2026-09-22.md +++ b/.workbuddy/memory/2026-09-22.md @@ -77,6 +77,18 @@ - 文档同步:`联机实战教程-由浅入深.md` 关卡 1 改成「先在 player.tscn 里加 Hud/HealthLabel 节点」; `联机框架使用指南.md` 的 player.tscn 结构图。 +### 血条改到玩家头顶(Label3D) + +- `player.tscn`:删掉 `Hud (CanvasLayer)`,改为 `HealthLabel (Label3D)` 直接挂在 `Player` 下, + position `(0, 2.05, 0)`、`billboard=1`、`no_depth_test=true`、`font_size=64`、`pixel_size=0.005`(默认值)、`outline_size=8`。 +- `player.gd`:`@onready var _health_label: Label3D = $HealthLabel`,删掉 `_hud` 引用与 `_hud.visible = false`。 +- **顺带消掉一个坑**:用 `Label3D` 后,血条天然待在各自头顶,不再需要「远端玩家隐藏血条」那一步 + (屏幕坐标的 CanvasLayer 才需要,否则 N 份叠在同一屏幕位置)。 + 现在默认人人可见别人的血量 —— 因为 health 是同步字段。 +- 尺寸换算:`Label3D` 的文字世界高度 ≈ `font_size × pixel_size`。64 × 0.005 ≈ 0.32 米。 +- 若要与关卡 3 的头顶名牌共存:血量放 `y=2.05`,名牌放 `y=2.3`,不重叠。 +- 验证:双实例 headless 联调,两个日志里 `ERROR|Invalid|null` 一条都没有。 + ### 反复出现的坑:编辑器把外部写盘的内容覆盖回去 `net.gd` 用 MCP `script_create` 写成功后,磁盘上又变回了旧版(`extends Node` + `_build_ui` 全在)。 diff --git a/docs/联机实战教程-由浅入深.md b/docs/联机实战教程-由浅入深.md index 67cb8cf..1911a6a 100644 --- a/docs/联机实战教程-由浅入深.md +++ b/docs/联机实战教程-由浅入深.md @@ -93,19 +93,29 @@ func _ready() -> void: **1a. 在 `player.tscn` 里加血条节点** 1. 打开 `res://player/player.tscn`。 -2. 选中根节点 `Player`,添加子节点,类型 **`CanvasLayer`**,改名 `Hud`。 - > `CanvasLayer` 让节点脱离 3D 世界、按屏幕坐标渲染。`Net` 那个菜单也是这么做的。 -3. 选中 `Hud`,添加子节点,类型 `Label`,改名 `HealthLabel`。 -4. 在 Inspector 里设 `HealthLabel` 的 `Position` = `(16, 56)`,`Text` = `HP 100 / 100`。 -5. `Ctrl+S` 保存。 +2. 选中根节点 `Player`,添加子节点,类型 **`Label3D`**,改名 `HealthLabel`。 + > 用 `Label3D` 而不是 `CanvasLayer` + `Label`:`Label3D` 是**3D 世界里的文字**, + > 挂在哪就跟着哪走,所以会自动待在玩家头顶,不需要每帧做屏幕投影。 +3. 在 Inspector 里设: + + | 属性 | 值 | 为什么 | + |---|---|---| + | `Transform → Position` | `(0, 2.05, 0)` | 头顶上方。胶囊高 1.8、中心在 y=0.9,头顶约在 1.8 | + | `Text` | `HP 100 / 100` | 运行时被脚本覆盖,这里只是编辑器预览 | + | `Billboard` | `Enabled` | 永远面朝相机。**不开的话文字会跟着角色转,从背后看是镜像的** | + | `No Depth Test` | 开 | 被别的物体挡住时也能看见 | + | `Font Size` | `64` | | + | `Pixel Size` | `0.005` | 字号→世界单位:64 × 0.005 ≈ 0.32 米高 | + | `Outline Size` | `8` | 描边,远看和杂乱背景上也能读 | + +4. `Ctrl+S` 保存。 层级应该是: ``` Player (CharacterBody3D) ├─ Collision / Body / CameraRig / Sync -└─ Hud (CanvasLayer) - └─ HealthLabel (Label) +└─ HealthLabel (Label3D) ← 头顶血条 ``` **1b. 在 `player.gd` 里加属性和节点引用** @@ -118,34 +128,40 @@ Player (CharacterBody3D) health = clampi(value, 0, 100) _refresh_health_label() -@onready var _hud: CanvasLayer = $Hud -@onready var _health_label: Label = $Hud/HealthLabel +## 头顶血条是 player.tscn 里的 Label3D 节点(3D 世界里的文字,会跟着角色走) +@onready var _health_label: Label3D = $HealthLabel ``` `clampi(value, 0, 100)` 把值夹在 0~100,省得到处写边界判断。 -**1c. 处理远端玩家** +**1c. 远端玩家的血条要不要显示?** -血条节点现在**每个玩家实例上都有**(因为它是场景的一部分)。远端玩家不需要显示,所以在 `_ready()` 的远端分支里关掉: +因为血条挂在**每个玩家节点**上,而 `health` 是同步过来的,所以默认行为是 +**每个人的血量都显示在他自己头顶**,你能看到别人还剩多少血。这通常正是想要的效果,不用额外写代码。 + +> 对比一下两种做法:如果血条是**屏幕坐标**的 `CanvasLayer` + `Label`(像左上角那种), +> 就必须在远端分支里 `hide()` 掉,否则 N 份血条会叠在同一个屏幕位置、显示别人的血量。 +> 换成 `Label3D` 之后这个坑自然消失了 —— 这也是「让 UI 待在它该待的世界位置」的好处。 + +如果你**只想看自己的**,在 `_ready()` 的远端分支里加一行: ```gdscript -func _ready() -> void: - _is_local = is_multiplayer_authority() if not _is_local: camera_rig.queue_free() - _hud.visible = false # ← 血条同理:N 份叠在同一位置会显示错的血量 + _health_label.visible = false # ← 只在需要「只看自己」时加 set_physics_process(false) set_process_unhandled_input(false) return +``` + +本地分支末尾记得补刷一次: + +```gdscript # ...相机初始化 set_mouse_captured(true) _refresh_health_label() # 补刷一次(原因见下面的提示) ``` -> **这一步千万别漏。** 血条节点的坐标是屏幕坐标,如果没有 `_hud.visible = false`, -> 那么每个玩家(包括别人)都会在左上角画一份血条,**全部重叠在同一位置**, -> 你看到的是最后生成的那个玩家的血量。现象是:主机按 `H` 扣自己的血,屏幕上的数字**纹丝不动**。 - **1d. 刷新函数** 放在文件底部: @@ -163,6 +179,9 @@ func _refresh_health_label() -> void: > (节点刚进场景树时)。那一次 setter 会因为 `_health_label == null` 被跳过, > 所以本地分支末尾要补一次 `_refresh_health_label()`,把当前值显示出来。 +> **和关卡 3 的名牌不冲突**:关卡 3 会在 `(0, 2.3, 0)` 加一个 `Label3D` 名牌, +> 血量放在 `2.05`,名牌放它上面,两个叠不到一起。 + ### 第 2 步:把 health 加进同步配置 打开 `player/player_sync.tres`,在末尾追加三行: diff --git a/docs/联机框架使用指南.md b/docs/联机框架使用指南.md index 4da72ba..bb8c041 100644 --- a/docs/联机框架使用指南.md +++ b/docs/联机框架使用指南.md @@ -90,8 +90,7 @@ E:\GD_Projects\agame\ │ │ ├─ CameraRig → SpringArm → Camera ← 远端玩家会被删掉这一支 │ │ ├─ Body → Mesh ← 视觉层,换角色模型挂这里 │ │ ├─ Sync (MultiplayerSynchronizer) ← 负责同步位置和朝向 -│ │ └─ Hud (CanvasLayer) ← 本地玩家的血条,远端玩家会被隐藏 -│ │ └─ HealthLabel (Label) +│ │ └─ HealthLabel (Label3D) ← 头顶血条(3D 世界里的文字,跟着角色走) │ ├── player.gd ← 加了「本地 / 远端」分支 │ └── player_sync.tres ← 同步配置:到底同步哪几个属性 └── tests\ diff --git a/player/player.gd b/player/player.gd index b7475cc..8d58540 100644 --- a/player/player.gd +++ b/player/player.gd @@ -10,8 +10,7 @@ extends CharacterBody3D ## │ └─ SpringArm (SpringArm3D) ← 承担俯仰 pitch,自动避障 ## │ └─ Camera (Camera3D) ## ├─ Sync (MultiplayerSynchronizer) ← 同步 position 与 Body.rotation -## └─ Hud (CanvasLayer) ← 本地玩家的血条(节点配置,不是代码建的) -## └─ HealthLabel (Label) +## └─ HealthLabel (Label3D) ← 头顶血条(节点配置,不是代码建的) ## ## 操作:WASD/方向键移动,鼠标转视角,Shift 疾跑,空格跳跃,Esc 释放/锁定鼠标 @@ -50,9 +49,8 @@ const JUMP_BUFFER_FRAMES: int = 6 @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 +## 头顶血条是 player.tscn 里的 Label3D 节点(3D 世界里的文字,会跟着角色走) +@onready var _health_label: Label3D = $HealthLabel var _gravity: float = float(ProjectSettings.get_setting("physics/3d/default_gravity", 9.8)) var _yaw: float = 0.0 @@ -68,11 +66,10 @@ func _ready() -> void: if not _is_local: # 远端玩家只是一具「回放身体」: # 拆掉相机 —— 否则每多一个玩家就多一台 Camera3D 互相抢渲染; - # 关掉血条 —— 否则 N 份血条叠在同一位置,显示的血量可能是别人的; # 关掉输入与物理 —— 否则与权威端双重模拟,表现为抖动、漂移、穿地。 - # 它的 position 与 Body.rotation 由子节点 Sync 同步过来。 + # 它的 position 与 Body.rotation 由子节点 Sync 同步过来; + # 头顶血条保留 —— health 同样是同步过来的,这样谁都能看到别人还剩多少血。 camera_rig.queue_free() - _hud.visible = false set_physics_process(false) set_process_unhandled_input(false) return diff --git a/player/player.tscn b/player/player.tscn index d1eb740..4ae6b4c 100644 --- a/player/player.tscn +++ b/player/player.tscn @@ -44,11 +44,10 @@ surface_material_override/0 = SubResource("StandardMaterial3D_yw30f") [node name="Sync" type="MultiplayerSynchronizer" parent="." unique_id=1979575210] replication_config = ExtResource("2_rkbax") -[node name="Hud" type="CanvasLayer" parent="." unique_id=425326662] - -[node name="HealthLabel" type="Label" parent="Hud" unique_id=348125801] -offset_left = 16.0 -offset_top = 56.0 -offset_right = 17.0 -offset_bottom = 79.0 +[node name="HealthLabel" type="Label3D" parent="." unique_id=300336561] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.05, 0) +billboard = 1 +no_depth_test = true text = "HP 100 / 100" +font_size = 64 +outline_size = 8