Godot 多人联机入门:从「一个人玩」到「两个人连上同一局」
前 43 天我们做的几乎都是单机:角色能动、敌人会追、Boss 会读招。但 Steam 上最容易被玩家复购、也最容易被主播带火的一类游戏,是能和朋友一起玩的。今天我们把游戏从「一个人玩」推进到「两个人连上同一局」——不讲玄学,只贴着 Godot 官方的 High-level multiplayer 文档,把最小可运行的联机雏形搭出来。
今日速览
- 游戏网络首选 UDP,不是 TCP:TCP「可靠、有序、但慢」,UDP「不可靠、乱序、但快」。游戏要的是低延迟,所以 Godot 用改良版 ENet(基于 UDP)在高层重建了「可选的可靠 + 可选的有序」。
- 高层 API 把活儿干了:
SceneTree自带的MultiplayerAPI,你只要造一个MultiplayerPeer(默认ENetMultiplayerPeer)并create_server()/create_client(),剩下的握手、分包、重传都交给引擎。 - 服务器 ID 永远是 1,客户端拿到一个随机正整数 ID;谁是谁,全靠这个 ID。
- 两种同步武器:RPC(事件,如「我开了枪」)+ 状态同步(如每帧广播位置)。事件用 RPC,连续变化的状态用「定时广播 + 插值」。
- 默认「主机权威」:
@rpc的默认模式是authority,也就是「只有服务器说了算」。玩家客户端只上传输入,由主机计算并广播结果——这是防止作弊的第一道墙。
核心学习内容
1. 先选协议:为什么游戏用 UDP
TCP 保证「一定送达、顺序不变」,代价是延迟和缓冲(还带 Nagle 算法),适合网页请求,不适合你在 60 帧里丢一颗子弹。UDP 相反:发了就忘、可能丢、可能乱序,但快。
Godot 的答案是在 UDP 之上用 ENet 重建游戏需要的那部分可靠性:你可以按需选择 reliable(必达且有序,如「买装备」)、unreliable(能丢,如位置广播)、unreliable_ordered(丢弃过时包)。关键认知:不是「越可靠越好」,而是「按消息重要性选传输模式」。
2. 初始化网络:三行代码开房 / 进房
高层网络由 SceneTree 管理,每个节点都有个 multiplayer 属性。开房和进房只差一个方法:
# 主机(服务器)
var peer = ENetMultiplayerPeer.new()
peer.create_server(PORT, MAX_CLIENTS) # 例:7000, 20
multiplayer.multiplayer_peer = peer
# 客户端
var peer = ENetMultiplayerPeer.new()
peer.create_client(IP_ADDRESS, PORT)
multiplayer.multiplayer_peer = peer
# 关闭联机
multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new()
连上之后,用信号感知人员进出:peer_connected(id)、peer_disconnected(id);客户端还会收到 connected_to_server() / connection_failed() / server_disconnected()。判断自己是主机还是客人,用 multiplayer.is_server();取自己的 ID 用 multiplayer.get_unique_id()。
局域网可以用
127.0.0.1或192.168.x.x直接连;要让公网连进来,得在路由器上转发 UDP 端口(Godot 高层 API 只走 UDP,别只转 TCP)。
3. 两把同步武器:RPC 与状态同步
RPC(远程过程调用) 用来同步「事件」。给函数加 @rpc 注解,就能用 某函数.rpc(...) 在所有 peer 上调用,或 rpc_id(1, ...) 只发给服务器。注解可带四个参数:
@rpc("authority", "call_remote", "reliable", 0)
mode:"authority"只有权威方(默认是服务器)能调;"any_peer"允许客户端调用(上传玩家输入就该用这个)。sync:"call_remote"本地不执行;"call_local"本地也执行(服务器自己也是玩家时必用)。transfer_mode:"reliable"/"unreliable"/"unreliable_ordered"。transfer_channel:通道号,必须放最后。聊天和战斗分开走不同通道,互不阻塞。
状态同步 用来同步「连续变化的东西」,如位置。做法是主机定时(如每秒 20 次)rpc 广播位置,客户端收到后用 Tween 插值平滑到目标点,避免一帧一跳。
⚠️ 两个必踩的坑:① RPC 的发送方与接收方,节点 NodePath 必须完全一致(名字也要一致),用 add_child(node, true) 强制可读名;② 加在某个脚本上的 @rpc 函数,另一端脚本也必须声明同签名的全部 RPC,签名含注解声明、函数、返回类型和 NodePath——少一个就会报一个和当前改动「看起来无关」的错。
4. 最小 Lobby:主机权威模型长什么样
官方 Lobby 示例的思路非常值得抄:用**一个 Autoload(Lobby)**装下「玩家字典 + 信号 + 开房/进房/开局」,UI 场景只负责发信号。核心几层是:
- Lobby 单例:
players = {}存所有玩家信息;开房时把自己(ID=1)也写进去。 - 握手信号:
player_connected / player_disconnected / server_disconnected,UI 订阅它来刷新玩家列表。 - 开局:等所有客户端都加载完场景后,主机调
Lobby.load_game.rpc(场景路径)一起进关。 - 权威原则:玩家只上传输入,主机算结果再广播。客户端不”自说自话”地改世界。
这一套下来,你已经有了联机游戏的骨架;后面加同步、加预测、加断线重连,都是往这个骨架上挂东西。
🎯 DeepSeek 提示词
提示词 1:生成可运行的联机 Lobby(开房 + 进房 + 玩家列表)
你是 Godot 4 资深网络程序员。请用 GDScript 帮我写一个「局域网联机雏形」,跑得起来、注释详细。
【目标】单人→双人:主机点「创建房间」开服,另一个客户端点「加入房间」连上,双方都能看到玩家列表。
【技术约束】
- 用 Godot 4.x 高层 API:SceneTree 的 MultiplayerAPI + ENetMultiplayerPeer(UDP)。
- 端口 7000,最多 20 人;局域网内用 127.0.0.1 测试。
- 用一个 Autoload 命名为 Lobby,内含:players 字典(key=peer id)、player_info、join_game(address)、create_game()、remove_multiplayer_peer();并定义信号 player_connected(peer_id, info) / player_disconnected(peer_id) / server_disconnected。
- 监听 multiplayer 的 peer_connected / peer_disconnected / connected_to_server / connection_failed / server_disconnected 全部信号,并打印日志。
- 服务器自身 ID 记作 1,开房后要把自己写进 players。
【输出格式】
1. 文件清单(每个脚本放哪个节点/场景)。
2. Lobby.gd 完整代码。
3. 一个最小 LobbyUI.tscn 的节点结构说明(按钮:创建/加入 + 一个玩家列表 Label)。
4. 如何用两个 Godot 实例本地测试的步骤。
【约束】不要用已废弃的 Godot 3 写法;不要自造网络层,只用 MultiplayerAPI。
提示词 2:主机权威的「输入上传 → 广播位置」同步
你是 Godot 4 网络同步专家。我要在已有联机 Lobby 基础上,实现「两个玩家能看到彼此移动」,要求主机权威,防简单作弊。
【目标角色】帮我把玩家角色做成可在网络间同步的 Player。
【任务与规则】
- 每个玩家角色脚本 Player.gd:本地玩家读取输入,非本地玩家禁用输入。
- 玩家只上传「输入」给主机(rpc_id(1, ...),@rpc("any_peer", "call_local", "reliable")),主机收到后用 get_remote_sender_id() 判断是谁,套用到对应角色上计算移动。
- 主机以 20Hz 广播所有玩家的位置(用一个 @rpc("authority", "call_remote", "unreliable") 函数),客户端收到后用 Tween 或 lerp 把角色平滑插值到目标位置,而不是瞬移。
- 每个角色用 set_multiplayer_authority(peer_id) 设置权威;只有本地玩家能改自己的输入。
【输出格式】
1. Player.gd 完整代码(含 _physics_process 里的输入分流逻辑)。
2. 说明哪些函数为什么选 reliable、哪些选 unreliable。
3. 一段「如何验证同步是否生效」的双实例测试步骤。
4. 列出 3 个最容易被忽略的坑(如 call_local、NodePath 一致、authority 设错)。
【约束】用 Godot 4.x GDScript;不要引入第三方库;注释解释每处 @rpc 参数。
提示词 3:审查 / 排查联机代码(RPC 签名、authority、NodePath)
你是 Godot 4 多人联机的 Debug 专家。我会贴给你一段联机代码或一个报错,请定位问题并给修复。
【排查清单】逐条检查并明确结论:
1. RPC 签名匹配:客户端脚本与服务器脚本是否声明了「同名同签名的全部 RPC」?(签名包含 @rpc() 参数、函数名、返回类型、NodePath;注意参数个数不参与校验这一点)
2. NodePath 一致:发送方与接收方节点路径/名称是否完全相同?add_child 是否传了 force_readable_name=true?
3. authority:@rpc 的 mode/sync 是否用对?(上传输入需 any_peer;服务器也是玩家需 call_local;广播结果用 authority)
4. transfer_mode:该 reliable 的(买装备/开局)和该 unreliable 的(位置)是否反了?
5. 生命周期:create_server/create_client 是否在 _ready 里、multiplayer_peer 是否真的被赋值、断线是否有处理?
【输出格式】
- 问题清单(每条:位置 → 现象 → 原因 → 修复代码)
- 修正后的完整代码
- 一句话总结「下次如何避免」
【输入】我会在下一行贴代码 / 报错信息:
<在这里粘贴你的代码或报错>
使用说明:提示词 1 用来从零搭骨架,提示词 2 用来补最核心的同步逻辑,提示词 3 用来救火。贴给 DeepSeek 时把
<在这里粘贴…>换成你的实际代码/报错,并附上 Godot 版本号,效果最好。
学习心得
回头看「用 DeepSeek 从零做一款能上架 Steam 的游戏」这个目标,联机是风险最高、也最容易加分的模块。风险在于:网络代码 bug 极难复现、极难调试;但它一旦跑通,「支持多人合作/对战」几乎是 Steam 商店页最有力的卖点之一,也是让玩家愿意反复开游戏的燃料。
好消息是,Godot 的高层 API 已经把最难的部分(ENet 握手、分包、重传、通道)封装好了,你要写的其实是三件事:初始化 peer、用 RPC 同步事件、用定时广播同步状态,外加一条铁律——主机权威。而这三件事,恰好都是 DeepSeek 最擅长替你写「模板」的部分:你把约束(端口、模式、传输方式、call_local、authority)交代清楚,它就能给你一份八九不离十的骨架,你再在编辑器里跑两个实例验证。先做一个能连上的「走路 Demo」,再谈同步、预测和断线重连——别一上来就想着做联机对战。
明日关注
- 用提示词 1 亲手跑通一个「双实例能连上、能看到 2 个玩家」的最小 Demo。
- 给玩家位置同步加上插值(Tween/lerp),对比「瞬移」和「平滑」的手感差异。
- 查一下 Godot 的
MultiplayerSynchronizer/SceneReplicationConfig节点,看官方如何用声明式配置替代手写状态广播。
本文由自动化学习流水线每日生成,坚持即学即记。
评论