news 2026/9/20 12:43:11

Netcode for Entities 实战指南:如何让 ECS 多人同步驯服延迟、告别橡皮筋

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Netcode for Entities 实战指南:如何让 ECS 多人同步驯服延迟、告别橡皮筋

Netcode for Entities 实战指南:如何让 ECS 多人同步驯服延迟、告别橡皮筋

【免费下载链接】EntityComponentSystemSamples项目地址: https://gitcode.com/GitHub_Trending/en/EntityComponentSystemSamples

你按下射击键,角色 0.3 秒后才开火;松开摇杆,角色又往前滑了两米。如果你曾在多人游戏里被这种手感卡住,那 ECS 网络同步就是绕不开的一课。Netcode for Entities 是 Unity 官方给出的 DOTS 生态答案:服务器持有唯一真实状态,客户端负责"即时性",两者之间隔着一层薄而精确的网络协议。这里的示例全部来自官方示例仓库的 NetcodeSamples 与 Dots101/Netcode101 目录,可以打开编辑器跟着跑。

想象这一群鱼,每条鱼都是一个玩家角色——你要的是:所有客户端看到的"鱼群"一致,但每个人自己的操作按下键就生效。

按了射击键 0.3 秒才开火:多人游戏卡在哪道坎上

⚠️ 多人项目的第一个坑几乎从来不是"发不出去",而是"发出去但不一样"。三类抱怨会在每个项目里重复出现:开枪的人说"我明明先开的枪",挨打的人说"我明明躲开了",围观的人说"为什么你们俩看到的画面不一样"。

这三句抱怨背后是同一个矛盾:服务器要花时间验证和计算,你想要"服务器说了算",就得付出等待的代价。Netcode 的选择不是二选一,而是让两边同时跑——客户端先"演一遍",服务器后"复述一遍",两边对不上时,由框架把差异平滑掉。整个架构因此围绕三个角色运转:服务器 World 跑全部权威逻辑;客户端 World 做预测、插值和渲染;中间的 NetStream 通道把命令往上送、把快照往下送。

你不需要手工搭这三样东西。仓库里 Dots101/Netcode101 的 Kickball 示例就是一个完整的最小闭环:同一台编辑器开两个客户端,一个玩家踢出球,另一个立刻看到。上手时建议先跑一遍 Kickball 再读代码,体感会比文字清楚得多。

接受了"服务器说了算",紧接着的第一个具体问题就是:哪些数据值得过网络?这是同步的地基。

同步地基:三步定好 Ghost 同步类型

新手最常问的一句话是:生命值要不要标 [GhostComponent]?速度要不要?枪口火花要不要?全标上,带宽账单爆炸;标少了,客户端连画面都画不出来。先打个比方——Ghost 同步类型就像群聊里的 @ 规则:AllPredicted 是"@所有人,而且你能预览我发的内容",Server 是"群主单方面宣布,其他人只管收",Client 是"你自己的草稿,谁也别想看"。

游戏里的数据其实只有两种:状态(你站在哪、还剩多少血)和命令(这一帧我做了什么)。Netcode 把两者统一成 Ghost 组件:实现 ICommandData 的是命令,其余是状态;命令身上还挂着一个 NetworkTick,告诉服务器"这条命令属于哪一帧"。

这份数据在干什么谁能写谁能读该选的类型
玩家输入(摇杆、开火)本地客户端所有人(服务器拿它回放)AllPredicted + ICommandData
角色位置与速度服务器算、客户端预测所有人渲染AllPredicted
血量、分数、击杀数服务器所有人Server
本地 UI、粒子、屏幕震动本机客户端仅本机Client

第一步是定义消息。Asteroids 示例里飞船的操纵键就是下面这样——关键看两处:[GhostComponent] 属性决定同步策略,ICommandData 接口告诉 Netcode"这是命令不是状态"。

