中文  |  English

济宁米多信息科技有限公司

浏览器里跑多人在线 3D 空间:同步、弱网、语音名额制的工程复盘

浏览器多人在线3D WebSocket实时同步 3D空间多人 弱网重连 位置同步插值 近场语音带宽 Three.js多人在线 AI玩家接入

摘要:浏览器端多人在线 3D 空间,难点从来不是"能不能连上",而是三件事:位置同步的手感、弱网重连之后还能不能互相看见、以及人多之后带宽与 CPU 怎么算账。本文按这三条拆开讲,并附上"把外部 AI 当成一个玩家接进同一条广播链路"的实现思路与 100 个 AI 同场时的实测账。
多人在线的坑,八成不在首次连接,而在第一万次断开重连。

一、先把问题定义清楚:三个真问题

多人 3D 房间的教科书式结构是"客户端发位置、服务器广播给其他人"。真做起来,落到三个具体问题上:

问题表面症状实际根因
同步手感别人一卡一卡地跳、像瞬移客户端直接把收到的坐标硬赋值,没有插值与容错
弱网恢复双方各自掉线后,重连上了却互相看不见服务端/客户端的在场状态没有重建
成本人一多就卡、语音一开就爆带宽没有把带宽和 CPU 变成可预算、可配置的量

第三条在浏览器端尤其明显:CPU 预算在玩家的设备上,你无法通过加服务器解决;带宽预算在你的服务器上,加钱能解决但很贵。所以要分开处理。

二、位置同步:滞后是可接受成本,跳变不是

服务端权威 + 客户端平滑插值,是这一层的标准答案。要点是区分"滞后"和"跳变"——玩家对滞后的容忍度远高于对跳变的容忍度。200ms 的稳定滞后,观感是"他稍微慢一点";一次坐标硬赋值,观感是"他瞬移了"。

我们的取舍是:真人端位置滞后控制在 3 米以内,换来的是移动全程平滑、没有一格一格跳。这个数字不是越小越好——把插值窗口压得太窄,网络抖动会直接传导成画面抖动,反而更难看。

还有一个容易被忽略的细节:AI 与真人的位置更新频率不一样。真人端是浏览器按帧上报,AI 端是服务端按档位推送(见第六节)。这两条流最后要汇进同一个位置表、同一套广播里,否则真人看到 AI 的移动会是另一种节奏。

三、弱网:真正难的是"重连之后还互相看得见"

这是我们在多人体验上花时间最多的一处,也是最容易在 demo 阶段被掩盖的一处——因为 demo 时不会掉线。

现象的经典版本:A、B 两人各掉一次线,各自重连成功,但彼此看不见了,必须双方手动刷新页面才能恢复。根因定位下来是一句话:

重连之后,服务器那边把你当新连接重新登记了,但"我已经在场"这个事实没有重新广播出去

于是我们做了一层在场状态守护,四个动作:

  1. 缓存 PLAYER_JOIN 事件,每次重连后自动重发——这是核心那一行;
  2. 无限重连(带退避),而不是重试 N 次就放弃;
  3. 应用层 PING/PONG 看门狗——TCP 层面的连接可能还活着但应用层已经死了,只靠 onclose 感知不到;
  4. 服务器侧"未登记连接"告警——用于发现第 1 条没覆盖到的边角情况。

验收判据很直接:重连后对端仍能看到我移动,9/9 通过

逻辑本身不长,示意如下(示意伪代码,不是项目源码):

// 客户端:把"我在场"这件事缓存下来,每次重连都重播一遍
let pendingJoin = null;          // 缓存 PLAYER_JOIN 报文

function onLocalJoin(payload) {
  pendingJoin = payload;         // 本地生成的进场事件,先存着
}

function onOpen() {              // 每次重连成功都走这里
  if (pendingJoin) send(pendingJoin);   // ← 核心一行:重播在场状态
  startPingPong();                      // 应用层看门狗
}

function onClose() {
  reconnectWithBackoff();        // 不设重试上限,带退避
}

// 服务端:发现"连上了但没登记"的异常连接,用于兜住上面漏掉的分支
setInterval(() => {
  for (const c of connections) {
    if (!registry.has(c.id)) warn('unregistered connection', c.id);
  }
}, 30_000);

另外一类坑是"看得见但会攒着不放":视频 DOM、灯池租用的资源、聊天气泡、僵尸连接。这些在长会话里一个都别留着——我们做过一轮内存审计,修掉四项泄漏。多人场景的内存泄漏是复利:单人的界面泄漏在多人下会被乘上人数。

四、语音:从"整区禁言"到名额制

近场语音(按住说话、30 米内可听)有两种退化设计,都不好:

  • 所有人一直开着:30 米内一多,服务器带宽直接失控;
  • 人多就整区禁言:简单粗暴,但把功能废掉了——用户感受到的是"这个功能一到人多就不能用"。

我们改成同时说话人数名额制:按住说话走半双工,30 米内中继,带距离衰减,但同时说话的人数是有上限的,上限还能按服务器规格在后台选。

同时说话人数带宽量级建议服务器
10 人约 1.3 Mbps1 核 2G
20 人约 2.6 Mbps2 核 4G

这个改法的价值在于:把"人多就不可用"换成了"人多就排队"。排队是可接受的,不可用是不可接受的。

还有一条合规上的选择:语音链路服务端零加工——不做语音识别、不做语音合成,音频只是转发(且不落库)。转写这件事交给 AI 客户端去做,归属清晰,也省掉了服务器上最贵的那部分。

五、把外部 AI 接进来:关键是"复用同一条广播链路"

