引言
多人游戏最难的不是「连上」,而是「同步」——每个人的屏幕都要看到同一个世界。本文系统讲 Defold 多人联机:先做架构选型(P2P 与客户端-服务器谁适合你),再讲核心的「权威服务器」模型(为什么服务器说了算),接着覆盖状态同步方案(完整状态 vs 增量)、位置插值与预测、房间与匹配、Defold 里的网络接入(WebSocket/HTTP),最后处理延迟补偿、断线重连与防作弊。
前置:/defold-game-engine-complex-logic-state-management/(消息路由与同步)、/defold-cross-platform-publish/(平台网络)。网络协议见 [[network]]。
目录
- 1. 多人游戏的核心难点:同步
- 2. 架构选型:P2P vs 客户端-服务器
- 3. 权威服务器:为什么服务器说了算
- 4. 状态同步:完整状态 vs 增量更新
- 5. 位置插值:让移动丝滑
- 6. 预测与回滚:低延迟手感
- 7. Defold 网络接入:WebSocket 与 HTTP
- 8. 房间与匹配系统
- 9. 断线重连、延迟处理与防作弊
- 10. 速查表
- 延伸阅读
1. 多人游戏的核心难点:同步
玩家 A 移动,玩家 B 怎么知道?——网络同步的本质:
A 端:本地模拟 → 发送「状态/输入」→ 网络延迟
B 端:收到 → 渲染对方状态
问题:延迟让 B 看到的 A 是「过去的 A」
同步的三个核心问题:
| 问题 | 说明 |
|---|---|
| 一致性 | 各端世界状态是否相同 |
| 延迟 | 传输往返时间(RTT) |
| 带宽 | 每秒要传多少数据 |
游戏类型决定同步需求:
回合制/卡牌:低频同步(每回合一次)
实时竞技:高频同步(每帧输入)
MMO:分区 + 只同步附近玩家
心智:同步 = 在「一致性、延迟、带宽」三者的权衡——回合制传状态、实时传输入、MMO 分区。
2. 架构选型:P2P vs 客户端-服务器
两种网络拓扑:
P2P:玩家直接互联(无服务器)
优点:零服务器成本、低延迟
缺点:无权威、防作弊难、NAT 穿透难
客户端-服务器:一个权威服务器,所有客户端连它
优点:权威、好防作弊、实现简单
缺点:要服务器、增加一跳延迟
选型决策:
| 场景 | 架构 |
|---|---|
| 小范围联机(局域网/朋友局) | P2P |
| 上架游戏/对抗竞技 | 客户端-服务器 |
| Web 游戏 | 服务器(浏览器对等难) |
Defold 生态现实:
Defold 没有内置网络库 → 常用方案:
1. 自建 WebSocket 服务器(Go/Node)
2. 第三方联机服务(Nakama/Photon 等)
3. Web 端用浏览器 WebSocket
记忆:没有服务器就没有「裁判」——上架游戏几乎都走客户端-服务器,权威才是防作弊的根。
3. 权威服务器:为什么服务器说了算
核心原则:服务器拥有最终世界状态:
客户端发「输入」而不是「结果」
服务器收到 → 用权威逻辑计算 → 广播「结果」给所有人
客户端只渲染服务器结果
为什么:
1. 防作弊:客户端不能改自己血量/位置(它只是提案)
2. 一致性:所有人以服务器状态为准
3. 简化:逻辑集中一处,各端不用各自算
消息流:
客户端 A:发送 { input: move_right, ts: 1234 }
服务器: 校验 → 计算新位置 → 广播 { pos, hp, ts }
客户端 A/B:收到 → 渲染
记忆:权威服务器 = 客户端「提议」、服务器「裁决」——客户端想什么不重要,服务器说是什么就是什么。
4. 状态同步:完整状态 vs 增量更新
两种同步数据的粒度:
完整状态(snapshot):每帧/每 N 帧发全部位置血量
优点:简单、掉包自愈
缺点:带宽大
增量更新(delta):只发变化的部分
优点:带宽小
缺点:掉包要请求补帧
消息压缩:
-- 状态消息示例(Lua 侧序列化)
local function pack_state(self)
-- 只传变化的实体
local changes = {}
for _, e in ipairs(self.entities) do
if e.dirty then -- 标记了「变了」
table.insert(changes, {
id = e.id,
x = e.pos.x, y = e.pos.y,
hp = e.hp,
})
e.dirty = false
end
end
return changes
end
节奏选择:
| 类型 | 同步频率 | 带宽 |
|---|---|---|
| 回合制 | 事件触发 | 极小 |
| 实时(位置) | 10-20Hz 状态或 60Hz 输入 | 中 |
| MMO | 分区内 5-10Hz | 受控 |
记忆:完整状态简单抗丢包、增量省带宽——小游戏用完整状态,实体多了再上增量。
5. 位置插值:让移动丝滑
对方移动为什么「跳」?——收到的位置是离散的。插值让它连续:
收到对方位置 P1(时间 t1)
下一帧收到 P2(t2)
中间帧 → 在 P1 和 P2 之间插值渲染
插值实现:
-- 用 vmath.lerp 在两次状态间插值
function update(self, dt)
-- 每收到新位置都推入缓冲
self.lerp_t = math.min(self.lerp_t + dt * self.interp_speed, 1)
local a, b = self.state_prev.pos, self.state_next.pos
local render = vmath.lerp(self.lerp_t, a, b)
go.set_position("." , render)
end
-- 收到新状态:把 prev 推进到 next,载入新 next
function on_net_state(self, msg)
self.state_prev = self.state_next
self.state_next = { pos = msg.pos, t = msg.ts }
self.lerp_t = 0
end
| 方案 | 手感 |
|---|---|
| 无插值 | 瞬移跳变 |
| 线性插值 | 平滑但迟滞 |
| 带延迟缓冲 | 平滑 + 稳 |
记忆:插值 = 在「上一个状态」和「最新状态」间补帧——对方移动从瞬移变成滑行。
6. 预测与回滚:低延迟手感
问题:权威服务器 + 网络延迟 = 输入到看到要一个 RTT,操作有「黏滞感」。
解法:客户端预测:
1. 客户端本地立即执行输入(不等服务器)→ 即时响应
2. 同时把输入发给服务器
3. 服务器返回权威状态 → 与本地预测比对
4. 不一致 → 回滚(reconciliation)重放正确状态
预测示例(本地即时移动):
-- 本地即时模拟移动(预测)
function on_input(self, action_id, action)
if action.pressed then
-- 立即移动 + 记录输入(带序号)
move_local(self, dir)
table.insert(self.pending_inputs, { dir = dir, seq = self.seq })
self.seq = self.seq + 1
end
end
记忆:预测治「迟滞」、回滚治「偏差」——本地先动、服务器裁决、对不上就重放,手感就回来了。
7. Defold 网络接入:WebSocket 与 HTTP
Defold 没有内置网络库——用 websocket 扩展或 HTTP:
WebSocket(实时双向):
-- 使用 websocket 扩展
local socket = websocket.create(url, {
on_open = function(ws) print("连接成功") end,
on_message = function(ws, msg) handle_net_msg(self, msg) end,
on_error = function(ws, err) print("错误:", err) end,
on_close = function(ws) print("断开") end,
})
-- 发送消息
socket.send(json.encode({ type = "move", x = 10, y = 20 }))
HTTP(请求-响应,低频):
-- 用 http 扩展做登录/排行榜
http.request("https://api.example.com/login", "POST",
json.encode({ user = "abc", token = "xyz" }),
function(status, body)
if status == 200 then self.logged_in = true end
end)
| 协议 | 适合 | 特点 |
|---|---|---|
| WebSocket | 实时联机 | 全双工低延迟 |
| HTTP | 登录/存档/匹配 | 简单可靠 |
记忆:实时联机用 WebSocket、低频逻辑用 HTTP——Defold 靠扩展,服务端配个 WebSocket 网关就齐了。
8. 房间与匹配系统
房间 = 一局游戏的独立会话:
创建房间 → 玩家加入 → 开始 → 结束 → 解散
服务器侧维护房间列表与成员
匹配流程:
1. 玩家请求匹配(可选参数:等级/地区)
2. 匹配服务找合适的房间/玩家
3. 双方确认 → 建立连接 → 开局
Defold 侧房间客户端逻辑:
-- 创建/加入房间(通过服务器 API)
local function join_room(self, room_id)
send_net("join", { room = room_id })
self.state = "joining"
end
function on_net_message(self, msg)
if msg.type == "joined" then
self.room = msg.room
self.state = "in_game"
start_game(self)
elseif msg.type == "player_left" then
handle_disconnect(self, msg.player_id)
end
end
记忆:房间 = 服务器维护的「会话名单」——创建/加入/退出都走服务器 API,客户端只管收发。
9. 断线重连、延迟处理与防作弊
断线处理:
-- 心跳 + 超时判定
function update(self, dt)
self.no_resp_t = (self.no_resp_t or 0) + dt
if self.no_resp_t > 5 then -- 5 秒无响应
self.state = "reconnecting"
reconnect(self)
end
end
-- 收到任何消息 → 重置计时
function on_net_any(self)
self.no_resp_t = 0
end
延迟显示与补偿:
延迟:显示 ping(服务器回显时间戳)
补偿:预测 + 插值缓冲(见第 5/6 节)
发包节流:移动输入本地合并,避免每帧发
防作弊手段:
1. 权威服务器(位置/血量服务器算)
2. 输入校验(移动速度上限、穿墙检测)
3. 服务器状态快照(可疑时回滚)
4. 加密与签名(防篡改消息)
> 铁律:**永远别信客户端**——权威服务器 + 输入校验 + 状态快照,三层防作弊缺一不可。
---
## 10. 速查表
| 需求 | 做法 |
|------|------|
| 架构 | 上架游戏走客户端-服务器 |
| 权威 | 客户端发输入、服务器裁决 |
| 同步粒度 | 小游戏完整状态、大项目增量 |
| 平滑 | 位置插值(lerp 缓冲) |
| 手感 | 客户端预测 + 服务器回滚 |
| 网络库 | WebSocket(实时)/ HTTP(低频) |
| 房间 | 服务器维护会话名单 |
| 断线 | 心跳超时 + 重连 |
| 延迟 | 显示 ping + 预测补偿 |
| 防作弊 | 权威 + 校验 + 快照 + 加密 |
**一句话记忆**:**多人联机 = 架构(客户端-服务器)+ 权威(服务器裁决)+ 同步(完整/增量)+ 平滑(插值)+ 手感(预测回滚);Defold 用 WebSocket 扩展接服务器、房间走 API、断线心跳重连;防作弊三层——权威服务器、输入校验、状态快照——别信客户端。**
---
## 延伸阅读
- /defold-game-engine-complex-logic-state-management/ — 消息路由与状态管理
- /defold-cross-platform-publish/ — 平台网络与打包
- [[network]] — TCP/WebSocket 协议
- [[nodejs]] — 用 Node 写联机服务器
- [[game]] — 多人游戏设计
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。