player.rpc("take_damage", 25) 这行代码在本地看起来只是个函数调用标注,实际发生时引擎做了五件事:查 RPC 配置、校验权威、编码包头、压缩参数、塞进传输通道。这篇把这条链跟到底,重点看包到底长什么样——理解了字节布局,带返回值的 RPC、call_local 的行为、以及"为什么 RPC 集合不一致会直接报错"就都顺理成章了。
编码链:从脚本语句到 peer 缓冲
调用入口在 Node::rpc(),委托给 SceneMultiplayer::rpcp(node, method, args, mode, channel)(scene/main/scene_multiplayer.cpp)。链条是这样:
- 配置查询:从脚本元数据里查这个方法的 RPC 配置(能不能远端调、谁有权调、可不可靠、走哪个通道)。没有
@rpc注解的方法在这里直接失败——注解不是装饰,是白名单登记; - 权威校验:
p_mode == RPC_MODE_AUTHORITY时检查调用方声明的 authority 是否和节点当前的multiplayer_authority匹配(七-1 说过,这是声明校验不是身份证明); - 方法名 → 整数索引:用脚本里的 RPC 方法表把方法名解析成索引号。这就是包里没有方法名字符串的原因;
- 参数压缩:每个参数过
encode_and_compress_variant(),首字节元信息 + 按值域分档的整数编码(七-2 那篇逐行读过,这里不重复); - 入队发送:目标 peer 列表逐个
put_packet()。
call_local 模式的语义在这一步体现:本地执行不是"发给自己再收回来",而是当场同步调用函数体,和网络发送并行发生。这意味着本地执行顺序在包到达远端之前——依赖"本地先跑还是远端先跑"的顺序保证的逻辑都是错的。
包格式
一个 RPC 包的布局(字段宽度以源码为准,这里画的是结构关系):
注意这里有两个压缩层次叠加:节点寻址用 NodePath 缓存整数(七-1),方法名用脚本 RPC 表的整数索引,参数用 Variant 值域分档(七-2)。三层压缩各管一段,共同效果是一个语义完整的远端函数调用只花几个字节。量级感:每帧一次的位置同步 + 偶发的事件 RPC,万行代码的游戏流量大头永远是前者——事件 RPC 的这点编码开销从来不是瓶颈,纠结"RPC 会不会太慢"之前先 profile 同步列表。
方法索引为什么必须配 checksum
方法索引在包里的代价是:它只在两端 RPC 表完全一致时才有意义。索引是表内序号,同一个索引在两份不同的脚本里可以指向完全不同的方法——参数按错误的签名解码,轻则崩溃重则静默错数据。
引擎的对策是整表 checksum:连接建立时两端交换各自全部 RPC 方法集合的哈希(函数名、参数、@rpc 注解配置全部参与),不一致直接断开并报错。这就是为什么报错信息常常指向一个你根本没在调的函数——校验是整表粒度的,任何一个函数的注解改了,整个哈希就变(七-2 里记过一笔,这里把原因补齐:逐函数校验要把函数级哈希塞进每个包,协议变胖;整表校验把一致性变成连接的前置条件,运行时每个包零校验成本)。
工程推论:只改 @rpc 注解的某次热更,会让新旧客户端互不兼容,比改函数体危险得多——函数体不影响 checksum,注解影响。灰度发布和 mod 生态都要注意这一点。
带返回值的 RPC
4.x 支持远端函数返回值:var hp = await player.rpc_id(1, "query_hp")。实现上是两条独立消息:调用包照旧发出去,收端执行完把返回值单独打成一个响应包带调用 id 送回来,调用端挂起协程等这个 id。几个由实现推出的使用约束:
- 不可靠通道上的返回值 RPC 可能永远等不到——响应包和调用包一样受 transfer mode 约束,超时没有内建重试;
- 开销是双倍的(去程 + 响应),高频查询别用返回值 RPC 模式,把"查询-应答"改成服务器主动推的同步属性更省;
- 协程挂起期间如果节点被释放,
await永远等不到结果也不会报错,记得在退出树时取消。
校验与调试速查
- 两端 RPC 表 checksum 不一致:连接直接失败,报错列出不一致的函数——改注解导致的,对齐两端脚本;
- "Function not found in RPC config":注解没写或写错位置(注解必须在远端也存在的脚本方法上);
- 方法被调但 authority 校验失败:
remote_sender和节点 authority 不匹配,检查set_multiplayer_authority的时机是否在 spawn 之前; - 想看真实字节流:
ENetConnection层抓包(Wireshark 对 ENet 不友好,直接在SceneMultiplayer的收发包回调里打印更实际)。
本文基于 Godot 4.2–4.4 源码,关键文件:scene/main/scene_multiplayer.cpp(rpcp / send_rpc)、scene/main/node.cpp(rpc() 入口)、core/io/multiplayer_api.cpp。包内字段宽度以版本为准,结构关系不变。