1. 项目背景与整体设计思路先说说这个项目的来龙去脉。我一直在研究Unity联机方案的选型手头需要搭一个轻量级、能快速出原型验证玩法的多人FPS框架。市面上的方案试了一圈——Mirror生态成熟但风格偏老Photon收费灵活但免费层限制多Netcode for GameObjectsNGO性能不错但上手门槛不低。后来朋友推荐了PurrNet花了几个晚上把demo跑起来实测手感、同步效果和开发效率都超出预期于是干脆把整套对战框架按FPS方向搭了出来。1.1 核心里程碑为什么瞄准FPS而不是其它类型选FPS射击游戏作为验证场景原因很直接它是联机同步的“压力测试器”。角色频繁移动、视角快速旋转、子弹高速飞行、伤害实时结算对网络延迟、状态同步、事件触发的要求都极高。能在FPS场景里跑得顺畅的架构换到MOBA、MMO副本或者吃鸡缩圈场景基本只需要改表现层。目标其实不复杂就是三个模块的组合玩家控制移动视角、武器系统开火命中判定、对战逻辑伤害计算玩家管理对局状态。PurrNet负责所有“网线”部分——连接的建立、数据的收发、状态的分发我们专注在做游戏逻辑本身。1.2 为什么用PurrNet而非自研或重框架拿PurrNet和另外两种常见路线做个对比自研方案UnityTransport 自写消息协议优点是完全可控、无第三方依赖缺点是坑极多。要自己处理连接握手、断线重连、RPC通道管理、序列化版本兼容、状态同步策略……一套下来光网络层就得两三个月。对于“要做游戏内容”的项目来说成本太高。重框架方案Mirror / NGO / Photon功能全、生态大、案例多但开发模式相对固定脚本要挂大量的网络组件对项目结构的侵入性比较强。初学阶段很容易陷入“跟着教程搭了个局域网Demo换到正式服务器又各种不通”的困境。PurrNet的差异化优势它的定位介于两者之间。相比NGOAPI更精简RPC注册和调用方式更直接相比Mirror对最新Unity版本和C#特性的支持更积极。整个库的核心思路是“数据优先”——你定义好数据结构网络同步的事情它替你规划。这对快速验证玩法的阶段非常友好。注意我这里说的是某个具体时间点的体验网络库的更新迭代非常快。如果你看到这篇文章时PurrNet已经出了大版本请以官方文档为准但整体架构思路是可以沿用的。1.3 适合哪些人参考这个方案这个项目的完整脉络适合这几类人想做联机FPS但还没确定网络方案的开发者已经在用Mirror写了一些代码想对比一下其它方案的同行想快速搭建一个能试手感的射击对战原型而不是一上来就啃大型架构的学生或独立开发者代码量控制在可接受的范围内不依赖现成FPS模板玩法和网络逻辑全部自己写这样你能清楚知道每一行代码在网络层做了什么。2. 联机架构与关键方案选型2.1 帧同步与状态同步FPS到底该选哪个这是个绕不开的老问题。市面上的方案可以粗略分成两大类状态同步服务器持有权威状态客户端发指令服务器计算后把结果广播给所有客户端。客户端看到的永远比服务器慢一拍所以需要各种预测和插值手段来掩盖延迟。帧同步锁步同步所有客户端跑同一份确定性逻辑只同步输入指令。早期RTS常用省流量但写战斗逻辑时对确定性要求极高浮点数稍微不一致就对不上局。FPS界的主流是“服务器权威的状态同步”。拿CS的老架构举例客户端做的“本地预测”只是让画面显得跟手真正决定是否命中、扣多少血的判断都在服务器完成。这能最大程度防止作弊。我们这次也沿用这套思路但做了一些“轻量化简化”——不做完整的预测回滚而是用少量本地插值来保证手感。PurrNet在这套架构里的分工很清楚它提供了一个可靠的数据通道和状态同步机制但“什么是权威状态”由你自己定义。你需要自己规划哪些逻辑放在服务器执行、哪些数据需要广播、哪些RPC只能由服务器调用。2.2 玩家、pawn与控制器职责划分的几种做法做多人游戏前必须先想清楚一个问题一个玩家在服务器上对应多少个实体比较常见的做法有三种单一Player Object方案每个玩家对应一个Player对象控制逻辑和视觉表现全挂在上面。适合原型Demo代码简单但后期扩展性差——如果玩家倒地后要操作无人机或者死亡后要切到观战模式就会发现控制权没法转移。Player Pawn分离方案Player表示“一个连接进来的玩家”负责持久化数据账号信息、排行、背包。Pawn表示“玩家当前控制的身体”可以是角色、载具、无人机。死亡换Pawn、切换控制目标都很自然。这是商业项目的常规做法但代码量稍大。PurrNet的Player Object 手动管理PawnPurrNet本身提供了PlayerObject的概念——连进来一个客户端服务器会在所有客户端上实例化一个标记了Owner的Player对象。这是天然的“连接ID”。我们在这个PlayerObject底下管理一个列表存放当前属于该玩家的“Pawn”也就是受控角色。这个折中方案既不需要自己维护复杂的连接ID到对象的映射又能保留换Pawn的扩展空间。我强烈建议在项目第一天就按“Player Pawn”分层来写哪怕你暂时只有一个角色后面加东西会轻松太多。2.3 远程过程调用设计哪些方法需要过网PurrNet的远程过程调用RPC是核心交互手段。但它不会替你想“这个方法要不要过网”这个决定必须在设计阶段就拍板。我用表格列出常见的网络交互场景和建议方案场景调用方向建议方式玩家按下开火键客户端→服务器客户端发Command服务器验证后广播服务器确认子弹造成伤害服务器→所有客户端服务器发ObserversRPC广播伤害事件服务器通知客户端“比赛开始”服务器→指定客户端/所有客户端TargetRPC或ObserversRPC新玩家加入同步已有玩家位置服务器→新玩家接入时手动同步全量状态玩家复活服务器→所有客户端服务器在Pawn生成后广播一个很重要的心得所有“判定”类逻辑必须放服务器。客户端报告的坐标、伤害数字一概不直接采信服务器只把它当作“请求”来看待。比如说客户端说“我开枪了朝这个方向”服务器要重新验证子弹发射位置是否合理、枪口方向是否在允许角度内、射击间隔是否满足射速限制。这些验证看着繁琐但没有它们你的游戏就是外挂的温床。2.4 状态同步粒度什么样的数据才值得同步不是所有变量都要同步。把每个字段都打成网络变量是新手最容易犯的错。不仅浪费带宽还会造成不必要的逻辑耦合。我在这套框架里同步的数据就三类Transform位置旋转这是FPS最核心的同步数据频率最高。必要状态量玩家的血量、当前武器的弹药数、对局阶段准备/进行中/结束。一次性事件击中特效、开火音效、击杀公告——这些用RPC一次性广播不做状态同步。血量这种低频变更的数据用状态同步没问题因为它变化少、事件性强位置这种高频变更是同步的核心难点需要单独设计平滑策略。想清楚每份数据的“变更频率”和“可容忍延迟”再决定用状态同步还是RPC架构就清晰了一大半。3. 核心细节拆解与实操要点3.1 网络Transform的平滑处理为什么直接同步会出现橡皮筋我一开始图省事直接用PurrNet提供的网络Transform组件同步角色位置结果局域网里测还没事一旦模拟了较高的网络延迟角色走路就变成了一卡一顿的“橡皮筋”——位置在每个同步间隔里跳来跳去视角根本没法瞄。这个问题的根源是网络同步频率是有限的通常10~30Hz而游戏渲染帧率是60~144Hz。两者之间的空隙必须靠插值来填补。我最终的方案分两层本地预测直接操控对于OwnClient你控制的角色不通过网络回环本地输入直接驱动Transform位移。这样你的操作手感永远是即时的不管网络多差按W就应该立刻往前走。远端插值平滑追赶对于其他玩家不直接赋值网络同步过来的位置而是每帧插值逼近目标位置。最简单的做法是线性插值进阶一点用球面插值处理旋转或者用速度外推做“预判”。插值代码是核心逻辑贴一段核心片段出来参考// 远端角色插值脚本核心逻辑 public class RemotePlayerSmoother : MonoBehaviour { [SerializeField] private float lerpSpeed 12f; private Vector3 targetPosition; private Quaternion targetRotation; private bool hasTarget; // PurrNet回调每次收到状态同步更新时调用 public void OnNetworkStateUpdate(Vector3 pos, Quaternion rot) { targetPosition pos; targetRotation rot; hasTarget true; } private void Update() { if (!hasTarget) return; // 线性插值位置球面插值旋转 transform.position Vector3.Lerp(transform.position, targetPosition, lerpSpeed * Time.deltaTime); transform.rotation Quaternion.Slerp(transform.rotation, targetRotation, lerpSpeed * Time.deltaTime); } }一个小技巧插值速度不宜过大否则平滑效果不明显也不宜过小否则角色“漂”得太远。我实测的一个经验范围是8~15具体数值根据项目手感调。3.2 武器系统的选型与实现即时命中还是弹道模拟FPS武器系统分两大流派即时命中开枪瞬间用射线检测命中即扣血。CS、CF这类传统竞技射击游戏的步枪基本都这么做。表现层加一条线或者光效模拟子弹轨迹但核心判定是瞬时的。弹道模拟子弹有初速度、重力、下坠、飞行时间服务器上模拟子弹飞行命中后结算伤害。PUBG类大战场会做下坠因为射击距离动辄几百米弹道差异明显。这两种方案的网络复杂度完全不同。即时命中的核心是一次“射线检测伤害结算”弹道模拟则需要服务器维护子弹实体、处理飞行碰撞、考虑延迟补偿。对于这个项目我两种都实现了但推荐你先从即时命中开始——它难度低、网络负载小对“验证框架”的目标来说已经足够了。实现要点服务器持有武器参数射速、伤害、弹夹容量客户端按下开火键→发送“开火请求”到服务器服务器验证冷却时间→执行射线检测→广播命中事件3.3 命中判定与伤害结算服务器权威的落地操作命中的判定放在服务器执行。关键步骤如下服务器收到开火请求读取开火者的当前位置和朝向。从枪口位置发射一条射线射线射程就是武器最大射程。射线撞到的第一个有效目标如果是可伤害对象读取其归属方Owner。判断是否为友军——是则跳过或者走友伤逻辑看规则否则调用伤害接口。伤害事件通过RPC广播给所有客户端播放特效、音效、更新血条。这里有个看似简单但容易踩坑的细节射线从谁的视角发射。服务器上你不知道“玩家按F5瞄准时屏幕中心到底对着哪”所以客户端的开火请求里必须带上瞄准方向。服务器拿到方向后用它做射线检测而不是用服务器上该角色当前Transform的方向——这两个方向在延迟下很可能不一致。3.4 网络引入的延迟补偿如何让命中判定更公平做了服务器权威判定之后会出现一个新问题玩家在自己屏幕上明明已经瞄到了对方但服务器判定是“没命中”因为客户端看到的位置和服务器记录的位置有偏差。网络延迟高的时候射击手感会变得非常“玄学”——感觉准星对准了就是打不到。这个问题FPS领域已有成熟解法最常见的思路叫“延迟补偿”。核心原理是服务器在处理玩家的开火请求时不是按当前时间的位置去判定而是按玩家发起请求那个时刻时间戳的位置去判定。简化版实现思路客户端开火时在请求包里附带上“本地时间戳”。服务器收到请求后根据网络往返时间估算一个“事件发生时刻”。服务器把目标位置尽量回退到该时刻的近似位置在服务器上保留一小段历史位置缓冲。用回退后的位置做射线检测。这个方案对服务器性能要求不高但能显著改善射击手感。实现时先做2~3秒的缓冲足够了。注意延迟补偿做过头会带来“我被穿墙打死”的争议——对方看到你还在墙角外服务器判定也认为你在墙角外。所以补偿的时间窗口要严格控制我建议先固定一个值体验后微调不要一上来就搞动态自适应补偿。3.5 伤害对象接口设计让子弹“打到谁都能结算”为了让各类可伤害物体角色、木箱、载具部件、爆炸物都能被射线正确服务需要抽象一层“可伤害接口”。不用关心具体对象是什么类型只知道它能不能吃伤害就行了。// 可伤害接口所有能被子弹命中的对象实现它 public interface IDamageable { // 服务器调用对象受到伤害 void TakeDamage(int damage, Vector3 hitPoint, NetworkPlayer shooter); }这样射线检测后无需一堆类型判断直接检查碰撞体是否实现了IDamageable实现则调用TakeDamage至于对象如何处理伤害是它自己的事。3.6 开火冷却与弹药管理防止客户端“乱报数据”射击游戏最常见的作弊行为是“无限子弹”“无限射速”。处理方式还是在服务器做权威校验弹药数只在服务器递增/递减客户端显示的数字只是“服务器状态的镜像”。射速限制在服务器强制执行即使客户端每秒发送一百次开火请求服务器也只在满足冷却时间时执行点火逻辑。这需要一个简单的“射击冷却计时器”// 服务器端射击冷却控制器 public class FireController { private float nextFireTime; // 服务器调用判断这次开火请求是否合法 public bool CanFire(float currentTime, WeaponConfig weapon) { if (currentTime nextFireTime) return false; nextFireTime currentTime 1f / weapon.FireRate; return true; } }弹药同步上弹药余量应该推给客户端用于UI显示但客户端本地不应直接改弹药数。我这个项目里弹药耗尽时服务器会发一个“换弹请求”的RPC客户端再播放换弹动画。4. 操实战过程与核心环节的实现4.1 环境准备项目初始化与插件导入先交代一下环境基线。我使用的是较新的Unity LTS版本项目开始时的稳定版PurrNet通过Package Manager窗口添加Git URL安装这是最快的接入方式。安装完成后第一步做一个“网络管理器”的空物体挂上PurrNet的核心管理器脚本。配置项里需要关注三个地方服务器地址开发期用本机地址因为客户端和服务器都跑在同一台机器。最大玩家数先设4~8FPS对战最小规模。自动连接开关开发期建议关掉手动控制连接时机。对这个管理器的职责理解很重要它管的是“谁连谁”不是“游戏怎么玩”。游戏逻辑分发给各玩家还是要靠我们自己的管理器。4.2 构建第一个可加入的服务器会话代码写到这里先解决“怎么让多个客户端互相看到”的问题。PurrNet内置了房间列表和加入房间的接口我们直接用它。服务器端初始化逻辑// 服务器启动流程 public class GameServer : MonoBehaviour { public void StartServer() { // 启动服务器并等待客户端加入 // PurrNet内部会自动管理连接鉴权、分配玩家ID等操作 } public void StopServer() { // 关闭服务器断开所有客户端 } }客户端加入逻辑更简单// 客户端启动流程 public class GameClient : MonoBehaviour { public void ConnectToServer() { // 发起连接请求 // 连接成功后PurrNet会自动在服务器上创建PlayerObject } }这一步的常见坑是开发机上同时起服务器和客户端却连不上。大多是防火墙拦了Unity引擎使用的端口。我的排查心得是先在代码里把端口固定下来并加入运行日志确认——先明确“网络层通了”再往下做游戏逻辑能省掉大量后期调试的精力。4.3 玩家对象的生成服务端分配、客户端控制这是联机项目第一个真正体现“权威”的环节。玩家的PlayerObject代表连接的实体由服务器创建但控制权Owner指向该客户端。流程拆解如下客户端连接请求成功后服务器回调触发。服务器生成一个PlayerObject实例设置Owner为刚连接的客户端。PurrNet自动向该客户端同步“你拥有了这个对象”的信息。客户端上的PlayerObject开始接收输入并发送给服务器。服务器每隔一个同步帧读取该玩家的输入状态驱动角色移动。这里的输入处理非常关键。客户端不能直接修改服务器上的Transform而是把“输入意图”——比如“W键按下、鼠标右移15度”——发送给服务器。服务器拿到后套用移动逻辑再把算好的位置广播给所有人。我搭建框架时在服务器上写了一个“移动求解器”接收输入数据结构输出位置变化。这个解法的好处是方便后续加入移动加速、减速带、技能位移等逻辑因为这个求解器不是写死在角色身上的而是通用逻辑任何Pawn都适用。4.4 角色控制器从输入到移动的完整链路虽然Unity有自带的CharacterController组件但在联机环境下有几个要注意的点。CharacterController的Move方法依赖本地物理帧如果在服务器上驱动客户端看到的画面会滞后一拍如果在客户端驱动又破坏权威。我建议的做法是客户端本地模拟玩家控制角色时用CharacterController直接移动本地Transform保证手感同时把输入同步给服务器。服务器权威校验服务器根据收到的输入在自己的物理世界里跑一遍移动逻辑得出“权威位置”然后广播给所有客户端。远端平滑其他客户端收到权威位置后用前面提到的插值脚本做平滑。这里的“服务器跑自己的物理”不是说服务器要开着完整的物理模拟而是把简单的移动公式复现一遍速度、加速度、碰撞半径把结果作为权威位置发布。对于FPS这种高速移动的场景服务器位置和客户端本地位置之间会有轻微偏差但通过插值和预测实现“看起来没区别”完全够用。4.5 视角同步为什么只用同步旋转就够用FPS的视角同步有点特殊。在3D游戏里玩家身体的控制和镜头的控制往往是分离的。身体由移动系统处理镜头由视角旋转系统处理。传统MMO里镜头是第三人称视角跟着角色转就完了FPS是第一人称镜头就是“眼睛”它不一定等于角色身体的朝向比如角色向左移动但镜头看着右边。所以视角同步的要点是不要整体同步Transform的旋转只同步镜头朝向的欧拉角。服务器的角色身体可能面向某个方向而玩家的眼睛在看另一个方向。远端客户端渲染这个玩家时会把视角角度发过去让对方的头部/枪口模型朝向那个方向。视角同步的数据流本地客户端鼠标移动 → 更新本地镜头旋转 → 发送旋转角到服务器服务器把该玩家的视角角度广播给所有客户端远端客户端渲染对方角色时把枪口朝向、身体向仰角改为该视角角度4.6 武器发射完整流程从扣扳机到击杀公告这一条链路值得完整走一遍因为它串起了前面所有模块。我在复盘时把这条链路叫做“一枪的完整生命周期”。第1步客户端输入检测玩家按下鼠标左键客户端本地播放开火动画和音效同时向服务器发送一个“ShootRequest”命令包里面包含// 开火请求数据结构 public struct ShootRequestData { public Vector3 Origin; // 枪口位置 public Vector3 Direction; // 瞄准方向 public float ClientTime; // 客户端时间戳用于延迟补偿 }Origin是枪口世界位置Direction是镜头朝向ClientTime用于服务器侧时间回退计算。第2步服务器校验服务器收到请求后先查“冷却时间是否合法”“弹药是否足够”再查Origin和Direction是否合理如Origin不能离玩家当前位置超过一定距离。校验不通过则直接丢弃。第3步服务器执行命中检测服务器从Origin沿Direction打一条射线检测第一个碰撞体。检测顺序优先近处的玩家因为近大远小实际命中优先级也应按距离来决定。第4步伤害结算与广播若命中目标实现IDamageable服务器调用TakeDamage。TakeDamage内部更新血量、检查是否死亡然后通过RPC广播对命中者显示命中反馈伤害数字、准星变色对被命中者显示受击反馈屏幕泛红、血量更新对所有玩家若被命中者死亡广播击杀公告这一步设计成“服务器事件驱动”而不是“客户端定期轮询”省掉大量无谓的状态同步流量。4.7 玩家重生与对战流程从单杀到一整局单次击杀跑通之后还需要把单个战斗事件串成完整“对局”。我做了三个状态来管理对局生命周期状态触发条件核心操作准备中玩家连接成功后显示大厅等待玩家就绪进行中准备倒计时结束生成Pawn开启战斗倒计时结束达到分数上限或时间耗尽锁定Pawn显示结算UI返回大厅重生逻辑的要点是“延迟重生位置选择”。死亡后立刻重生会让死亡失去意义也会给敌人带来“杀不完”的挫败感。先让玩家等待几秒再在随机安全点生成新Pawn。对局逻辑写在服务器上用事件驱动的方式监听玩家死亡事件和倒计时触发这样无需在客户端做任何权威判断。4.8 新玩家中途加入处理全量状态同步多人对战的常态是“边打边来人”。新玩家加入时需要什么信息答案是“所有当前状态”包括所有玩家的位置和血量当前的武器配置和弹药当前对局阶段是不是已经打起来了比分情况PurrNet的自动同步机制对“持续变化的状态”很拿手但对“加入时的初始快照”没有内置方案。我采取的方式是服务器维护一个“GameState”数据结构里面存放所有需要同步的状态实体。新玩家连接成功后服务器主动把这个GameState快照发送给他。后续的增量变化通过PurrNet的正常机制同步。设计要点是“快照完整但不大”。位置可以用半精度浮点压成16位数字血量控制在255以内用一个字节表示。每次快照控制在几百字节内对网络影响几乎可以忽略。5. 常见问题与排查技巧实录5.1 角色抖动与“橡皮筋”问题排查症状其他玩家的角色在自己的屏幕上无规律地跳来跳去或者走几步回跳一下。排查思路检查网络Transform是否用的是直接赋值。如果是先改成插值方案。检查同步频率设置是否过低。10Hz以下的同步会让插值间隔太长需要把插值速度调高。检查是否发生“双写冲突”——既有网络同步赋位置又有本地物理驱动Transform。二者同时改同一个Transform必然抖动。我的调试工具很简单在角色脚下画一条轨迹线记录最近两秒的位置更新点。如果更新点之间距离远大于每帧移动距离说明网络瓶颈如果更新点稀疏但平滑说明插值策略是对的。5.2 客户端A能连上但客户端B连不上症状一个客户端能正常加入游戏另一个死活连不上报错提示连接超时或拒绝连接。排查步骤确认两个客户端的版本一致特别是PurrNet版本的序列化协议不能跨版本。查看服务器日志是否收到连接请求。如果服务器日志完全没动静大概率是网络层被防火墙或路由器阻挡。用的是本地IP还是127.0.0.1局域网测试时写成本机局域网地址跨网测试时要走公网IP或内网穿透。这里有个开发期常用的调试技巧先在一台机器上起多个客户端自测确认逻辑无问题再开第二台机器测真实网络环境。分步排查能快速定位是“逻辑问题”还是“环境问题”。5.3 延迟高时射击不中的原因分析排除操作因素后还总是打不中大概率是命中判定没有做延迟补偿。前面写过核心是服务器在玩家开火那一刻要回溯到“玩家当时看到的位置”而不是用“收到请求时世界的最新位置”。我做了一个很有效的实验验证这个问题把服务器物理模拟暂停1秒让两个玩家模型保持相对静止然后在某一端射击。如果“看到命中但服务器判未命中”说明延迟补偿阈值需要加大如果“没看到命中但服务器判命中”说明阈值过大。用这个办法反复调整能很快找到项目合适的补偿值。5.4 服务器内存/带宽压力优化这一套架构跑下来最多遇到过几百个同步对象推送全量Transform带宽直接被打满。优化手段按照收益从高到低排列降低同步频率角色Transform从30Hz降到15Hz视觉几乎无差异带宽减半。只同步必要的字段向量位置用打包格式发送x、y、z各压缩为16位旋转只同步一个Y轴偏航角不要同步三个轴的完整旋转。区域性剔除超出一定距离的玩家不推送Transform同步。FPS对战里隔着100米推送对方位置完全没意义。批量RPC把短时间内的松散事件攒成一批再发送而不是每个命中都单独发一个包。5.5 作弊防护的初级认知边界要说清楚一点没有任何网络方案能100%防作弊尤其是客户端始终持有本地内存的情况下。服务器权威只能防住“改网络数据”类外挂防不住“读内存看敌人位置”的透视挂。我的建议是所有判定逻辑在服务器这是底线。服务器对客户端上报的输入做合理性校验。开火方向与镜头朝向偏差过大时判定为可疑行为。不做过度设计——独立游戏的玩家群没大到外挂开发者愿意投入的程度先把核心玩法做好防作弊是持续迭代的事。6. 扩展方向与经验沉淀6.1 从单机原型到可发布Demo的检查清单当这个框架跑通“4人局域网对战击杀重生比分”以后可以按这个清单检查是否可以对外发布[ ] 服务器和客户端是否为不同构建目标如服务器用Linux无头构建、客户端用Windows[ ] 是否有断线重连机制[ ] 是否处理了玩家中途退出后Pawn的清理[ ] UI是否适配不同分辨率[ ] 是否有基础的音效和特效反馈缺一个都会让玩家体验大打折扣。特别是断线重连我最初没做测试时直接拔网线再插回去角色动不了也没提示玩家会以为是卡死了。6.2 从FPS框架向吃鸡/爆破模式的扩展路线这套框架完全可以支撑后续扩展。吃鸡模式需要做的主要是“缩圈系统”服务器驱动一个缩圈区域对圈外玩家持续伤害和“物品掉落”系统共享的武器拾取点。爆破模式则需要引入“目标点”概念服务器维护A/B点的占领状态胜负判定从“击杀数”改成“目标达成”。扩展的关键是“状态机复用”。现成的对局状态机稍作扩展加一个阶段枚举值“缩圈中”就能跑通吃鸡把胜利判定条件改一下就能变成爆破模式。6.3 关于网络同步频率的最终经验值用这套框架踩过一轮坑之后我总结出下面几个推荐经验值角色位置同步频率15~20HzFPS够用竞技端游可能要到30Hz但作为独立项目15Hz起步够了镜头旋转同步频率20~30Hz旋转对延迟更敏感因为人眼对旋转的卡顿比位移更敏感血量同步事件驱动非帧同步对局状态变化事件驱动非帧同步实际操作中我一上来就用了很高的同步频率觉得“拉满就是快”。结果带宽跑满手感和20Hz完全相同。同步频率不是越高越好够用就行。6.4 最后分享一个小技巧同步数据结构的版本标记网络协议最大的噩梦是“前后端数据结构对不上”。客户端连上服务器服务器用的是Version 2的数据结构客户端还是Version 1一收包就崩。解决办法是在握手时互相交换版本号不一致就给出友好的错误提示“客户端版本过旧请更新游戏”而不是让玩家面对红屏报错。这看似是小事但实际做过联机项目的人都知道版本不兼容是社区反馈里最常见的问题之一。在网络层做一层防御比事后发补丁成本低多了。从第一天规划Player和Pawn的分层再到服务器权威判定、延迟补偿、状态同步粒度的把控这一套走完我感觉自己最大的收获不是“会调用某个网络库的API了”而是真正理解了“联机游戏的设计是围绕数据所有权展开的”。每个数据都要问一句谁拥有它的最终解释权谁来产生它谁来消费它想清楚这三点换个游戏类型、换个网络库骨架都是通用的。