前言
在联机模拟或游戏中,我们经常需要处理"区域触发"。例如:一名学员进入了高压危险区,所有人的屏幕上都要弹出红光警告。你可能会想:直接在 OnTriggerEnter 里写逻辑不就行了?但在多人环境下,如果处理不当,会出现"只有触发者看到了特效"或者"每个人都触发了一次导致逻辑重叠"的情况。今天我们就通过解析官方 NetworkTrigger 脚本,学习如何优雅地处理全局事件。

一、 官方代码逻辑深度剖析
我们先看这段核心逻辑的精妙之处:
csharp
void OnTriggerEnter(Collider other)
{
// 关键点 1:本地过滤
if (other.TryGetComponent(out XROrigin origin))
{
// 只有【本地玩家】走进区域时,才发起通知
TriggerRpc(NetworkManager.Singleton.LocalClientId);
}
}
[Rpc(SendTo.Everyone)] // 关键点 2:全员广播
void TriggerRpc(ulong clientId)
{
// 关键点 3:本地响应
m_NetworkedTriggerUnityEvent?.Invoke(clientId);
}
1.1 为什么一定要做"本地过滤"?
在多人场景中,会有多个玩家。如果 A 走进了区域,A 的客户端会检测到碰撞,B 的客户端也会检测到"玩家 A 进入了区域"。如果不判断 XROrigin(本地玩家),那么当 A 走进去时,A 会发一次 Rpc,B 也会替 A 发一次 Rpc。结果就是:一个事件被触发了 N 次。
1.2 为什么使用 Rpc(SendTo.Everyone)?
SendTo.Everyone 确保了包括服务器在内的所有客户端都会执行 Invoke。这非常适合播放音效、粒子特效等表现层逻辑。
二、 工业级项目的三个优化方向
虽然官方代码能跑通,但在复杂的工业仿真中,我们需要更严谨的处理方案。
- 2.1 状态权威:谁来判定"触发"?官方方案是客户端判定(Client-Authoritative)。优点: 触发者没有延迟感,走进区域立刻播音效。缺点: 如果玩家网络卡顿,可能他已经走过去了,别人才收到通知。组长建议: 对于核心逻辑(如触发了电力跳闸),建议由服务器判定位置,或者客户端发起 ServerRpc,由服务器确认后再广播。
- 2.2 逻辑解耦:视觉 vs 逻辑视觉层: 进门声音、灯光闪烁。用
Rpc(SendTo.Everyone)。数据层: 任务进度加一、分数扣除。必须在服务器端完成,建议使用 NetworkVariable。 - 2.3 断线重连的坑想象一下,区域内有一个开关,玩家进去后灯亮了。Rpc 的局限: Rpc 是瞬时的。如果玩家 C 在灯亮后才连接进来,他由于错过了那个 Rpc,看到的灯依然是关着的。解决方案: 对于持续性状态(如灯亮不亮),请改用
NetworkVariable<bool>。
三、 实战扩展:带状态同步的高级触发器
下面是一个更健壮的实现,它结合了 即时广播 和 状态保存:
csharp
[RequireComponent(typeof(NetworkObject))]
public class AdvancedNetworkTrigger : NetworkBehaviour
{
// 使用 NetworkVariable 记录区域状态,确保中途加入的玩家能同步
public NetworkVariable<bool> IsAreaOccupied = new(false);
[SerializeField] private UnityEvent onGlobalTrigger;
void OnTriggerEnter(Collider other)
{
// 仅本地玩家且是 Owner(或者特定判断)发起请求
if (other.CompareTag("Player") && other.GetComponent<NetworkObject>().IsLocalPlayer)
{
RequestTriggerServerRpc(true);
}
}
void OnTriggerExit(Collider other)
{
if (other.CompareTag("Player") && other.GetComponent<NetworkObject>().IsLocalPlayer)
{
RequestTriggerServerRpc(false);
}
}
[ServerRpc(RequireOwnership = false)]
void RequestTriggerServerRpc(bool state)
{
// 服务器修改状态,这会自动同步给所有客户端
IsAreaOccupied.Value = state;
// 同时可以广播一个瞬时事件(比如警报声)
NotifyTriggerClientRpc(state);
}
[ClientRpc]
void NotifyTriggerClientRpc(bool state)
{
if(state) onGlobalTrigger?.Invoke();
}
}
四、 不同方案对比
| 方案 | 纯 RPC (官方示例) | NetworkVariable + RPC (进阶版) |
|---|---|---|
| 实时性 | 极高(客户端先行) | 中(需经过服务器确认) |
| 持久性 | 无(错过就没有了) | 高(新进玩家能看到最新状态) |
| 安全性 | 容易被客户端恶意触发 | 服务器可控,更安全 |
| 适用场景 | 烟花、脚步声、UI 弹窗 | 开门、任务目标、电力开关 |
五、 总结:避坑清单
- Tag 校验: 确保触发层级(Layer)或 Tag 设置正确,防止场景中的 NPC 或小猫小狗把全局警报踩响。
- 频率限制: 如果是一个可以反复进出的区域,记得加一个简单的冷却计时器(Cooldown),防止玩家在边界反复摩擦导致 Rpc 洪水。
- 本地表现先行: 如果追求极致手感,可以先在本地 OnTriggerEnter 里播音效,然后再发 Rpc 给别人播。

参考链接:
NGO RPC 官方文档: NGO RPC 官方文档