第 34 章 · NPC 的三种架构
这一章解决:NPC 该不该同步、由谁驱动、状态放在哪里,以及什么时候该把 NPC 降级成装饰。
先看一下
合作防守世界需要敌人,休闲世界可能只需要一个在吧台后面挥手的角色。两者都叫 NPC,网络需求完全不同。前者影响分数、波次和胜负,后者只提供空间感。把这两类 NPC 写成同一套同步 AI,会同时浪费预算和增加调试风险。
这一章会拿到什么
- NPC 的三种架构:local-only 装饰、Owner 驱动、分片 Owner
- 一张按功能选择同步范围的决策表
GameState、NPC 对象、玩家本地表现之间的字段边界- 一份「什么时候别做同步 NPC」的判断清单
依赖前面
- 第 5 章的可恢复状态
- 第 11 章的
GameState Owner边界 - 第 26 章的轻实时 tick
- 第 32 章的修正与降级顺序
这是 difficulty 4 章节。只想先完成最小主项目时,可以先跳到第八部的闭环,再回来看本章的高风险系统。
先按功能分 NPC
Section titled “先按功能分 NPC”NPC 不是一种技术对象,而是一组功能。写代码前先判断它承担什么工作。
| NPC 功能 | 例子 | 推荐架构 | 迟入要求 |
|---|---|---|---|
| 空间装饰 | 酒吧服务员、远处巡逻影子、观众席人群 | local-only | 不要求一致 |
| 节奏推进 | 合作防守敌人、巡逻守卫、刷怪波次 | Owner 驱动 | 要能从当前状态恢复 |
| 大量实体 | 一波 30 个小怪、多区域巡逻群 | 分片 Owner | 要按对象恢复 |
| 剧情触发 | 对话 NPC、任务目标、房间机关 | GameState 触发 + 本地表现 | 关键阶段必须恢复 |
这张表的重点是「一致性需求」。NPC 是否看起来聪明,往往没有「所有玩家是否在同一局游戏里」重要。装饰 NPC 可以每个客户端各自跑动画;影响分数和胜负的 NPC 必须让某个 Owner 负责写共识状态。
架构一:local-only 装饰 NPC
Section titled “架构一:local-only 装饰 NPC”local-only NPC 只在本地运行。每个客户端都看到类似的角色,但不要求完全一致。
适合这类内容:巡逻背景角色、远处观众、环境动物、只播放待机动画的店员、只给本地玩家显示的指引角色。它们不加分、不挡路、不决定胜负,也不要求迟入玩家看到和其他玩家一模一样的位置。
最小结构很简单。
public class LocalNpcDecoration : UdonSharpBehaviour{ public Animator animator; public Transform[] patrolPoints;
private int pointIndex;
private void Start() { PickNextPoint(); }
public void PickNextPoint() { pointIndex = (pointIndex + 1) % patrolPoints.Length; transform.position = patrolPoints[pointIndex].position; SendCustomEventDelayedSeconds(nameof(PickNextPoint), 8f); }}这段没有 [UdonSynced],也没有 Owner 判断。它只回答一个问题:本地画面需要一个角色在动。多人之间位置不一致也没有破坏规则。
local-only 的边界也要写清楚。它不适合做可击杀敌人、可阻挡路径的守卫、会改变分数的目标、多人共同对话的剧情角色。只要 NPC 的状态会进入 GameState、PlayerObject 或计分系统,就进入后两种架构。
架构二:Owner 驱动 NPC
Section titled “架构二:Owner 驱动 NPC”Owner 驱动是默认的同步 NPC 架构。一个 NPC 对象有一个 Owner,只有 Owner 运行 AI 决策并写同步字段,其他客户端只读字段和播放表现。
[UdonSynced] private int state;[UdonSynced] private int hp;[UdonSynced] private int targetPlayerId;[UdonSynced] private Vector3 syncedPosition;
private void Update(){ if (!Networking.IsOwner(gameObject)) return; if (state == NPC_DEAD) return;
TickNpcAI(); syncedPosition = transform.position;}
public override void OnDeserialization(){ ApplyNpcView(state, hp, targetPlayerId, syncedPosition);}这里的关键不是代码,而是写权:Owner 决策,远端表现。远端客户端可以本地插值、播放动画、显示受击效果,但不能各自改 hp 或 state。这样迟入玩家进来时,拿到同步字段就能恢复:这个 NPC 活着还是死了,当前目标是谁,位置大致在哪里。
Owner 驱动适合数量较少、状态会影响玩法的 NPC,例如 1 个 boss、几只精英怪、巡逻守卫、解谜里的移动目标。它的主要风险是 Owner 负载集中。一个客户端同时驱动 40 个 NPC,会把脚本、物理和同步压力堆在同一个人身上。
架构三:分片 Owner NPC
Section titled “架构三:分片 Owner NPC”分片 Owner 用在数量变多的时候。多个 NPC 被分到不同玩家或不同对象 Owner 上,每个 Owner 只负责自己那一片。
| 分片方式 | 适合场景 | 风险 |
|---|---|---|
| 按对象轮询 | 20 个普通敌人平均分给玩家 | Owner 离开时要重分配 |
| 按区域 | 多个房间各自有巡逻 NPC | 玩家集中到一区时负载不均 |
| 按波次 | 当前波由一个 Owner 驱动,下一波换人 | 切换边界要清楚 |
| 按重要度 | boss 独立 Owner,小怪 local-only 或低频同步 | 实现复杂度上升 |
分片 Owner 不是免费优化。它减少单个 Owner 的负载,也增加了跨 Owner 调试成本。两个 NPC 互动、NPC 共同围攻、跨区域追击、统一暂停,这些行为都需要额外协议。
一个可控的分片方案要满足三条:对象归属可查,Owner 离开可重分配,GameState 只保存聚合状态。
[UdonSynced] private int npcOwnerPlayerId;[UdonSynced] private int npcState;
public override void OnPlayerLeft(VRCPlayerApi player){ if (player.playerId != npcOwnerPlayerId) return; RequestNpcOwnerReassign();}GameState 不应该直接存每个 NPC 的完整 AI 状态。它保存波次、剩余敌人数、胜负阶段。每个 NPC 自己保存 HP、目标、状态。这样分片后仍然能保持边界:全局进度归 GameState,单体状态归 NPC 对象。
什么时候别做同步 NPC
Section titled “什么时候别做同步 NPC”有几种情况适合降级。
| 情况 | 更稳的做法 |
|---|---|
| NPC 不影响规则,只提供氛围 | local-only 动画或 Timeline |
| NPC 数量很多,只需要制造压力 | 同步少量关键敌人,其他做本地表现 |
| 命中公平是核心卖点 | 避开精确 PvP,改成区域、低频技能或合作目标 |
| Quest 需要支持,场景已经接近预算 | 减少活跃 NPC,改成波次式分批出现 |
| Owner 离开会导致整局无法恢复 | 先实现 Owner 重分配和死亡状态恢复,再加行为复杂度 |
「别做」不是取消 NPC,而是取消不必要的一致性。一个休闲世界的猫可以每个客户端各走各的;一个合作防守的 boss 必须同步 HP 和阶段;一群背景观众可以完全不进网络层。
最小选择流程
Section titled “最小选择流程”选择 NPC 架构时按这个顺序判断。
- 这个 NPC 是否影响分数、胜负、阶段或可恢复状态?否,优先 local-only。
- 是否只需要少量关键 NPC 一致?是,Owner 驱动。
- 数量是否多到一个 Owner 明显吃不下?是,再考虑分片 Owner。
- 分片后是否能写清 Owner 离开和迟入恢复?否,回退到更少 NPC 或更低同步频率。
SimpleAI 这类社区项目适合当历史参考。它展示了 VRChat 约束下 local AI 和 synced AI 的实现思路,但当前仓库已归档,不适合作为新项目的生产框架。第七部采用最小自写模型,是为了让架构边界先稳定下来。
挑一条试。
- 把一个巡逻守卫从 Owner 驱动改成 local-only。哪些玩家可见差异会变大?哪些同步字段可以删除?
- 给 boss 增加第二阶段。这个阶段是 NPC 自己同步,还是
GameState同步?迟入玩家怎样看到正确阶段?
| 场景 | 预期 | 实际 |
|---|---|---|
| 1. local-only NPC 多客户端观察 | 各客户端动画可能不同,但不影响分数和阶段 | … |
| 2. Owner 驱动 NPC 受击 | 只有 Owner 写 HP,远端通过同步字段更新表现 | … |
| 3. NPC Owner 离开 | 对象进入重分配流程,死亡 / HP / 阶段不丢 | … |
| 4. 迟入玩家进入战斗中 | 能看到 NPC 当前存活、HP、阶段和大致位置 | … |
| 5. 同时 20 个 NPC 活跃 | 记录帧率、同步失败和 Owner 负载,决定是否分片或降级 | … |
- 能解释 local-only、Owner 驱动、分片 Owner 的区别
- 能按 NPC 功能判断是否需要同步
- 能说明
GameState为什么只保存 NPC 聚合状态 - 能写出 Owner 离开后的最小重分配入口
- 能指出一种该把 NPC 降级成装饰的场景
- Unity Manual 2022.3 · NavMesh Agent — Unity NPC 移动与避障的基础组件。
- Centauri2442 · SimpleAI(访问日期:2026-06-17)— 已归档的 VRChat UdonSharp AI 示例项目,适合作历史参考。
- 第 11 章 · 全局状态管理器 —
GameState Owner的边界。 - 第 26 章 · 轻实时范式 — 波次、tick 和对象池接口边界。