这套世界后来开放了 AI 接入——外部 AI 可以以人形角色进入世界,被真人看到、能走能说。工程上最值得讲的一点不是 AI 本身,而是接入方式的选择

不给 AI 另开一套可见性系统,而是让它伪装成一个玩家,复用现有的玩家广播。

具体只有一个焊接点:把 AI 的位置写进玩家位置表,然后复用现有的 PLAYER_JOINED / POSITION_UPDATE / CHAT 三条广播。结果是真人端的浏览器几乎零改动就能看到 AI 的 3D 形象——它和另一个真人玩家走的是同一条链路。

好处是双向的:

  • 对现网零破坏:专用通道按路径分流(/ws/agent 走 AI,其余一切路径兜底给人类),不动人类链路的任何行为;
  • 对成本极友好:AI 的模型从不下发给 AI,它的 Avatar 由真人浏览器替它渲染。AI 拿到的是结构化的 JSON 雷达,不是画面、不是截图。

相关的一处产品决策也值得记:门禁是倒过来的。没有 API Key 也能凭域名拿到一张 30 分钟的公开票进场(只拉不推),只有稀缺的"推流"资源才需要 Key。理由很直白——上线初期的风险是没人来,不是被滥用。把门槛设在稀缺资源上,而不是设在"看一看"上。

推流本身也做了分档,因为"每个 AI 都要实时收到世界变化"是没必要的高成本:

档位行为成本
eco纯拉取,不推送0
standard每秒聚合一次
realtime10Hz 逐条推送高(实测 9.0Hz/实体、4.5KB/s)

六、算一次账:100 个 AI 同时在场

这是最能说明架构价值的一组数字。实测场景:100 个游客 AI + 3 个 Key Agent + 2 个真人玩家,同场持续 60 秒

指标实测值
服务器 CPU0.079 核
服务器内存(RSS)92 MB
雷达接口延迟P50 7ms / P95 14ms
限流触发0 个 429
真人端帧率真 GPU 60 FPS
浏览器控制台报错0
游客连接的推送流量0 消息 / 0 字节(红线反证)

请注意最后一行的意义:它不是在测性能,是在反证红线真的生效了——游客身份确实一条推送都没收到。安全设计如果不能被反证,就只是文档。

几条内置的硬约束也一起列出来,因为它们解释了成本为什么压得住:游客不接收任何推送、观察半径硬钳 30 米、位置流不消耗令牌桶、背压 1MB 警告 / 4MB 断开、30 秒心跳、空闲踢出、单 Agent 单连接顶替(旧的静默断开,不闪断影响真人)。

七、当前短板(如实说)

  1. 玩家三档分级渲染还没做。人多时按距离对玩家角色做分级渲染(远处降级/不渲染)是一块明确能省性能的地方,对应的模块当前不在代码里,需要重建。也就是说:人多时的性能目前主要靠模型侧的 LOD 撑着,玩家角色本身还没有分级。
  2. 位置流的抖动抑制偏朴素。低帧率设备上,角色移动的手感会受影响(源码里 player.js 仍是每帧固定增量而非帧率解耦),这在低配手机上是能被感知的。

这两条都是"已知、有方案、未完成",写出来比藏起来好。

八、常见问题

问:浏览器端多人 3D,用 WebSocket 还是 WebRTC?

答:位置这种小消息、需要服务端权威仲裁的,用 WebSocket 更简单可控;语音这类连续媒体流适合走 WebRTC 或专门的媒体链路。我们的语音走的是 30 米内中继 + 半双工,服务端只转发不加工。

问:人多了怎么办,加服务器就行吗?

答:位置同步这部分 CPU 其实很便宜(100 个实体不到 0.1 核),真正吃资源的是语音渲染。渲染在玩家设备上,你加服务器没用——要做的是模型 LOD 和玩家角色分级;语音才是有明确服务器规格对应的部分。

问:怎么测弱网下的表现?

答:别测"能不能连上",要测"重连之后还看不看得见对方"。我们的验收判据就是让 AB 双方各自掉线再自愈,然后检查对端是否仍能看到我的移动——这条过了才算数。

问:多人房间的看门狗有必要吗,onclose 不够?

答:不够。网络中间设备可能让连接"半死"(TCP 还活着、应用层已经没响应),onclose 不会触发。要加应用层的 PING/PONG 和"服务器未登记连接"的告警这两道。

问:AI 和真人能同时在一个世界里吗?

答:可以,而且走的是同一条广播链路。真人浏览器负责渲染 AI 的形象,AI 本身只收结构化的空间数据——这是它成本低的原因。

关于创世Genesis

创世Genesis是一套基于Three.js+WebGL构建的自部署3D虚拟世界系统,帮助个人与企业搭建属于自己的3D空间。浏览器直接访问,PC和手机双端兼容,支持多人在线、联邦传送、商铺系统,并支持Agent接入——AI能以具身角色进入你部署的世界。数据运行在你自己的服务器上,不经过第三方平台——让每个世界都真正属于它的主人。

如果你在做浏览器端多人在线 3D,上面这套踩坑记录来自创世Genesis——一套浏览器端、可部署在自有服务器上的 Three.js 3D 虚拟世界基底,指标来自仓库里可重跑的验收脚本,也开放了 AI 以人形角色接入。

本文为开发过程中的技术复盘,数据来自项目内验收脚本,可直接复跑。标注为「当前短板」的部分属实未完成,不作为已交付能力。

源码与仓库

三个地址内容一致,国内访问用上面两个更快。仓库里有部署说明与演示入口。

← 返回文章列表