Update 联机实战教程-由浅入深.md
This commit is contained in:
+55
-32
@@ -326,27 +326,28 @@ func _refresh_nameplate() -> void:
|
||||
```gdscript
|
||||
func _ready() -> void:
|
||||
_is_local = is_multiplayer_authority()
|
||||
_make_nameplate() # ← 名牌是所有玩家都要显示的东西
|
||||
_make_nameplate() # ← 名牌是所有玩家都要显示的东西,要放在 is_local 判断之前
|
||||
if not _is_local:
|
||||
camera_rig.queue_free()
|
||||
...
|
||||
set_physics_process(false)
|
||||
set_process_unhandled_input(false)
|
||||
return
|
||||
# 只有自己给自己起名
|
||||
nickname = get_node_or_null("/root/Net").player_name if ...
|
||||
```
|
||||
|
||||
最后那行写清楚点,用工具函数更好:
|
||||
|
||||
```gdscript
|
||||
# 下面几行是前面几关攒下来的,只有本地玩家会执行
|
||||
nickname = _read_player_name()
|
||||
```
|
||||
_make_health_label()
|
||||
body_color = Color.from_hsv(randf(), 0.65, 0.9)
|
||||
set_mouse_captured(true)
|
||||
|
||||
```gdscript
|
||||
|
||||
## 从 Net 单例读菜单里填的名字。用节点路径访问,不依赖 autoload 全局名
|
||||
func _read_player_name() -> String:
|
||||
var net := get_node_or_null("/root/Net")
|
||||
if net == null or net.player_name.is_empty():
|
||||
if net == null:
|
||||
return "玩家 " + name
|
||||
var typed := str(net.get("player_name"))
|
||||
if typed.is_empty():
|
||||
return "玩家 " + name # 没填就用节点名(= peer id)
|
||||
return net.player_name
|
||||
return typed
|
||||
```
|
||||
|
||||
> **为什么用 `/root/Net` 而不是直接写 `Net`?**
|
||||
@@ -614,7 +615,7 @@ A 按 K
|
||||
├─ is_server() ✓
|
||||
├─ 发起者是不是 B 节点的 owner?✓
|
||||
├─ 距离 ≤ 3 米?✓
|
||||
└─ parent.broadcast_hit.rpc("B", 10) ← 广播给所有人
|
||||
└─ world.broadcast_hit.rpc("B", 10) ← 广播给所有人
|
||||
│
|
||||
├─ A 的机器:找到 B 节点 → receive_damage() → B 不是本地的 → 什么都不做
|
||||
├─ B 的机器:找到 B 节点 → receive_damage() → B 是本地的 → health -= 10
|
||||
@@ -922,22 +923,38 @@ func _create_player(peer_id: int) -> Node:
|
||||
|
||||
### 第 5 步:服务器直接改血量
|
||||
|
||||
现在 `health` 的权威端是服务器了,所以服务器可以直接写它,不需要再让「受害者自己执行」。
|
||||
现在 `health` 的权威端是服务器了,服务器写它的值就会自动同步给所有人,客户端改了也会被覆盖回来。
|
||||
|
||||
把 `player.gd` 里的 `receive_damage()` 删掉,并修改 `main.gd` 的广播:
|
||||
在 `player.gd` 里,把关卡 5 写的 `receive_damage()` **换成**这个——判断条件从「我是不是本地玩家」变成了「我是不是服务器」:
|
||||
|
||||
```gdscript
|
||||
## 扣血入口。现在只有服务器会调它 —— health 的权威端就是服务器
|
||||
func apply_server_damage(amount: int) -> void:
|
||||
if not multiplayer.is_server():
|
||||
return
|
||||
health = maxi(0, health - amount)
|
||||
```
|
||||
|
||||
`world/main.gd` 的广播相应改成:
|
||||
|
||||
```gdscript
|
||||
@rpc("authority", "call_local", "reliable")
|
||||
func broadcast_hit(target_name: String, amount: int) -> void:
|
||||
var target := get_node_or_null(target_name)
|
||||
var target := get_node_or_null(target_name) as PlayerController
|
||||
if target == null:
|
||||
return
|
||||
# 服务器直接改。因为 health 的权威端就是服务器,
|
||||
# 这个值会自动同步给所有人,客户端改了也会被覆盖回来
|
||||
target.health = target.health - amount
|
||||
# 服务器直接扣。玩家自己的机器现在改不动这个值了
|
||||
target.apply_server_damage(amount)
|
||||
```
|
||||
|
||||
注意这里是 `target.health = ...`,**不是** `target.receive_damage(...)`。现在所有权在服务器手上,服务器说了算。
|
||||
**这一关的全部意义就是判断条件从 `_is_local` 变成了 `is_server()`:**
|
||||
|
||||
| | 关卡 5 | 关卡 7 |
|
||||
|---|---|---|
|
||||
| `health` 的权威端 | 玩家自己 | 服务器 |
|
||||
| 谁执行扣血 | 受害者的客户端 | 服务器 |
|
||||
| 客户端能偷偷改血量吗 | 能(作弊) | 不能(会被服务器覆盖) |
|
||||
| 谁来判定命中 | 攻击方的客户端 | 服务器 |
|
||||
|
||||
### 第 6 步:命中判定(子弹打中人)
|
||||
|
||||
@@ -956,8 +973,8 @@ func _physics_process(delta: float) -> void:
|
||||
if multiplayer.is_server():
|
||||
var victim := _find_hit()
|
||||
if victim != null:
|
||||
var world := get_parent().get_parent() # Bullets → Main
|
||||
if world != null and world.has_method("broadcast_hit"):
|
||||
var world := get_tree().current_scene as GameWorld
|
||||
if world != null:
|
||||
world.broadcast_hit.rpc(victim.name, DAMAGE)
|
||||
queue_free()
|
||||
return
|
||||
@@ -966,19 +983,22 @@ func _physics_process(delta: float) -> void:
|
||||
queue_free()
|
||||
|
||||
|
||||
func _find_hit() -> Node3D:
|
||||
func _find_hit() -> PlayerController:
|
||||
for node in get_tree().get_nodes_in_group("net_player"):
|
||||
if node.name == str(owner_peer):
|
||||
var candidate := node as PlayerController
|
||||
if candidate == null:
|
||||
continue
|
||||
if candidate.name == str(owner_peer):
|
||||
continue # 别打自己
|
||||
# 用「到角色胶囊中心」的距离粗略判定,够用且不受碰撞层影响
|
||||
var center: Vector3 = node.global_position + Vector3(0, 0.9, 0)
|
||||
var center: Vector3 = candidate.global_position + Vector3(0, 0.9, 0)
|
||||
if center.distance_to(global_position) < HIT_RADIUS:
|
||||
return node
|
||||
return candidate
|
||||
return null
|
||||
```
|
||||
|
||||
> 用距离判定而不是物理碰撞,是因为子弹和玩家都不需要额外的碰撞层配置,
|
||||
> 而且**判定只发生在服务器**,天然避开了「各端物理结果不同步」的问题。
|
||||
> `get_tree().current_scene` 就是主场景的根节点(本项目的 `Main`),比 `get_parent().get_parent()` 更稳,
|
||||
> 少一个「层级一变就断」的隐患。
|
||||
|
||||
### 验证
|
||||
|
||||
@@ -1161,10 +1181,13 @@ var _player_list: Label = null
|
||||
func _refresh_player_list() -> void:
|
||||
if _player_list == null or _hud_root == null or not _hud_root.visible:
|
||||
return
|
||||
var lines: Array[String] = []
|
||||
# 用 PackedStringArray 而不是 Array[String]:
|
||||
# String.join() 要的就是 PackedStringArray,传 Array[String] 会在运行时转换失败
|
||||
var lines := PackedStringArray()
|
||||
for node in get_tree().get_nodes_in_group(PLAYER_GROUP):
|
||||
# 名字是同步过来的字段,所以这里读到的是「那个玩家自己起的名字」
|
||||
var display: String = str(node.get("nickname"))
|
||||
# 名字是同步过来的字段,所以这里读到的是「那个玩家自己起的名字」。
|
||||
# 用 get() 而不是 node.nickname:node 是 Node 类型,静态访问拿不到脚本变量
|
||||
var display := str(node.get("nickname"))
|
||||
if display.is_empty():
|
||||
display = "玩家 " + str(node.name)
|
||||
lines.append("· %s(peer %s)" % [display, node.name])
|
||||
|
||||
Reference in New Issue
Block a user