第 3 章 · Owner 不是 Master
第 2 章实现 C 的按钮里有一行:
if (!Networking.IsOwner(gameObject)){ Networking.SetOwner(Networking.LocalPlayer, gameObject);}这一行同时出现了三个网络身份概念:IsOwner、gameObject(被拥有的对象)、LocalPlayer(当前客户端是谁)。还有一个概念第 2 章里没出现,但很多教程会把它和 IsOwner 混用:IsMaster。这一章把这四个概念彻底分开。
三个判断都在回答不同问题
Section titled “三个判断都在回答不同问题”| 判断 | 回答的问题 | API |
|---|---|---|
Networking.IsOwner(obj) | 这个对象当前是不是我在拥有? | Networking.IsOwner(gameObject) |
Networking.IsMaster | 我是不是这个实例的 Master? | Networking.IsMaster |
Networking.LocalPlayer | 我这个客户端代表的是哪个 Player? | VRCPlayerApi 对象 |
三个判断的「主语」不一样。Owner 是网络 GameObject 上的属性:同一个 GameObject 上的 UdonBehaviour 和 VRCObjectSync 共享 Owner,不同 GameObject 可以各有不同 Owner,所以同一时刻 A 物体属于玩家 1、B 物体属于玩家 2 是正常的。Master 是实例上的属性,整个房间里只有一个人是 Master。Local Player 是客户端上的属性,每台机器上 Networking.LocalPlayer 就是这台机器前面的人。
写错最常见的形态是把第二条当第一条用:「我是 Master,所以这个按钮归我管」。这是错的。Master 不是默认的「全局裁判」,新版 SDK 文档里官方明确把 IsMaster 标记为旧逻辑,推荐改用 IsOwner 判断(针对具体对象)或 IsInstanceOwner 判断(针对实例创建者)。
Owner:对象级权威
Section titled “Owner:对象级权威”每个网络对象有且只有一个 Owner。Owner 的两条核心能力:
- 它是唯一能修改这个对象上
[UdonSynced]字段并让变更被同步出去的人。 - 它是唯一能调用
RequestSerialization()真正发包的人(手动同步模式下)。
非 Owner 即使代码里直接写 isOn = true,本地内存里那个字段确实变了,但 VRChat 网络层不会把它发出去,过一会儿 Owner 那边的同步又会把它盖回来。
写 VRChat 同步代码时这是第一条纪律:改同步字段前,确认是 Owner。
if (!Networking.IsOwner(gameObject)){ Networking.SetOwner(Networking.LocalPlayer, gameObject);}isOn = !isOn;RequestSerialization();这段代码适合入门示例,但有一个伏笔:SetOwner 的结果要经过网络传播,远端确认会有延迟。所有权转移后的可靠确认点是 OnOwnershipTransferred(player)。如果业务逻辑更复杂,比如先抢权、等待一段时间、再修改关键字段,就不要把「调用了 SetOwner」直接当成「所有客户端都承认我是 Owner」。第二部 13 章会回到这个时间窗口的问题。
默认 Owner 是谁
Section titled “默认 Owner 是谁”场景里一开始就摆着的网络对象,Owner 默认是 Master。Master 是房间第一位玩家,或上一任 Master 离开后被自动指定的那位。
空场景刚被加载时,所有同步组件的 Owner 都是 Master。Master 离开时,他拥有的对象会自动转给新 Master。任何时候有人调用 Networking.SetOwner(somePlayer, obj),对象的 Owner 就脱离 Master 系,归 somePlayer 了。
所以 Master 既不是「全局裁判」,也不是「永远的 Owner」。它的真实身份是:没人主动 SetOwner 时的兜底 Owner。
Master:兜底,不是裁判
Section titled “Master:兜底,不是裁判”Master 的官方定义只有几条。进入空实例的第一个玩家成为初始 Master。当前 Master 离开时,所有权会自动转给某位剩余玩家,具体选举规则官方未公开。要感知自己接管了什么,靠对象上的 OnOwnershipTransferred,而不是去赌「我是不是新 Master」。Networking.IsMaster 返回的就是本地玩家是不是 Master。
这里有两个容易踩的细节。一个是 Master 切换不可控:Master 离开、Android 玩家把 VRChat 切到后台太久,都可能让 Master 易主,任何依赖「这个人一直是 Master」写出来的逻辑都会在某次切换后崩。另一个是不应把「是不是 Master」作为玩法权限的门槛:需要门槛时用 Networking.IsInstanceOwner(实例创建者)或自己维护一个「主持人」同步字段。
IsMaster 在 Vol.2 里会出现,但出现的位置非常有限,主要在第 11 章讲 GameState 兜底的时候。除此之外的代码里,看到 IsMaster 都应该停下来想一想能不能换成 IsOwner 或者一个明确的「裁判玩家」字段。
Local Player:每台机器的「我」
Section titled “Local Player:每台机器的「我」”Networking.LocalPlayer 在每一台机器上返回该机器上的玩家本人。它是 VRCPlayerApi 对象,能查 ID、姓名、位置、朝向、是否手持物体等。
几个要点。编辑器里 Networking.LocalPlayer 可能是 null,因为编辑器里没有真实玩家,需要时用 Utilities.IsValid(player) 包一层。每台机器的 LocalPlayer 不一样,所以 Networking.SetOwner(Networking.LocalPlayer, gameObject) 含义是「让我自己成为这个对象的 Owner」,是常用的抢权写法。
不要把 Networking.LocalPlayer 存到 [UdonSynced] 字段然后同步出去并期待别人那边读到的是「同一个玩家」。它是本地概念。需要跨客户端引用某位玩家时,用 playerId(int)同步。
三个常见的踩坑场景
Section titled “三个常见的踩坑场景”下面三种情况是写 Vol.2 同步代码时几乎一定会撞到的。
场景一:Master 离开后,怪不动了
Section titled “场景一:Master 离开后,怪不动了”症状:写了一个简单的 AI,刷怪、巡逻、追玩家全在一段 UdonSharp 里跑。Master 离开后,怪愣在原地不动。
原因:刷怪用的那个 GameObject 是场景里默认存在的,它的 Owner 默认是 Master。Master 离开后 Owner 转给了新 Master,但 AI 代码里所有「移动怪」的逻辑只在 Owner 端跑,新 Master 的客户端可能还没意识到自己接管了这件事,或者 AI 代码里写了 if (!Networking.IsMaster) return; 这种判断,新 Master 的客户端要等下一次 tick 才发现自己是 Master,这中间的几百毫秒到几秒,怪就停了。
正确思路:AI 状态由对象的 Owner 驱动,而不是由 Master 驱动。OnOwnershipTransferred(player) 事件会在所有人那边触发,新 Owner 接到这个事件就知道「现在该轮到我推动 AI 了」。第二部 14 章会展开「Owner 失效恢复检查表」。
场景二:抢权后立刻改字段,对方先看到旧值
Section titled “场景二:抢权后立刻改字段,对方先看到旧值”症状:玩家 A 按按钮,本地代码先 SetOwner(LocalPlayer, obj),再设 isOn = true,再 RequestSerialization()。但远程玩家 B 那边看到「灯先亮一下,又灭一下,再亮起来」。
原因:抢权请求和值变更几乎同时发出。B 这边的执行序列可能是:先收到「Owner 变成了 A」,但这个对象当前的同步值还是上一帧 A 还没设完时的旧值,于是先用旧值刷一次本地,几十毫秒后再收到 A 的新值,再刷一次。视觉上就是闪一下。
正确思路:本地切完 Owner 和值之后,本地直接调用 ApplyLight() 让本地立刻是对的状态。远程那边短暂的「值还没到」就让它呈现旧值好了,新值很快会到。如果业务实在不能接受这种闪烁,把抢权和应用值拆开做:先抢权,等 OnOwnershipTransferred 在自己这边触发了再改值并 RequestSerialization。第二部 13 章详细展开这个时序。
场景三:判断成本写反
Section titled “场景三:判断成本写反”症状:每帧 Update 里写了 if (Networking.IsOwner(gameObject)) 来决定要不要做事,但游戏里有几十个这样的对象,性能监视器显示 Networking 调用占用很高。
原因:Networking.IsOwner(go) 不是缓存查询,每次调用都要做一次内部检查。在 Update 里频繁调用是有代价的。
正确思路:在 Start 里取一次自己的 player,在 OnOwnershipTransferred(player) 里更新一个本地 bool isOwner 字段,Update 里直接读这个字段。这种小优化第六部会再讲,但基础概念这里先提一下。
合作防守世界里有一个 GameState 对象,承载当前波次、总分、阶段。回答下面三个问题:
- 它的 Owner 应该是谁?是 Master?是某个固定玩家(比如 InstanceOwner)?还是「轮流当」?三种方案各有什么取舍?
- 如果 Owner 中途离开了,波次和分数会发生什么?怎么避免「波次倒退」「分数被清零」?
- 一个临时陷阱物体(玩家进区域时触发),它的 Owner 默认是 Master 还是被触发的那个玩家?为什么?
不需要写代码。能在脑子里讲清楚三种 Owner 拓扑的取舍,就准备好读第二部了。
这一章做了什么
Section titled “这一章做了什么”三个判断各自回答不同问题:IsOwner(这个对象归我?)、IsMaster(我是兜底裁判?)、LocalPlayer(我这个客户端代表谁?)。默认 Owner 是 Master,但 SetOwner 后归别人;Master 是兜底,不是裁判。改同步字段前先 IsOwner,看到 IsMaster 都该停下来想一想能不能换。
第 4 章会把「RequestSerialization 不是立即发送」这一句拆开来讲,把整条手动同步生命周期画清楚。
- VRChat Creator Docs · Network Components —
IsOwner/IsMaster/LocalPlayer/SetOwner等 API 的官方说明。 - VRChat 汉化文档 · Udon 网络 —
IsMaster旧逻辑提示与新推荐做法。 - 附录 · 术语表 —
Owner、Master、Local Player、Authority、SetOwner的客观定义。