跳转到内容

第 34 章 · NPC 的三种架构

约 7 分钟 难度:4 动手章

这一章解决:NPC 该不该同步、由谁驱动、状态放在哪里,以及什么时候该把 NPC 降级成装饰。

先看一下

合作防守世界需要敌人,休闲世界可能只需要一个在吧台后面挥手的角色。两者都叫 NPC,网络需求完全不同。前者影响分数、波次和胜负,后者只提供空间感。把这两类 NPC 写成同一套同步 AI,会同时浪费预算和增加调试风险。

这一章会拿到什么

  • NPC 的三种架构:local-only 装饰、Owner 驱动、分片 Owner
  • 一张按功能选择同步范围的决策表
  • GameState、NPC 对象、玩家本地表现之间的字段边界
  • 一份「什么时候别做同步 NPC」的判断清单

依赖前面

  • 第 5 章的可恢复状态
  • 第 11 章的 GameState Owner 边界
  • 第 26 章的轻实时 tick
  • 第 32 章的修正与降级顺序

这是 difficulty 4 章节。只想先完成最小主项目时,可以先跳到第八部的闭环,再回来看本章的高风险系统。


NPC 不是一种技术对象,而是一组功能。写代码前先判断它承担什么工作。

NPC 功能例子推荐架构迟入要求
空间装饰酒吧服务员、远处巡逻影子、观众席人群local-only不要求一致
节奏推进合作防守敌人、巡逻守卫、刷怪波次Owner 驱动要能从当前状态恢复
大量实体一波 30 个小怪、多区域巡逻群分片 Owner要按对象恢复
剧情触发对话 NPC、任务目标、房间机关GameState 触发 + 本地表现关键阶段必须恢复

这张表的重点是「一致性需求」。NPC 是否看起来聪明,往往没有「所有玩家是否在同一局游戏里」重要。装饰 NPC 可以每个客户端各自跑动画;影响分数和胜负的 NPC 必须让某个 Owner 负责写共识状态。


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 架构。一个 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 决策,远端表现。远端客户端可以本地插值、播放动画、显示受击效果,但不能各自改 hpstate。这样迟入玩家进来时,拿到同步字段就能恢复:这个 NPC 活着还是死了,当前目标是谁,位置大致在哪里。

Owner 驱动适合数量较少、状态会影响玩法的 NPC,例如 1 个 boss、几只精英怪、巡逻守卫、解谜里的移动目标。它的主要风险是 Owner 负载集中。一个客户端同时驱动 40 个 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 不影响规则,只提供氛围local-only 动画或 Timeline
NPC 数量很多,只需要制造压力同步少量关键敌人,其他做本地表现
命中公平是核心卖点避开精确 PvP,改成区域、低频技能或合作目标
Quest 需要支持,场景已经接近预算减少活跃 NPC,改成波次式分批出现
Owner 离开会导致整局无法恢复先实现 Owner 重分配和死亡状态恢复,再加行为复杂度

「别做」不是取消 NPC,而是取消不必要的一致性。一个休闲世界的猫可以每个客户端各走各的;一个合作防守的 boss 必须同步 HP 和阶段;一群背景观众可以完全不进网络层。


选择 NPC 架构时按这个顺序判断。

  1. 这个 NPC 是否影响分数、胜负、阶段或可恢复状态?否,优先 local-only。
  2. 是否只需要少量关键 NPC 一致?是,Owner 驱动。
  3. 数量是否多到一个 Owner 明显吃不下?是,再考虑分片 Owner。
  4. 分片后是否能写清 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 和对象池接口边界。