Update 联机实战教程-由浅入深.md
This commit is contained in:
+55
-32
@@ -326,27 +326,28 @@ func _refresh_nameplate() -> void:
|
|||||||
```gdscript
|
```gdscript
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
_is_local = is_multiplayer_authority()
|
_is_local = is_multiplayer_authority()
|
||||||
_make_nameplate() # ← 名牌是所有玩家都要显示的东西
|
_make_nameplate() # ← 名牌是所有玩家都要显示的东西,要放在 is_local 判断之前
|
||||||
if not _is_local:
|
if not _is_local:
|
||||||
camera_rig.queue_free()
|
camera_rig.queue_free()
|
||||||
...
|
set_physics_process(false)
|
||||||
|
set_process_unhandled_input(false)
|
||||||
return
|
return
|
||||||
# 只有自己给自己起名
|
# 下面几行是前面几关攒下来的,只有本地玩家会执行
|
||||||
nickname = get_node_or_null("/root/Net").player_name if ...
|
|
||||||
```
|
|
||||||
|
|
||||||
最后那行写清楚点,用工具函数更好:
|
|
||||||
|
|
||||||
```gdscript
|
|
||||||
nickname = _read_player_name()
|
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:
|
func _read_player_name() -> String:
|
||||||
var net := get_node_or_null("/root/Net")
|
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 "玩家 " + name # 没填就用节点名(= peer id)
|
||||||
return net.player_name
|
return typed
|
||||||
```
|
```
|
||||||
|
|
||||||
> **为什么用 `/root/Net` 而不是直接写 `Net`?**
|
> **为什么用 `/root/Net` 而不是直接写 `Net`?**
|
||||||
@@ -614,7 +615,7 @@ A 按 K
|
|||||||
├─ is_server() ✓
|
├─ is_server() ✓
|
||||||
├─ 发起者是不是 B 节点的 owner?✓
|
├─ 发起者是不是 B 节点的 owner?✓
|
||||||
├─ 距离 ≤ 3 米?✓
|
├─ 距离 ≤ 3 米?✓
|
||||||
└─ parent.broadcast_hit.rpc("B", 10) ← 广播给所有人
|
└─ world.broadcast_hit.rpc("B", 10) ← 广播给所有人
|
||||||
│
|
│
|
||||||
├─ A 的机器:找到 B 节点 → receive_damage() → B 不是本地的 → 什么都不做
|
├─ A 的机器:找到 B 节点 → receive_damage() → B 不是本地的 → 什么都不做
|
||||||
├─ B 的机器:找到 B 节点 → receive_damage() → B 是本地的 → health -= 10
|
├─ B 的机器:找到 B 节点 → receive_damage() → B 是本地的 → health -= 10
|
||||||
@@ -922,22 +923,38 @@ func _create_player(peer_id: int) -> Node:
|
|||||||
|
|
||||||
### 第 5 步:服务器直接改血量
|
### 第 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
|
```gdscript
|
||||||
@rpc("authority", "call_local", "reliable")
|
@rpc("authority", "call_local", "reliable")
|
||||||
func broadcast_hit(target_name: String, amount: int) -> void:
|
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:
|
if target == null:
|
||||||
return
|
return
|
||||||
# 服务器直接改。因为 health 的权威端就是服务器,
|
# 服务器直接扣。玩家自己的机器现在改不动这个值了
|
||||||
# 这个值会自动同步给所有人,客户端改了也会被覆盖回来
|
target.apply_server_damage(amount)
|
||||||
target.health = target.health - amount
|
|
||||||
```
|
```
|
||||||
|
|
||||||
注意这里是 `target.health = ...`,**不是** `target.receive_damage(...)`。现在所有权在服务器手上,服务器说了算。
|
**这一关的全部意义就是判断条件从 `_is_local` 变成了 `is_server()`:**
|
||||||
|
|
||||||
|
| | 关卡 5 | 关卡 7 |
|
||||||
|
|---|---|---|
|
||||||
|
| `health` 的权威端 | 玩家自己 | 服务器 |
|
||||||
|
| 谁执行扣血 | 受害者的客户端 | 服务器 |
|
||||||
|
| 客户端能偷偷改血量吗 | 能(作弊) | 不能(会被服务器覆盖) |
|
||||||
|
| 谁来判定命中 | 攻击方的客户端 | 服务器 |
|
||||||
|
|
||||||
### 第 6 步:命中判定(子弹打中人)
|
### 第 6 步:命中判定(子弹打中人)
|
||||||
|
|
||||||
@@ -956,8 +973,8 @@ func _physics_process(delta: float) -> void:
|
|||||||
if multiplayer.is_server():
|
if multiplayer.is_server():
|
||||||
var victim := _find_hit()
|
var victim := _find_hit()
|
||||||
if victim != null:
|
if victim != null:
|
||||||
var world := get_parent().get_parent() # Bullets → Main
|
var world := get_tree().current_scene as GameWorld
|
||||||
if world != null and world.has_method("broadcast_hit"):
|
if world != null:
|
||||||
world.broadcast_hit.rpc(victim.name, DAMAGE)
|
world.broadcast_hit.rpc(victim.name, DAMAGE)
|
||||||
queue_free()
|
queue_free()
|
||||||
return
|
return
|
||||||
@@ -966,19 +983,22 @@ func _physics_process(delta: float) -> void:
|
|||||||
queue_free()
|
queue_free()
|
||||||
|
|
||||||
|
|
||||||
func _find_hit() -> Node3D:
|
func _find_hit() -> PlayerController:
|
||||||
for node in get_tree().get_nodes_in_group("net_player"):
|
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 # 别打自己
|
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:
|
if center.distance_to(global_position) < HIT_RADIUS:
|
||||||
return node
|
return candidate
|
||||||
return null
|
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:
|
func _refresh_player_list() -> void:
|
||||||
if _player_list == null or _hud_root == null or not _hud_root.visible:
|
if _player_list == null or _hud_root == null or not _hud_root.visible:
|
||||||
return
|
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):
|
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():
|
if display.is_empty():
|
||||||
display = "玩家 " + str(node.name)
|
display = "玩家 " + str(node.name)
|
||||||
lines.append("· %s(peer %s)" % [display, node.name])
|
lines.append("· %s(peer %s)" % [display, node.name])
|
||||||
|
|||||||
Reference in New Issue
Block a user