ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

零依赖网页小游戏如何用 WebRTC 与 P2P 实现低延迟联机

零依赖网页小游戏如何用 WebRTC 与 P2P 实现低延迟联机 1. 零依赖不是噱头为什么单文件游戏值得较真这两年我经常在各种社区里看到类似的问题“有没有不用下载 App、复制链接就能直接在浏览器里玩的网页小游戏”大多数人的第一反应是扔一个链接过去很少有人会停下来想这类游戏背后的工程到底是怎么组织的我陆陆续续做过不少网页小游戏也帮朋友改过一些“复制全部代码到本地就能跑”的 HTML 小游戏。说实话真正做起来之后你会发现零依赖不是偷懒反而是一种逼着你把代码写干净的约束。1.1 所谓“复制链接就能玩”拆开看其实就三样东西先还原一下那些看起来特别轻巧的网页游戏底层到底有什么。拿比较有代表性的两类来说一类是类似《Mikutap》那种按键盘出音效、画面跟着节奏变化的互动音乐小游戏另一类是类似“夜间飞行”那种单文件 HTML JS 的躲避/飞行类小游戏。这两类游戏的共同点是不需要安装运行时、不需要后端服务、不需要数据库甚至连图片和音频文件都不用额外加载。它们做到“复制即玩”的核心通常只依赖三样东西HTML 结构一个canvas画布偶尔加几个 DOM 按钮作为 UIJavaScript 逻辑游戏循环、碰撞检测、输入监听全部塞在script标签里内联资源音频用 Web Audio API 实时合成图片用 Canvas 绘制或者干脆用 CSS 形状避免任何外部文件依赖。你打开浏览器按 F12能看到这些游戏的完整源码往往只有几百行。它们之所以能“秒开”关键在于没有任何网络请求。所有资源都在一个 HTML 文件里浏览器解析完就能跑这才是真正意义上的“零依赖”。但这里有个误解零依赖不等于代码可以乱写。恰恰相反正因为没有框架帮你管理状态、没有第三方库帮你处理事件所有逻辑都得靠你自己组织。一旦游戏复杂度上来几百行代码就会快速膨胀成难以维护的一坨。OmniGame 这个项目最开始的想法就是想把这套“零依赖小游戏”的工程上限往上推一推。1.2 没有框架约束时怎么避免代码写成“毛线团”我见过不少零依赖小游戏初期跑得很欢一旦加入新的关卡、新的敌人类型、新的音效代码就开始失控。典型的症状是全局变量满天飞游戏循环里堆了三百行判断想加一个暂停功能得先读懂所有状态标志。在 OmniGame 里我做的最关键决定是不引入任何框架但严格划分三个模块用最简单的命名空间方式组织代码而不是把所有逻辑扔进一个main函数里。示例结构大概长这样const OmniGame { core: null, // 游戏逻辑状态、规则、实体 render: null, // 渲染Canvas 绘制、音效合成 input: null // 输入键盘、鼠标、触摸 }; class GameCore { constructor() { this.state ready; // ready / running / paused / ended this.entities []; this.score 0; } update(delta) { /* 只处理逻辑不管画什么 */ } } class Renderer { constructor(canvas) { this.ctx canvas.getContext(2d); } draw(gameState) { /* 只根据状态画东西不碰逻辑 */ } } class InputManager { constructor() { window.addEventListener(keydown, e this.keys[e.code] true); window.addEventListener(keyup, e this.keys[e.code] false); } getPressedKeys() { /* 返回当前帧的输入快照 */ } }从代码里能看出来GameCore完全不关心画面和声音Renderer只根据传入的状态去画InputManager把底层事件变成输入快照。三者通过外层的一个简单循环串起来let lastTime 0; function gameLoop(timestamp) { const delta (timestamp - lastTime) / 1000; lastTime timestamp; if (Game.state running) { const input Input.getPressedKeys(); Game.update(delta, input); Renderer.draw(Game); } requestAnimationFrame(gameLoop); } requestAnimationFrame(gameLoop);这个分层带来的直接好处是后面要加 WebRTC 联机时GameCore不用大改只要在update之前多塞一个“网络输入”的来源就行。没有框架的零依赖靠的就是这种手工的架构感来维持可维护性。1.3 零依赖场景下的调试盲区谁动了我的状态零依赖还有一个很多人没意识到的隐藏难题状态一旦错了排查路径特别长。有框架的时候状态管理有固定的约定比如单向数据流、响应式依赖收集出问题比较容易定位到是哪一步更新引发的。纯手写之后什么都是全局可改的一个对象可能被渲染层改了、被音频层改了、被输入层改了最后逻辑全乱。OmniGame 在这方面做了两件“笨但有效”的事。第一把状态变化收敛到一个方法里比如GameCore.applyAction(action)所有能改变游戏状态的入口只有这一个方法配合console.trace可以清楚看到是谁发起了改动。第二用不可变的输入快照代替直接改实体属性——每帧产生一个新的输入对象传给核心逻辑而不是让InputManager直接改Player.speed。这个习惯在单机时是锦上添花后面做联机同步时就成了必需品。提示如果你也想做零依赖的小游戏先别急着写代码花十分钟想清楚“谁改了什么、通过什么入口改的”后面省下的调试时间会远超这十分钟。2. 点对点不是玄学WebRTC 给网页游戏补上的短板单机小游戏做到干净、流畅、零依赖这只是第一个阶段。真正让我觉得“网页小游戏的工程上限可以重新定义”的是 WebRTC 带来的 P2P 能力。你可能在热搜词里看到过 WebRTC 和 P2P 相关的词抛开那些乱七八糟的用途WebRTC 本身是浏览器内置的点对点通信技术它能让两个浏览器之间直接传输数据而不经过任何中间服务器。2.1 网页游戏联机三条技术路线的取舍做网页游戏联机摆在面前的无非三条路方案数据路径延迟服务器成本适合场景WebSocket 中心转发玩家A → 服务器 → 玩家B较高受服务器地理位置影响持续承担所有流量大厅匹配、房间管理、需要服务器仲裁的游戏WebRTC P2P玩家A → 玩家B低接近物理网络极限仅信令阶段有少量开销两人或多人之间的实时互动、语音、对局混合架构关键操作 P2P数据存档/匹配走服务器中取决于业务有账户系统、排行榜、防作弊需求的产品单机小游戏做联机绝大多数人第一反应是上 WebSocket。这没有错WebSocket 技术成熟、实现简单、跨 NAT 没问题而且服务器可以顺便做数据校验。但它的天然瓶颈是所有数据都要绕道服务器。假设服务器在北京你在上海、朋友在广州两个人一个操作经过服务器多走一两千公里延迟直接多几十毫秒。对飞行躲避类这种追求反应速度的游戏来说几十毫秒足以改变胜负。WebRTC 的思路完全不同它让两个浏览器先通过一次短暂的“握手”彼此认识之后数据就在两端直接流动不再需要服务器在中间转发少数NAT场景除外后面会讲。这个“绕开中间人”的特性正是实时游戏最需要的。2.2 DataChannel浏览器里的一条“数据管道”很多人一听 WebRTC 就觉得是音视频通话其实 WebRTC 里的核心组件之一是DataChannel它是一条任意二进制/文本数据都能走的高速通道。你可以把它理解成浏览器里的“Socket”但比 Socket 更灵活——它的底层基于 SCTP 协议可以配置成两种模式可靠模式reliable保证数据按顺序到达类似 TCP不可靠模式unreliable允许丢包不保证顺序类似 UDP。对网页小游戏来说这个选择特别重要。拿类似《Mikutap》的节奏互动来说音效触发这种数据丢了就丢了硬要重传反而会让节奏错乱但玩家分数这种关键状态就必须可靠传输。DataChannel允许你在同一个连接上开多条通道一条可靠一条不可靠这个自由度是 WebSocket 给不了的。建立一条 DataChannel 的最小代码其实非常短const pc new RTCPeerConnection({ iceServers: [{ urls: stun:stun.l.google.com:19302 }] }); // 一端创建 DataChannel const channel pc.createDataChannel(game, { ordered: true }); channel.onopen () console.log(通道已打开); channel.onmessage (e) handlePeerMessage(JSON.parse(e.data)); channel.send(JSON.stringify({ type: hello, data: 嗨能听见吗 }));另一端监听ondatachannel事件就能拿到对方创建的通道。这段代码单独是跑不通的因为中间还缺了“交换连接信息”这个过程——这正是下一章要展开的重点。2.3 为什么网页小游戏是 WebRTC 的天然主场我工作室的项目里有一些用 WebSocket 做的小游戏也有一版改造过 WebRTC 的 OmniGame。为什么说小游戏是 WebRTC 的天然主场因为网页小游戏普遍有三个特点会话短打一把三五分钟频道生命周期短不需要复杂的连接池管理无安装门槛用户通过浏览器打开链接就能玩WebRTC 是浏览器原生能力不需要额外装插件状态轻小游戏的核心状态位置、血量、得分通常可以用几个数字精确描述非常适合高频快照同步。对比之下大型重度游戏对反作弊、服务器权威的要求极高不适合纯 P2P而网页小游戏的“玩个乐呵”属性反作弊压力小Peer 之间互相信任的成本低可以让每个玩家自己的设备充当权威节点。这种场景下WebRTC 的“零服务器转发、低延迟、免安装”优势被发挥到了极致。3. 唯一绕不开的服务器信令交换的一次握手全流程WebRTC 什么都不需要服务器唯独有一件事绕不开双方互相告诉对方“怎么找到我”以及“怎么加密通信”。这个过程叫信令交换。很多人第一次接触 WebRTC 就是被这一环劝退的——RTCPeerConnection 创建容易真正建立连接要交换 SDP 和 ICE 候选光看文档容易一头雾水。3.1 SDP、ICE、STUN三个名词就能拆穿握手护城河先直观理解三件事SDPSession Description Protocol一段描述“我支持什么音频/视频/数据格式、用什么加密方式、我的网络端口是什么”的文本。简单说就是“我的能力清单和联系方式”。ICEInteractive Connectivity Establishment一套“尝试所有可能路径来连通”的机制。它可能直接走公网 IP也可能走 NAT 打洞实在不行就走 TURN 中继。STUN一种帮浏览器查出“我的公网 IP 是什么”的服务器。注意它只负责查地址不负责转发数据。整个握手可以类比成两个人在不同楼里要互相传纸条A 先写一份“我在哪、我支持什么方式”的说明书SDP offer通过邮局信令服务器递给 BB 看完后写一份自己的说明书SDP answer回给 A随后两人各自汇报“我能通过哪扇窗接到纸条”ICE候选地址最终双方挑一条最快的路径直接开始互传。3.2 OmniGame 的信令方案本地开发用“手动粘贴”线上再上服务器我在 OmniGame 项目里做了一个非常“土”但极其好用的决策信令服务器只负责交换 SDP 和 ICE 候选交换完就退居二线。本地调试阶段我甚至不写服务器直接在两个浏览器控制台之间手动复制粘贴连接信息。在这个阶段完整流程如下玩家 A 在浏览器打开页面控制台执行createOffer()生成一串 SDPA 把 SDP 复制发给 B微信/邮件/随便什么方式B 在同一页面的控制台执行acceptOffer(sdp)自动生成 answer SDP 并打印出来B 把 answer SDP 复制回给 AA 执行acceptAnswer(sdp)建立 RTCPeerConnection双方再互相复制粘贴 ICE 候选地址连接打通。这套“手动信令”虽然笨但调试信息完全透明你能亲眼看到 SDP 是怎么生成的、ICE 状态是怎么从不连通变成连通的。一旦发现问题直接在控制台看pc.iceConnectionState的状态流转就明白了。等游戏基本定型我再把信令功能做成一个不到 100 行的 WebSocket 转发服务逻辑只有三步A 发 offer → 服务器存起来并转发给 B → B 发 answer → 服务器转发给 A。代码逻辑可以参考这个简版// 信令服务器Node.js ws const WebSocket require(ws); const wss new WebSocket.Server({ port: 8080 }); const pending new Map(); wss.on(connection, (ws) { ws.on(message, (raw) { const msg JSON.parse(raw); if (msg.type offer) { // 存下 offer等待对方获取 pending.set(msg.room, msg.sdp); ws.send(JSON.stringify({ type: waiting })); } if (msg.type answer) { // 把 answer 转发给 offer 方 ws.send(JSON.stringify({ type: answer, sdp: msg.sdp })); } if (msg.type ice) { // 交换 ICE 候选 ws.send(JSON.stringify({ type: ice, candidate: msg.candidate })); } }); });提示很多生产环境的 WebRTC 会自动通过 ICE 的 Trickle 过程逐渐收集候选地址不需要等所有候选都收集完再发。本地手动调试时建议先等iceGatheringState变成complete再复制 SDP否则很容易漏掉候选信息导致连不通。3.3 交换完了之后服务器真的“退休”了吗这是 WebRTC 新手最容易困惑的点信令服务器在连接建立之后到底还有没有用答案取决于网络环境理想情况多数公网玩家信令交换完成后双方通过 ICE 找到了最直接的 P2P 路径所有游戏数据直接走浏览器之间信令服务器可以完全不管后续数据。NAT 严格/对称型网络打洞失败时浏览器会自动改走 TURN 服务器中继流量。这种时候信令服务器依然不转发游戏数据但你必须提供一个 TURN 服务器作为兜底否则这类玩家永远连不上。我在实际测试中遇到过一种情况两个浏览器都在同一个局域网ICE 居然没有直接建立 P2P而是走了公网打洞后绕了一圈。后来加了host类型的候选优先级配置才直接走局域网直连。这类“同网段却绕远路”的问题在调试 WebRTC 联机游戏时非常常见但只需在 ICE 候选选择上稍作调优即可解决。4. 真实改造单机飞行游戏如何长出联机对战的翅膀前面讲的都是原理和基础这一章拿 OmniGame 里一个具体的游戏来实操。我选了“夜间飞行”这种单文件风格的飞行障碍躲避游戏作为样本先把单机版跑通再一步步给它加上 WebRTC 联机能力。4.1 单机版的最小闭环先定义“游戏到底是什么”以飞行躲避为例核心玩法是玩家控制一架飞机左右移动躲避随重力下落或从前方飞来的“障碍物”碰到障碍物游戏结束。单机版的核心状态可以浓缩成这几个字段const gameState { player: { x: 200, y: 500, width: 40, height: 40 }, obstacles: [], score: 0, alive: true, timestamp: 0 // 用于同步帧序号 };单机版的每次update(delta, input)只是根据键盘输入挪动player.x然后让障碍物按速度向下移动并检测碰撞。这个版本跑起来没有任何问题但它不具备“网络可复制性”。为了让后续联机改造更平滑我在写单机版时特意做了一件事把所有会产生随机数的逻辑障碍物生成位置、速度浮动单独抽出来并且允许传入一个外部生成的随机数。这一步不是先见之明而是踩过“联机后两边障碍物位置完全对不上”的坑之后才补的。4.2 双方都不作弊Host 权威 按序输入为飞行游戏选同步模型的思路很简单首先排除“服务器权威”——因为 OmniGame 本身是 P2P 零依赖没有一个中心服务器去仲裁其次排除“各算各的”——飞行躲避比的是反应速度和操作精度两边画面不一致就失去了竞技意义。最后剩下Host 权威 客户端输入同步的模型Host房主负责运行完整的物理和碰撞逻辑生成障碍物Client客人把按键输入通过 DataChannel 发给 HostHost 计算出下一帧游戏状态后把状态快照发回给 ClientClient 不自己跑物理而是用“延迟插值”来渲染 Host 下发的状态保证画面平滑。Host 权威的代码核心是输入队列// Host 端把收到的客户端输入按时间戳排队 const clientInputQueue []; // 收到网络输入时 function onClientInput(data) { clientInputQueue.push({ timestamp: data.timestamp, keys: data.keys }); // 排序保证按序处理 clientInputQueue.sort((a, b) a.timestamp - b.timestamp); } // 每帧更新时从队列里取出到当前帧时间为止的输入 function applyPendingInput(currentTime) { while (clientInputQueue.length clientInputQueue[0].timestamp currentTime) { const input clientInputQueue.shift(); applyMove(input.keys); } }这背后有一个非常现实的理由网络是不可靠的如果你直接“收到一条输入就立刻处理”那么延迟抖动和丢包会直接表现为游戏内容跳变而把输入按时间戳入队再逐帧消费可以让 Host 以一个稳定节奏处理玩家操作减少网络抖动的影响。4.3 快照也不是越大越多越好状态压缩与发送频率Host 每帧计算完状态后向 Client 发送快照。这里有个很容易踩的坑如果每帧都发送完整的状态对象一个包含几十个障碍物的快照 JSON 可能就有好几 KB一秒 60 帧就是几百 KB 的流量DataChannel 虽然扛得住但延迟会因为网络拥塞而飙升。我的做法有三点降频发送Host 不按 60fps 发快照而是按 20fps每 50ms 发一次。客户端用“历史缓冲插值”补齐中间帧的显示视觉上依然顺滑增量快照如果这 50ms 里障碍物没变化比如没生成新障碍就不重发完整的障碍物列表只发玩家坐标和分数关键事件优先新障碍生成、碰撞判定这类事件走可靠通道普通坐标走不可靠通道。这样即便掉几个坐标包玩家画面也是流畅的不会出现飞机瞬移后再突然弹回的情况。// 发送快照只发关键字段 function sendSnapshot() { const snapshot { t: gameState.timestamp, p: gameState.player, // 玩家位置 o: gameState.obstacles.length, // 障碍物数量与上次不同时才发完整列表 s: gameState.score, a: gameState.alive }; reliableChannel.send(JSON.stringify(snapshot)); }这个“只管关键字段”的习惯是从观察游戏数据包大小后悟出来的。刚开始联机时我用 WebRTC internals 面板看到单次消息 8KB延迟飙升到 200ms 以上换成增量快照后单次消息降到 150B 左右延迟也回落到 40ms 内。做网页联机游戏消息体积比消息频率更值得关注。4.4 断线重连玩家中途关闭页面后的应急预案网页游戏的宿命是玩家说关就关标签页关了DataChannel 立刻断开。我在 OmniGame 里做了两层兜底。第一层是游戏内降级如果连接断开发生在 15 秒内显示“对方掉线等待重连”倒计时如果对方重新打开页面通过信令服务器重新协商一次连接超过 15 秒判定对方放弃结束对局。第二层是Host 迁移如果 Host 关闭了页面Client 会等在信令服务器里不允许页面关闭此时如果 Host 重新进入同一个房间码Client 自动切换为新 Host。这些恢复逻辑听着简单实际过程比预想复杂很多。最容易被忽略的是DataChannel的close事件处理——很多浏览器在断线时不会立刻触发close而是先触发了iceConnectionState变化。正确的做法是同时监听channel.onclose和pc.oniceconnectionstatechange基于“连接其实已经断了”这一事实来触发重连流程而非等一个特定事件。5. 工程上限零依赖与 P2P 组合成的架构边界把单机飞行游戏跑通联机之后我最大的收获不是“能联机了”而是清晰看到了零依赖 P2P 这种组合的工程上限在哪里以及为了让上限更高代码结构应该怎么搭。5.1 重新设计代码边界Core、Render、Net 三层分离前面提过零依赖靠三层分离来维持整洁加入 WebRTC 之后这个模型需要进化。OmniGame 最终采用了四个模块的架构Core纯游戏逻辑不依赖任何浏览器网络 API只接收输入和时间戳产出状态Render接收状态负责 Canvas 绘制和音频合成Input统一收集键盘/鼠标/触摸输入产出标准化的输入快照Net封装 WebRTC 的建立、信令交互、DataChannel 收发、重连逻辑。核心设计原则是Core完全不知道Net的存在。也就是说同一个Core既能跑在纯单机模式输入来自本地键盘也能跑在联机模式输入来自网络队列还能跑在未来的“回放模式”输入来自录制的文件。这种可切换性让我在调试联机问题时特别省劲——我可以让Core跑在本地用一个假的网络层同时注入本地输入和远端输入从而在后端无浏览器环境下复现 bug。四层之间不直接用全局函数互相调用而是通过一个很瘦的Controller组合起来class OmniGameController { constructor() { this.core new GameCore(); this.net null; // 默认无网络 } startLocal() { this.net null; this.loop(); } startP2P(role) { this.net new NetManager(role, { onIncomingInput: (input) this.core.applyInput(input), onStateSnapshot: (snap) this.render.interpolate(snap) }); this.loop(); } loop() { const input this.net ? this.net.pendingInput() : this.input.read(); this.core.update(delta, input); if (this.net) this.net.sendSnapshot(this.core.snapshot()); this.render.draw(this.core); requestAnimationFrame(() this.loop()); } }这种“组合式装配”让零依赖小游戏也能轻松地在不同运行模式间切换而不需要为每个模式写一套独立的游戏逻辑。5.2 能力检测与降级没有 WebRTC 时游戏不能“白屏”不是所有环境都支持 WebRTC。尤其在公司内网、某些老版本浏览器、以及部分 WebView 环境中RTCPeerConnection可能不存在或被禁用。OmniGame 在进入联机模式之前先做了三步能力检测function hasWebRTC() { return typeof window.RTCPeerConnection ! undefined || typeof window.webkitRTCPeerConnection ! undefined; } function hasDataChannel() { const pc new RTCPeerConnection(); const dc pc.createDataChannel(test); return dc typeof dc.send function; }检测不通过时游戏不能直接死掉而是自动降级为单机模式并提示“当前浏览器不支持 P2P 联机可体验单机玩法”。这个降级策略非常实用用户粘性保住了且不会因为一个不支持的浏览器失去潜在玩家。我还在信令服务器端设置了“连接超时自动释放房间”的逻辑避免掉线玩家一直占用房间号。5.3 性能上限与实战调优数据这里整理一些我在 OmniGame 联机测试中亲测的数据供你参考参数建议值实测效果快照发送频率20Hz视觉流畅度介于 60fps 和 30fps 之间网络开销可接受单条 DataChannel 消息大小尽量 1KB1KB 以下时延迟稳定在 20-50ms输入发送频率40-60Hz低于 20Hz 时手感明显发飘ICE 收集超时3 秒超过不设超时会出现卡在 connecting 状态的情况重连等待15 秒短于 10 秒误判率高长于 20 秒玩家没耐心有两个性能盲区值得单独说。一是JSON 序列化开销快照发送频率 20Hz 时看起来还好但每帧都把整个状态转成字符串再解析在低端手机上还是会造成短暂的卡顿。后来我把障碍物列表改成二进制格式用Uint8Array 手动写入坐标数字消息体积和解析耗时都大幅下降整个过程只需要写一个 30 行的编码/解码函数。二是requestAnimationFrame 在后台标签页会被暂停当玩家把页面切到后台再切回来游戏循环会经历一次“时间断层”如果不处理 deltaTime 的上限游戏会毫无征兆地跳一大段。我在Core.update里显式限制delta最大为 50ms超过部分丢弃。5.4 调试利器用“两个窗口 控制台注入”复现线上问题本地联机调试最头痛的问题是“两边代码一样为什么 A 连得上、B 连不上”。OmniGame 的团队现在用的方法是开两个浏览器窗口窗口 A 打开页面后手动执行初始化 Host 的代码窗口 B 打开页面后手动执行初始化 Client 的代码同时把两者的控制台输出拉到同一个 Log 面板里比对。对于 SDP 交换我们会打日志输出“发送 offer 的完整 SDP”和“接收 answer 的 ICE 状态”这样每次握手失败都能精确定位是 SDP 字段问题、ICE 候选问题还是 STUN 服务器不可达问题。如果要多端测试也可以用 Chrome 的chrome://webrtc-internals页面实时查看每个连接的状态机变化、每个 DataChannel 的收发字节数、ICE 候选对的选择过程。这些信息对判断“是不是被 NAT 卡住了”“是不是消息太大了”非常有帮助。6. 做完这个项目我最想提醒你的三件事OmniGame 从零依赖的单文件游戏一路走到 WebRTC P2P 的实时联机最大的体会是网页小游戏的工程上限从来不是“能不能做到”而是你在动手之前有没有想清楚每一层该做什么、不该做什么。第一件事把网络层当成一个普通输入源而不是游戏里的特殊存在。只要你能让游戏核心逻辑无视输入来源那么单机、联机、回放、AI 对战全都可以共用同一套核心代码。这套架构思维的价值远超 WebRTC 本身。第二件事别被浏览器原生的“零依赖”神话迷惑。WebRTC 虽然免去了服务器转发数据的成本但信令服务、TURN 兜底、断线重连、状态同步这些工程细节一个都不会少。零依赖省掉的是安装成本不是工程复杂度。第三件事从小体量游戏开始尝试验证 P2P。先做一个双人版的“夜间飞行”或者《Mikutap》的联机版跑通一次完整的“手动信令 → DataChannel 收发 → 状态同步”闭环再考虑更大规模的多人对局。我见过太多人在一开始就试图做千人同屏的 P2P 游戏结果连两个人的状态同步都还没理顺。最后再分享一个小技巧如果你在做 WebRTC 联机小游戏时卡在“两边明明交换了 SDP但 DataChannel 就是打不开”试着在创建RTCPeerConnection时把iceServers留空强制走本机直连。如果这样能连上说明问题出在 STUN 服务器或者 NAT 穿透策略上如果这样也连不上那就回去检查 SDP 交换是否漏掉了 ICE 候选。排查万变不离其宗祝你的网页小游戏早日长出 P2P 的翅膀。
返回列表