戰(zhàn):小游戲平臺實(shí)時通信與樣式隔離復(fù)盤)
項(xiàng)目做到第二周我對著瀏覽器調(diào)試面板和用戶的抱怨差點(diǎn)把鍵盤拍爛游戲房間延遲高到?jīng)]法玩不同小游戲的樣式互相污染改一處按鈕顏色到處爆炸。后來我決定把平臺里的實(shí)時通信和組件封裝全部推倒重來換成 WebRTC Shadow DOM 組合。這篇復(fù)盤就是我當(dāng)時從選型到落地、從踩坑到調(diào)優(yōu)的全過程適合正在做網(wǎng)頁小游戲平臺、或者想給站點(diǎn)加實(shí)時對戰(zhàn)能力的開發(fā)者參考。我不會寫教科書式的說明只講我在真實(shí)項(xiàng)目里怎么選、怎么搭、怎么被坑、怎么修。1. 這個組合解決了我游戲平臺的哪三個核心難題1.1 實(shí)時交互的延遲底線從輪詢到 WebRTC DataChannel一開始我的平臺里玩家匹配到同一房間后所有操作指令都先發(fā)給服務(wù)器再由服務(wù)器廣播給房間里的其他人。也就是一個典型的 WebSocket 轉(zhuǎn)發(fā)架構(gòu)。小游戲只有幾十人同玩還好一旦動作頻率上來比如貪吃蛇、你畫我猜這種每幀都要同步走位或畫筆坐標(biāo)的玩法服務(wù)器轉(zhuǎn)發(fā)延遲就會被拉得很明顯。我實(shí)測過一輪普通 WebSocket 轉(zhuǎn)發(fā)在機(jī)房服務(wù)器和用戶距離中等的情況下往返延遲平均在 80ms 到 150ms 之間高峰期能到 200ms 以上。對休閑小游戲來說這個延遲太難受了會有明顯的方向盤遲滯感。WebRTC DataChannel 不一樣它走的是瀏覽器到瀏覽器的點(diǎn)對點(diǎn)鏈路服務(wù)器只在最開始幫忙交換一次連接信息后面游戲狀態(tài)直接 P2P 傳輸。在我的實(shí)測環(huán)境里同一城市的兩臺設(shè)備點(diǎn)對點(diǎn)延遲穩(wěn)定在 20ms 到 40ms。也就是說同樣一個蛇頭轉(zhuǎn)向指令服務(wù)器轉(zhuǎn)發(fā)方案要等 0.15 秒P2P 方案 0.03 秒就過去了手感完全不同。這里要說明一下WebRTC 并不是完全不需要服務(wù)器它需要信令服務(wù)器來交換 SDP 與 ICE 候選。只是信令只發(fā)生在連接建立前一旦連接建立業(yè)務(wù)數(shù)據(jù)就不再經(jīng)過服務(wù)器。這一點(diǎn)后面專門講。1.2 組件污染小游戲樣式打架的根源網(wǎng)頁小游戲平臺的生態(tài)很特殊很多游戲是第三方開發(fā)者或者我復(fù)用歷史代碼拼接的每份代碼都有自己的 reset.css自己定義的 .btn甚至全局div { box-sizing: border-box; }這種野路子。以前所有游戲都渲染在同一個主頁面 DOM 下樣式?jīng)_突是必然的。A 游戲?qū)懙陌粹o圓角能影響 B 游戲里的按鈕B 游戲設(shè)的全局字體棧會讓 C 游戲標(biāo)題變形。更可怕的是有些老代碼會在 window 上掛全局對象一沖突整個平臺白屏。我試過 CSS Modules、scoped 樣式、postcss 自動添加命名空間都不徹底。因?yàn)橛螒騼?nèi)部 DOM 結(jié)構(gòu)是動態(tài)生成的有些是字符串拼接的 HTMLscoped 選擇器根本罩不住。最終選 Shadow DOM每個小游戲掛在獨(dú)立的 custom element 下內(nèi)部 DOM 放進(jìn) shadowRoot。shadow 樹里的樣式默認(rèn)只作用于 shadow 樹內(nèi)部天然免疫全局樣式也影響不到外部。這個隔離能力幫我把游戲之間互不干擾從工程約定變成了瀏覽器級強(qiáng)制行為。1.3 為什么選原生 WebRTC 而不是現(xiàn)成的游戲?qū)?zhàn) SDK在選擇實(shí)時通信方案時我也認(rèn)真評估過那些商業(yè)化游戲?qū)?zhàn) SDK。它們的優(yōu)點(diǎn)是開箱即用房間管理、信令、斷線重連都封裝好了。但我不選它們的原因有三點(diǎn)第一授權(quán)和流量成本不可控。小游戲平臺的用戶量可能突然漲如果按并發(fā)時長計費(fèi)冷啟動階段很容易白交錢。自建信令服務(wù)器用的是我自己現(xiàn)有的云主機(jī)費(fèi)用增量幾乎為零。第二自定義信令策略。我的平臺里有閃現(xiàn)匹配隨機(jī)拒絕觀戰(zhàn)這類奇怪玩法需要服務(wù)器在匹配階段做很多定制邏輯。第三方 SDK 的房間流程是固定的我要繞它們的流程反而更折騰。第三Shadow DOM 是瀏覽器平臺原生能力不需要引入額外依賴WebRTC 同樣是標(biāo)準(zhǔn) API。兩個原生標(biāo)準(zhǔn)組合沒有框架鎖定的風(fēng)險后續(xù)遷移到任何前端框架都可以復(fù)用這套封裝。2. WebRTC 連接管理實(shí)戰(zhàn)信令、ICE 與房間狀態(tài)機(jī)2.1 信令服務(wù)的選型自建 WebSocket、Socket.io 還是托管實(shí)時庫信令服務(wù)的職責(zé)只有一個在連接建立前幫兩個瀏覽器交換 SDP offer/answer 和 ICE candidate。不需要它轉(zhuǎn)發(fā)游戲數(shù)據(jù)所以選型重點(diǎn)應(yīng)該放在可靠性和簡單性上。我第一版用了 Socket.io理由是它自帶房間概念斷線重連也現(xiàn)成。后來發(fā)現(xiàn)一個坑Socket.io 的事件名和自定義數(shù)據(jù)格式如果和游戲業(yè)務(wù)消息混用調(diào)試時非常亂。信令消息和業(yè)務(wù)消息在同一個 socket 連接里傳輸一旦業(yè)務(wù)量大信令消息可能在隊(duì)列里被擠后導(dǎo)致新玩家入房變慢。最后我把信令拆成獨(dú)立通道。新的結(jié)構(gòu)是通道消息類型用途業(yè)務(wù)通道WebSocket房間匹配、聊天、觀戰(zhàn)、房間解散通知信令通道獨(dú)立 WebSocket 連接僅交換 SDP / ICE candidate數(shù)據(jù)通道WebRTC DataChannel游戲幀同步、畫筆坐標(biāo)、手勢操作實(shí)測下來拆開之后新玩家入房時間穩(wěn)定在 500ms 內(nèi)就算房間里游戲數(shù)據(jù)已經(jīng)很密集也不會影響信令交換。2.2 玩家房間的生命周期與重連策略WebRTC 連接在多玩家房間里并不是簡單的兩兩互聯(lián)。三個人以上我一開始圖省事用了 mesh 架構(gòu)每個人和其他人各建一條 RTCPeerConnection。3 人就是 3 條連接4 人就是 6 條。在瀏覽器里同時維護(hù)幾條 PeerConnection 沒有問題但超過 6 人后移動端設(shè)備內(nèi)存和 CPU 明顯吃緊。后來我限制了一個房間的 P2P 人數(shù)最多 6 人超過 6 人的房間自動切換到服務(wù)器轉(zhuǎn)發(fā)模式。這個妥協(xié)很重要小游戲平臺的核心體驗(yàn)是低延遲和輕量不能因?yàn)?P2P 而限制玩法規(guī)模也不能把所有玩家都拖進(jìn) mesh 把手機(jī)燒成暖手寶。連接狀態(tài)管理上我借鑒了 TURN 連接的狀態(tài)機(jī)思路設(shè)計了一套自己的房間狀態(tài)機(jī)waiting玩家已匹配到房間等待信令握手connecting正在交換 SDP / ICE candidateplayingDataChannel 已打開游戲數(shù)據(jù)互通reconnecting網(wǎng)絡(luò)切換、瀏覽器切后臺導(dǎo)致連接波動正在嘗試 ICE restart每個狀態(tài)在 UI 上都有明確表現(xiàn)。強(qiáng)制刷新頁面之后玩家會重新走一遍waiting - connecting - playing但房間 ID 和玩家身份通過 token 恢復(fù)不讓玩家覺得斷線就要重開一局。2.3 ICE 配置與 NAT 穿透調(diào)優(yōu)ICE 候選的收集質(zhì)量直接決定連接能否建立、是不是走內(nèi)網(wǎng)直連。我用的配置是const pc new RTCPeerConnection({ iceServers: [ { urls: stun:stun.l.google.com:19302 }, { urls: turn:turn.example.com:3478, username: your-user, credential: your-pass } ] });TURN 服務(wù)器我是用 coturn 自建的僅用于那些 NAT 環(huán)境特別嚴(yán)苛、P2P 直連失敗的場景。注意不要依賴外部免費(fèi)的 STUN生產(chǎn)環(huán)境自建 STUN 能減少公共服務(wù)的抖動影響。實(shí)測中有個經(jīng)驗(yàn)當(dāng)瀏覽器走 mDNS 隱藏真實(shí)候選時內(nèi)網(wǎng)直連仍然可用但候選收集耗時會長 50ms 左右。所以我在ICE gathering state上加了超時控制收集超過 1.5 秒就主動用當(dāng)前的候選列表發(fā)起連接不傻等所有候選收集完。2.4 DataChannel 的可靠性與背壓處理游戲控制指令要求不丟數(shù)據(jù)但歷史上 3 秒前的操作也沒意義。我創(chuàng)建 DataChannel 時設(shè)置如下const dc pc.createDataChannel(game, { ordered: true, maxRetransmits: 3 });ordered: true保證消息按順序到達(dá)maxRetransmits: 3限制最多重傳三次超過就丟棄。這種配置在弱網(wǎng)下既不會堵塞通道也不會因?yàn)閬G一條走位指令導(dǎo)致游戲崩潰。對于畫筆軌跡這種可容忍部分丟失的數(shù)據(jù)我另建一條ordered: false, maxRetransmits: 0的低可靠通道降低帶寬消耗。背壓處理是另一個容易忽略的點(diǎn)。如果游戲主循環(huán)瘋狂往 DataChannel 里塞數(shù)據(jù)bufferedAmount會暴漲并導(dǎo)致卡頓。我在每幀發(fā)送前檢查if (dc.bufferedAmount 1024 * 1024) { dc.send(payload); }超過 1MB 就跳過本幀發(fā)送下一幀再繼續(xù)。這比無腦塞數(shù)據(jù)穩(wěn)健得多也讓瀏覽器內(nèi)存曲線保持平滑。3. Shadow DOM 技術(shù)復(fù)盤樣式隔離之外的收益3.1 把游戲外殼做成 custom elementshadowRoot 的創(chuàng)建與掛載我在平臺里定義了一個通用游戲掛載器所有第三方游戲都包在同一個 custom element 里class GameShell extends HTMLElement { constructor() { super(); this.attachShadow({ mode: open }); } connectedCallback() { const gameId this.getAttribute(game-id); const config getGameConfig(gameId); this.shadowRoot.innerHTML style :host { position: relative; display: block; } .game-container { width: 100%; height: 100%; } /style div classgame-container/div ; bootGame(this.shadowRoot.querySelector(.game-container), config); } } customElements.define(game-shell, GameShell);attachShadow({ mode: open })允許我們通過element.shadowRoot訪問內(nèi)部 DOM方便調(diào)試。如果你的游戲?qū)Π踩砸筇貏e高比如需要防止外部腳本直接篡改內(nèi)部 DOM可以選closed模式但日常咨詢和第三方調(diào)試不建議這么做排查問題會非常痛苦。3.2 全局樣式穿透的四種方式和我的選擇Shadow DOM 不是完全密封的真實(shí)項(xiàng)目里你總需要讓平臺主題顏色、字號進(jìn)入 shadow 樹。我總結(jié)有四種穿透方式方式適用場景我的選擇:host給宿主元素自身設(shè)置樣式比如外層邊框、背景必用:host-context(selector)根據(jù)外部祖先類名切換內(nèi)部樣式少用依賴 DOM 層級::part()給 shadow 內(nèi)的關(guān)鍵元素暴露樣式錨點(diǎn)對外部主題定制很合適CSS 自定義屬性通過繼承把變量傳進(jìn) shadow 樹主力方案我在平臺主題系統(tǒng)里用了 CSS 自定義屬性平臺側(cè)設(shè)置--game-accent-color游戲殼內(nèi)部按鈕直接background: var(--game-accent-color)。這樣游戲作者不用關(guān)心平臺主題如何變化只要遵守自定義屬性契約就能自動適配。這是 Shadow DOM 資料里不怎么強(qiáng)調(diào)的收益它逼迫你把樣式協(xié)議顯式化。3.3 Shadow DOM 對性能的真實(shí)影響樣式計算與長列表很多人擔(dān)心 Shadow DOM 會增加樣式計算成本。我實(shí)際測過在 Chrome 下渲染 100 個游戲卡片每個卡片都包含一個 shadow tree首次布局時間和不加 Shadow 的版本幾乎一致。原因是 CSS 選擇器匹配具有邊界效應(yīng)shadow 樹內(nèi)部的選擇器不會去遍歷外部節(jié)點(diǎn)反而減少了無謂的匹配。但要注意每個 shadow root 都會維持自己獨(dú)立的樹結(jié)構(gòu)大量短生命周期 shadow 節(jié)點(diǎn)可能會造成 GC 壓力。我的平臺上是這樣權(quán)衡的游戲主界面使用 Shadow DOM 封裝生命周期長游戲列表項(xiàng)、匹配卡片不用 Shadow DOM用普通組件 CSS 前綴因?yàn)榱斜眄?xiàng)的樣式?jīng)_突是可控的而性能要求更敏感。原則是需要隔離并且長期復(fù)用的組件才值得掛 shadow臨時列表不要濫用。3.4 事件重定向被事件路徑逼瘋一次之后的理解Shadow DOM 里的事件很多默認(rèn)在邊界被重定向了。比如點(diǎn)擊 shadow 內(nèi)部按鈕事件觸發(fā)目標(biāo)看起來是 host 元素而不是內(nèi)部 button。在 debug 時我一直以為所有點(diǎn)擊都發(fā)生在外層后來才發(fā)現(xiàn)原來是 retarget 機(jī)制在起作用。正確寫法是使用event.composedPath()獲取真實(shí)事件路徑gameShell.addEventListener(click, (e) { const path e.composedPath(); const button path.find(el el.tagName BUTTON); if (button) handleAction(button.dataset.action); });composedPath()是穿透 shadow 邊界的關(guān)鍵 API。如果你要在 shadow 內(nèi)部監(jiān)聽事件建議把事件用冒泡方式綁定在宿主節(jié)點(diǎn)上再統(tǒng)一處理性能和可讀性都比每個內(nèi)部節(jié)點(diǎn)單獨(dú)綁監(jiān)聽好。4. 性能優(yōu)化實(shí)錄渲染路徑、內(nèi)存與幀率4.1 從 60ms 卡頓到穩(wěn)定滿幀Canvas 與 WebRTC 視頻流的坑有一款你畫我猜游戲需要把玩家的攝像頭畫面和畫布疊在一起展示。一開始我直接把video元素插進(jìn) shadowRoot再把 canvas 覆蓋在上面。結(jié)果滾輪縮放、拖動畫布時視頻元素和 canvas 的合成層頻繁重排幀率掉到 30 以下卡得玩家筆都畫不直。后來我改為把視頻流直接繪制到 Canvas 上一個 canvas 同時負(fù)責(zé)背景視頻和畫筆圖層const video document.createElement(video); video.srcObject stream; video.muted true; video.playsInline true; await video.play(); function drawFrame() { ctx.drawImage(video, 0, 0, canvas.width, canvas.height); ctx.drawImage(penLayer, 0, 0); requestAnimationFrame(drawFrame); } drawFrame();這樣一個 Canvas 搞定所有繪制瀏覽器的合成層數(shù)量大幅減少。優(yōu)化后同場景幀率穩(wěn)定在 60fps。代價是每次drawImage(video)都有一次視頻幀解碼拷貝但在小游戲這種尺寸下完全可接受。4.2 用 getStats 盯住連接質(zhì)量不要靠猜WebRTC 內(nèi)置的getStats()接口能拿到非常細(xì)的質(zhì)量數(shù)據(jù)。我之前只看心跳延遲結(jié)果用戶反饋游戲不卡但畫聲音像機(jī)器人后來才發(fā)現(xiàn)是丟包高。完整跟蹤幾個關(guān)鍵指標(biāo)指標(biāo)閾值參考對應(yīng)的用戶體驗(yàn)currentRoundTripTime大于 150ms 預(yù)警操作延遲、轉(zhuǎn)身慢jitter大于 30ms 預(yù)警聲音卡頓、數(shù)據(jù)抖動packetsLost占比超過 2% 預(yù)警畫筆斷線、操作丟步framesPerSecond小于 45 持續(xù) 5s視頻不流暢我把這些指標(biāo)每分鐘上報一次后臺聚合成房間健康度。健康度低于閾值時會自動給玩家彈一個降低畫質(zhì)以保障流暢的按鈕本質(zhì)上切換視頻分辨率而不是讓用戶在卡到不行時自己找設(shè)置。4.3 內(nèi)存泄漏元兇看不到的 MediaStreamTrack這個坑折磨了我整整一個周末?,F(xiàn)象是用戶每次進(jìn)出游戲房間頁面前端內(nèi)存漲十幾兆多進(jìn)出幾次瀏覽器直接崩。用 Chrome DevTools 的內(nèi)存快照對比發(fā)現(xiàn)大量MediaStreamTrack沒被釋放。原因很粗心我在退出房間時只pc.close()沒有主動停止視頻和音頻軌道。function closeStream(stream) { if (!stream) return; stream.getTracks().forEach(track track.stop()); }加了track.stop()之后攝像頭指示燈會正確關(guān)閉內(nèi)存曲線也恢復(fù)到進(jìn)出房間不增長的狀態(tài)。這里提醒各位RTCPeerConnection.close()只會關(guān)閉連接不會停止媒體采集。只要getUserMedia拿到的軌道還活著麥克風(fēng)燈還是會亮。Shadow DOM 側(cè)也有內(nèi)存泄漏風(fēng)險如果 shadowRoot 內(nèi)的元素上綁了引用外部 object 的閉包監(jiān)聽器而自定義元素又被移除保留的監(jiān)聽器會讓整棵樹無法回收。我最后統(tǒng)一封裝了一個disconnectedCallback在這個鉤子里清理所有事件監(jiān)聽和 MediaStream。5. 用戶問得最多的WebRTC 怎么關(guān)閉或禁用5.1 為什么有人要關(guān) WebRTCIP 泄露的原理WebRTC 怎么關(guān)閉這個搜索熱度一直很高。不少用戶擔(dān)心 WebRTC 會把自己的真實(shí) IP 暴露給對方這個擔(dān)心是有技術(shù)依據(jù)的瀏覽器在收集 ICE candidate 時會向 STUN 服務(wù)器請求自己的公網(wǎng)映射地址這個地址會經(jīng) SDP 交換發(fā)送給對端。也就是說即使你在網(wǎng)頁里開著代理或者處于某種隱藏模式下WebRTC 的 STUN 請求仍可能直接走真實(shí)網(wǎng)絡(luò)路徑把公網(wǎng) IP 暴露出來?,F(xiàn)在主流瀏覽器已經(jīng)在改進(jìn)Chrome 和 Firefox 在收集 candidate 時默認(rèn)使用 mDNS 偽裝主機(jī)名把真實(shí) IP 替換成.local的隨機(jī)名稱。mDNS 方案在不破壞 NAT 穿透的前提下大幅降低了 IP 泄露面。但注意并不是所有瀏覽器版本和平臺都默認(rèn)開啟老版本內(nèi)核和部分 WebView 仍然會暴露真實(shí)地址。我在平臺文檔里明確寫了 WebRTC 的數(shù)據(jù)傳輸范圍也告訴用戶協(xié)議默認(rèn)會暴露你的公網(wǎng) IP 給房間內(nèi)其他玩家但不經(jīng)過服務(wù)器轉(zhuǎn)發(fā)。這種透明聲明比藏著掖著好得多能過濾掉一批隱私敏感的玩家也能降低后續(xù)投訴概率。5.2 平臺提供的降級方案禁用 WebRTC 也能玩考慮到用戶可能有更強(qiáng)的隱私偏好或者企業(yè)網(wǎng)絡(luò)管理員直接禁用了 UDP 傳輸我的平臺不能把所有玩法都押在 WebRTC 上。我在設(shè)置面板里加了一個低實(shí)時模式選項(xiàng)開啟后平臺自動進(jìn)行能力檢測function isWebRTCSupported() { return typeof RTCPeerConnection ! undefined; }如果瀏覽器不支持或者用戶主動關(guān)閉了 WebRTC 相關(guān)開關(guān)游戲房間會退回到 WebSocket 短輪詢模式。延遲會升高但對于回合制小游戲猜數(shù)字、成語接龍、棋盤類完全夠用。實(shí)時類游戲在降級模式下會提示當(dāng)前網(wǎng)絡(luò)環(huán)境不支持實(shí)時模式建議打開網(wǎng)頁的 WebRTC 權(quán)限后重進(jìn)。為了真正做到用戶可控我在隱私設(shè)置里提供了命名開關(guān)開啟實(shí)時對戰(zhàn)默認(rèn)開啟使用 WebRTC P2P 傳數(shù)據(jù)關(guān)閉實(shí)時對戰(zhàn)所有房間強(qiáng)制服務(wù)器轉(zhuǎn)發(fā)不上傳攝像頭畫面攝像頭和麥克風(fēng)每次單獨(dú)授權(quán)不在非必要客房請求千萬不要一進(jìn)頁面就調(diào)getUserMedia這樣會給用戶極強(qiáng)的被監(jiān)控感。實(shí)踐證明把授權(quán)請求綁定到用戶主動開始一場需要攝像頭的游戲時授權(quán)通過率會提高好幾倍。5.3 給開發(fā)者的建議不要對用戶隱私傲慢一個很小的細(xì)節(jié)我們的匹配大廳本來為了氛圍燈效果嘗試拿到麥克風(fēng)環(huán)境音量來讓背景呼吸燈波動。雖然技術(shù)上沒有把音頻傳出去但只要用戶看到瀏覽器的錄制紅標(biāo)就會認(rèn)為平臺在偷聽。后來我把這個功能改成點(diǎn)擊開啟氛圍燈后才請求音頻授權(quán)并且頁面上明確寫音頻僅用于本地音量監(jiān)測不會上傳或發(fā)送給其他人。措辭清楚以后用戶的隱私投訴基本為零。用戶搜WebRTC 怎么關(guān)閉說明很多用戶對實(shí)時通信協(xié)議有隱私顧慮。作為平臺方我們應(yīng)該讓用戶可以輕松關(guān)閉而不是用功能綁架用戶。隱私開關(guān)看似降低了功能使用率實(shí)際上卻讓那些愿意打開實(shí)時模式的用戶玩得更放心。6. 復(fù)盤清單哪些決策值得復(fù)用6.1 信令和業(yè)務(wù)邏輯分離這是最值得堅持的決策把信令通道從業(yè)務(wù)通道中獨(dú)立出來以后整個平臺的可用性上升了一個臺階。為什么因?yàn)樾帕钍堑皖l、高關(guān)鍵度的消息業(yè)務(wù)是高頻、可延遲的消息。這兩種消息混在一個通道里要么被擠爆要么觸發(fā)不必要的重連。推薦所有 WebRTC 項(xiàng)目都從第一天就把通道拆開不要圖省事用同一個 socket。6.2 Shadow DOM 的隔離是有代價的別當(dāng)銀彈Shadow DOM 帶來的隔離確實(shí)是硬隔離但它也意味著你失去了一些便利內(nèi)部 DOM 不參與外部表單規(guī)則、默認(rèn)繼承的樣式表現(xiàn)也有細(xì)微差異、所有樣式都需要通過契約傳遞。我建議按照組件復(fù)用頻率和樣式?jīng)_突概率來決定是否啟用 Shadow DOM。不要一聽說能隔離就全站套上那樣會造成大量不必要的 shadow root 內(nèi)存開銷。6.3 如果重來一次我會改什么P2P 人數(shù)上限和權(quán)限管理第一版我把房間 P2P 人數(shù)擴(kuò)展到了 8 人結(jié)果移動端在 8 人 mesh 模式下的 CPU 占用直接爆表?,F(xiàn)在回頭看應(yīng)該在架構(gòu)階段就明確 P2P 房間的上限是 4 到 6 人超過就上服務(wù)器轉(zhuǎn)發(fā)。權(quán)限管理也一樣第一次做只給了全部房間都要攝像頭權(quán)限的設(shè)定后來改成攝像頭只在需要攝像頭的游戲里強(qiáng)制其他游戲默認(rèn)不需要攝像頭卻發(fā)現(xiàn)也能拿到圖像這個邊界必須由平臺代碼控制不能依賴游戲開發(fā)者的自覺。6.4 現(xiàn)在可以繼續(xù)做的方向WebTransport 與語音房間WebRTC 鏈路已經(jīng)穩(wěn)定跑了大半年。最近我注意到 WebTransport 逐漸成熟它能提供比 WebSocket 更低的傳輸延遲而且天然支持多路復(fù)用和流控。下一個版本我想拿 WebTransport 替換掉一部分服務(wù)器轉(zhuǎn)發(fā)模式下的業(yè)務(wù)通道讓非 P2P 場景也能獲得更接近 WebRTC 的實(shí)時體驗(yàn)。語音房間也可以基于 WebRTC 的音頻軌道做混音這一步我已經(jīng)在會議室小規(guī)模驗(yàn)證過效果比預(yù)期好很多。最后分享一個調(diào)試小技巧打開chrome://webrtc-internals可以實(shí)時看到每一條 PeerConnection 的候選配對、比特率、丟包和延遲數(shù)據(jù)。很多明明感覺連接是通的但游戲就是卡的問題在這個頁面上 10 秒就能定位是帶寬、抖動還是路由問題。你這邊的項(xiàng)目如果也在這套組合上翻過車歡迎對照著參數(shù)排查大概率能省下好幾個通宵。