[GhostComponent(PrefabType = GhostPrefabType.AllPredicted)] public struct ShipCommandData : ICommandData { public NetworkTick Tick { get; set; } // 关键:标记这条命令属于哪一帧 public byte left; public byte right; public byte thrust; public byte shoot; }

四个动作各用一个 byte 装下,是省带宽的小例子;快照里实际只会发送变化过的字段,这一点在带宽账单部分再拆开说。源码在 ShipCommandData.cs。

第二步是把按键写进缓冲。Kickball 的输入系统是个标准模板,注意 GhostInputSystemGroup 和 GhostOwnerIsLocal 两处:前者保证写入发生在 Netcode 规定的输入阶段,后者保证只改本地玩家的缓冲,不会动到别人输入副本。

[UpdateInGroup(typeof(GhostInputSystemGroup))] public partial struct PlayerInputSystem : ISystem { public void OnUpdate(ref SystemState state) { var moveValue = InputSystem.actions.FindAction("Move").ReadValue<Vector2>(); // 关键:只写本地玩家的输入缓冲 foreach (var input in SystemAPI.Query<RefRW<PlayerInput>>() .WithAll<GhostOwnerIsLocal>()) { input.ValueRW.Horizontal = moveValue.x; input.ValueRW.Vertical = moveValue.y; // ... 此处省略约 10 行按键检测(如踢球标志位) } } }

写完输入后就不用再想"怎么发",Netcode 会自动打上 tick 号发往服务器。第三步最容易被忘掉:把角色预制体注册进 Subscene 的 EntityPrefabs——不注册的话,服务器不知道这个实体长什么样,客户端收到的只会是一串没法定型的 ID。源码见 PlayerInputSystem.cs。

数据流向定好之后,下一个问题就是手感:玩家不想等服务器那一个来回。

手感优先:预测半径调多大才不橡皮筋

角色比你的按键慢 100 毫秒,你就会觉得"这不是我的角色"。预测的做法像"点外卖先自己炒一盘吃上,外卖到了再对账":玩家先吃(先看到),对账后置。

完整链路是这样:按下键 → 客户端当场本地预测出移动,本帧就生效 → 命令同时带 tick 上行 → 服务器用权威逻辑重放同一帧 → 快照下行 → 客户端把"我预测的"和"服务器说的"对账。对得上,玩家毫无感知;对不上,Netcode 把误差摊到几帧里修正。这套"先演后对账"就是客户端预测回滚机制。

预测不是免费的,每个预测实体都要让客户端多跑一份模拟,所以不是场上每个实体都值得预测。Asteroids 示例用两个并行 Job 做"近处预测、远处插值",注意它用了两个半径:

var queues = SystemAPI.GetSingletonRW<GhostPredictionSwitchingQueues>().ValueRW; // 半径内:把 ghost 切到"预测" new SwitchToPredictedGhostViaRange { playerPos = playerPos, enterRadiusSq = settings.predictionRadius * settings.predictionRadius, predictedQueue = queues.ConvertToPredictedQueue, // ... 此处省略约 5 行并行命令缓冲初始化 }.ScheduleParallel(); // 退出半径 = 预测半径 + 余量,防跨界状态抖动 new SwitchToInterpolatedGhostViaRange { playerPos = playerPos, exitRadiusSq = (settings.predictionRadius + settings.predictionRadiusMargin) * (settings.predictionRadius + settings.predictionRadiusMargin), interpolatedQueue = queues.ConvertToInterpolatedQueue, // ... 此处省略约 5 行并行命令缓冲初始化 }.ScheduleParallel();

💡 半径怎么定?给个实操公式:覆盖"最坏往返时间里角色能跑出的距离"(100ms ping × 10m/s ≈ 一两米)再加余量,margin 取半径的 1.2~2 倍。两个半径错开就是"不橡皮筋"的关键:同一阈值切换时,玩家贴着边界走会逐帧在预测/插值之间反复横跳,错开之后进入和离开各只触发一次切换。自己的角色建议永远预测,其他角色交给半径。源码在 AsteroidSwitchPredictionSystem.cs。

服务器和预测对不上时,Netcode 手里握着每个实体的预测历史:快照一到就回滚到对应 tick、重放几步,差异小就按 TransitionDurationSeconds 摊开,差异大(通常是服务器端物理或延迟造成)就先瞬移再混合。玩家只会觉得"轻轻一推",而不是"被传走了"。

有了预测,也少不了跟服务器"打个电话"——玩家要求进对局、服务器要给某个特定客户端单独推消息时,靠的就是 RPC。

跨端对话:RPC 从定义到广播的完整链路

RPC 像在群里寄一封实体信:把内容(命令结构体)写进信封,Netcode 负责投递和回执。完整链路五步:定义实现 IRpcCommand 的消息结构体 → 客户端造一个临时实体,挂上消息 + SendRpcCommandRequest 发出 → 服务器端它变成一个带 ReceiveRpcCommandRequest 的实体 → 你的系统处理,单播(设置了 TargetConnection)或广播(不设置)→ 处理完销毁实体。

flowchart LR A[定义 IRpcCommand 结构体] --> B[客户端: 临时实体<br/>+ SendRpcCommandRequest] B --> C[Netcode 网络传输] C --> D[服务器: 实体携带<br/>ReceiveRpcCommandRequest 出现] D --> E{发送方式} E -->|TargetConnection 已设置| F[单播: 仅该连接收到]

【免费下载链接】EntityComponentSystemSamples项目地址: https://gitcode.com/GitHub_Trending/en/EntityComponentSystemSamples

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/20 12:42:51

MCP 挂 Codebase-Memory 后 Agent 报 401?TaoToken 校正 Base URL

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/20 12:42:26

用 1Panel 应用商店一键部署 DBX:15MB 轻量级数据库客户端自托管指南

用 1Panel 应用商店一键部署 DBX&#xff1a;15MB 轻量级数据库客户端自托管指南 【免费下载链接】dbx 15MB&#xff0c;轻量级跨平台数据库客户端、数据库管理工具。支持 MySQL、PostgreSQL、SQLite、Redis、MongoDB、DuckDB、ClickHouse、SQL Server 等。15MB, lightweight, …

作者头像 李华
网站建设 2026/9/20 12:40:04

Java+Vue构建可解释羽毛球技战术分析系统

简介&#xff1a;这是一份面向Java与Vue全栈开发者、体育数据分析研究者及高校相关专业学生的智能体育系统实战项目&#xff0c;聚焦羽毛球落点预测与技战术深度分析&#xff0c;解决赛事复盘低效、训练决策缺乏数据支撑、国产智能分析工具稀缺等实际问题。资源为1个85KB的docx…

作者头像 李华
网站建设 2026/9/20 12:39:56

改进PSO算法在光伏MPPT中的应用与优化

1. 光伏系统MPPT技术背景解析光伏阵列在实际运行中常面临局部遮阴问题——当部分电池板被树木、建筑或云层阴影遮挡时&#xff0c;整个系统的输出特性曲线会出现多峰现象。传统MPPT算法如电导增量法或扰动观察法在这种工况下极易陷入局部极值点&#xff0c;导致发电效率显著下降…

作者头像 李华
网站建设 2026/9/20 12:38:06

伺服增益参数简述及其调整

参考文献 [1] 颜嘉男. 伺服电机应用技术[M]. 第1版. 北京:科学出版社, 2010. [2] 台达. ASDA-B2伺服手册[EB/OL]. [3] 陈国强, Putra A S. 工业自动化中的驱动与控制[M]. 第1版. 北京:机械工业出版社, 2016 :86-105. [4] 汇川技术. IS620F系列伺服用户手册[EB/OL]. 相关内容回…

作者头像 李华