OmniGame 技术白皮书:从零依赖到 WebRTC P2P,重新定义网页小游戏的工程上限

OmniGame 技术白皮书:从零依赖到 WebRTC P2P,重新定义网页小游戏的工程上限

摸鱼无界 · 对决瞬发

当 100+ 款游戏装进浏览器,当 WebRTC 把局域网延迟压到 5ms 以内,当 Shadow DOM 让每一款游戏互不干扰------这就是 OmniGame。


一、为什么我们要做 OmniGame

1.1 传统网页小游戏的三大痛点

在浏览器游戏这件事上,行业已经走了二十年,但绝大多数产品依然停留在"打开一个网页、加载一堆广告、玩三十秒就弹窗"的初级阶段。我们在调研了近百款网页小游戏平台后,总结出三个根深蒂固的痛点:

第一,加载慢、体积重、广告多。 一个所谓的"秒开小游戏",首屏往往要加载 2-5MB 的 JS 包、几十个广告 SDK、一堆第三方追踪脚本。用户点进去,广告还没播完,耐心已经耗光了。更不用说那些挂着"免费"名头、实际每 30 秒弹一次充值窗口的产品------这根本不是游戏,是广告载体。

第二,单机为主、联机缺失。 大多数网页小游戏只能单人玩,想和朋友联机?要么下载客户端,要么加微信群传文件。WebRTC 技术出来都快十年了,但真正把 P2P 联机做好的网页游戏平台屈指可数。局域网对战这种最自然的场景------两个人坐在同一间办公室、连同一个 Wi-Fi------反而被所有平台忽略了。

第三,污染环境、容易暴露。 上班族玩网页游戏,最大的风险不是被老板看到,而是游戏的 CSS 污染了整个网页、游戏的弹窗跳不出来、游戏的 JS 报错把整个页面搞崩。更糟的是,很多游戏平台为了追流量,会在页面里插一堆悬浮广告、自动弹窗、甚至后台下载------这在办公环境里简直是灾难。

1.2 OmniGame 的技术哲学

基于这三个痛点,我们确定了 OmniGame 的三条技术铁律:

  1. 100% 离线优先:零外部 CDN 依赖、零网络追踪、零广告 SDK。所有游戏资源打包进浏览器扩展,断网也能玩。
  2. P2P 联机原生支持:WebRTC DataChannel 直连,不经过我们的服务器,延迟最低可以压到 1ms 以内。
  3. 物理隔离沙盒:每一款游戏都跑在独立的 Shadow DOM 里,CSS 不污染、JS 不泄露、崩溃不影响主页面。

这三条原则听起来简单,但要全部落地,需要解决一大堆工程问题。接下来我们逐层拆解。


二、整体技术架构

2.1 架构分层

OmniGame 的整体架构可以分为四层:

arduino 复制代码
┌─────────────────────────────────────────────────┐
│  应用层  │ 官网 / 扩展商店页 / Web 在线大厅       │
├─────────────────────────────────────────────────┤
│  引擎层  │ 游戏运行时 / 联机引擎 / 沙盒管理器    │
├─────────────────────────────────────────────────┤
│  数据层  │ 本地持久化 / Elo 天梯 / 云存档同步     │
├─────────────────────────────────────────────────┤
│  基础设施│ WebRTC / Shadow DOM / Web Audio / P2P │
└─────────────────────────────────────────────────┘

应用层负责用户入口。目前我们有三个入口:官方网站(Next.js 16 构建)、Chrome 扩展商店版(Manifest V3)、以及即将上线的 Web 在线大厅(免装插件直接在浏览器开玩)。

引擎层是核心。里面有三个子系统:

  • 游戏运行时:负责把每一款游戏加载到独立的 Shadow DOM 沙盒里
  • 联机引擎:封装了 WebRTC DataChannel 的所有底层细节,对外暴露简单的"创建房间 / 加入房间"API
  • 沙盒管理器:管理所有游戏实例的生命周期,包括创建、销毁、聚焦、隐藏

数据层 负责持久化。本地用 chrome.storage.local 加 localStorage 双轨存储,保证在扩展和普通网页两种环境下都能正常读写。天梯分数、游戏进度、设置项全部本地保存,等云存档功能上线后再增量同步到服务器。

基础设施层 就是浏览器原生能力。我们尽量不引入第三方库------WebRTC 用浏览器原生的 RTCPeerConnection,音频用 Web Audio API 直接合成,DOM 隔离用 Shadow DOM------这样整个产品的依赖体积可以压到极小。

2.2 技术选型背后的思考

很多人会问:为什么用 Next.js 16?为什么用 Tailwind CSS 4?为什么用 Framer Motion?

答案很简单:我们要的是"快"和"小"。

  • Next.js 16 (Turbopack):相比 Webpack,Turbopack 的冷启动速度快 10 倍以上,热更新快 100 倍。对于我们这种需要频繁迭代 100+ 款游戏元数据的项目,开发体验的提升是决定性的。
  • Tailwind CSS 4:原子化 CSS 的好处是生产环境最终只打包用到的 class,我们官网最终的 CSS 体积不到 20KB。比起传统的 CSS-in-JS 方案,性能好一个数量级。
  • Framer Motion:动画性能最好的 React 动画库,没有之一。我们官网的所有过渡效果------悬浮球拖拽、弹窗动效、页面切换------都是用 Framer Motion 做的,60 帧不掉帧。

三、核心技术一:WebRTC P2P 联机引擎

3.1 为什么选 WebRTC,而不是 WebSocket

很多人第一反应是:联机游戏用 WebSocket 不就行了?干嘛要上 WebRTC?

答案是:延迟和成本。

WebSocket 的架构是这样的:

css 复制代码
玩家 A ←→ 中转服务器 ←→ 玩家 B

所有数据都要经过我们的服务器中转。如果两个人在同一个办公室、连同一个 Wi-Fi,数据包也要绕一圈公网再回来------延迟至少 30-50ms,还浪费服务器带宽。

WebRTC 的架构是这样的:

css 复制代码
玩家 A ←→ P2P 直连 ←→ 玩家 B

一旦连接建立,数据直接在两台设备之间传输,不经过任何服务器。局域网内延迟可以压到 1-5ms,跨网直连(通过 STUN 打洞)通常也在 50ms 以内。而且因为不经过我们的服务器,带宽成本是零------用户越多,我们越省钱。

当然,WebRTC 不是没有缺点。它的 API 非常底层:ICE 候选收集、STUN/TURN 服务器配置、SDP 协商、DataChannel 配置......一堆细节要处理。我们做的事情,就是把这些底层细节全部封装起来,给上层游戏开发者暴露一个极其简单的 API:

typescript 复制代码
// 创建房间
const room = await OmniNet.createRoom({
  gameId: "gravity-4",
  maxPlayers: 2,
});

// 加入房间
const room = await OmniNet.joinRoom("123456");

// 发送消息
room.send({ type: "move", x: 3, y: 5 });

// 监听消息
room.on("message", (msg) => {
  console.log("收到:", msg);
});

3.2 局域网雷达:最被低估的功能

OmniGame 有一个杀手级功能:局域网雷达。

你在办公室打开 OmniGame,同一个 Wi-Fi 下的所有朋友会自动出现在你的雷达列表里------不需要加好友、不需要输 IP、不需要扫码。点一下就能对战。

这个功能听起来简单,实现起来要解决三个问题:

第一,怎么发现同 Wi-Fi 下的其他设备?

答案是 mDNS + UDP 广播。每台设备启动后,会在 239.255.255.250:1900 端口广播自己的存在,格式是 SSDP 协议。同一局域网内的其他设备收到广播后,就知道"附近有一个 OmniGame 实例"。

Chrome 扩展环境里我们直接用 chrome.mdns API(需要 permissions 声明),普通网页环境里我们用 WebRTC 的 ICE 候选收集机制------因为 STUN 请求会在局域网内广播,我们可以从 ICE 候选地址里提取出局域网内的其他设备 IP。

第二,发现之后怎么建立连接?

发现只是第一步,真正建立连接还是要走 WebRTC 流程。但因为我们已经知道对方的局域网 IP 了,ICE 打洞的成功率几乎是 100%------不需要经过公网 STUN 服务器,直接走局域网内的 host candidate,连接速度从几秒降到几百毫秒。

第三,怎么保护用户隐私?

很多人担心:我打开 OmniGame,隔壁同事的电脑就能看到我?

答案是:默认隐身。雷达发现功能默认关闭,用户需要手动开启"可被发现"模式。而且每一次被发现都会在 UI 上提示------用户完全知道自己正在被谁看到。我们不会偷偷收集任何局域网内的设备信息。

3.3 6 位房间密钥:跨网直连

不在同一个局域网怎么办?比如你在家、朋友在公司,怎么联机?

我们设计了 6 位数字房间密钥。房主创建房间后,会得到一个 6 位数字(比如 123456),把这个数字发给朋友,朋友输入就能加入。

背后的原理是:

  1. 房主创建房间后,我们的信令服务器(非常轻量,只做消息转发,不传输游戏数据)会生成一个 6 位房间号
  2. 房主和信令服务器建立 WebSocket 连接,等待加入
  3. 朋友输入 6 位房间号,信令服务器把两个人的 SDP Offer/Answer 互相转发
  4. SDP 交换完成后,WebRTC 打洞建立 P2P 直连,之后游戏数据不再经过信令服务器

整个过程对用户完全透明。你只需要输入 6 位数字,剩下的连接、打洞、加密全部自动完成。

值得一提的是:因为游戏数据走的是 WebRTC 加密通道(DTLS-SRTP),我们的信令服务器根本看不到任何游戏数据------它只做最基本的信令转发。从隐私角度来说,这是最安全的架构。


四、核心技术二:Shadow DOM 沙盒系统

4.1 为什么游戏需要沙盒

想象一下这个场景:你在 OmniGame 里打开了 2048,玩了一会想换成俄罗斯方块。如果两个游戏的 CSS 都写了 body { background: red; },那会发生什么?------两个游戏的样式互相污染,页面变得乱七八糟。

更严重的是 JS 层面的冲突。如果游戏 A 写了 window.gameState = {...},游戏 B 也写了 window.gameState = {...},那 B 直接把 A 的状态覆盖了。

传统的解决方法是用 iframe。但 iframe 的问题太多了:

  • 性能开销大,每开一个 iframe 就相当于新开一个浏览器进程
  • 通信麻烦,postMessage 各种序列化反序列化
  • 样式隔离是隔离了,但 DOM 操作起来很别扭

我们用的是 Shadow DOM。

4.2 Shadow DOM 是什么

Shadow DOM 是浏览器原生的组件隔离机制。它允许你在一个普通的 DOM 元素下,挂载一个"隐藏的 DOM 子树"------这个子树对外界是完全隔离的:

  • 外面的 CSS 选择器选不到 Shadow DOM 里面的元素
  • Shadow DOM 里面写的 CSS 也不会泄漏到外面
  • 外面的 JS 可以操作 Shadow DOM,但需要通过 shadowRoot 接口

最关键的是:Shadow DOM 是原生 DOM 的一部分,不是 iframe,所以没有进程开销。开 100 个 Shadow DOM 游戏实例,性能和开一个差不多。

4.3 我们的沙盒管理器

我们实现了一个 SandboxManager,负责所有游戏实例的生命周期管理:

typescript 复制代码
class SandboxManager {
  private sandboxes = new Map<string, ShadowRoot>();

  // 创建一个新的游戏沙盒
  create(gameId: string, container: HTMLElement): ShadowRoot {
    const host = document.createElement("div");
    host.style.position = "absolute";
    host.style.width = "100%";
    host.style.height = "100%";
    
    const shadow = host.attachShadow({ mode: "closed" });
    container.appendChild(host);
    
    this.sandboxes.set(gameId, shadow);
    return shadow;
  }

  // 加载游戏 HTML 到沙盒里
  loadGame(gameId: string, html: string) {
    const shadow = this.sandboxes.get(gameId);
    shadow.innerHTML = html;
  }

  // 销毁沙盒
  destroy(gameId: string) {
    const shadow = this.sandboxes.get(gameId);
    shadow.host.remove();
    this.sandboxes.delete(gameId);
  }
}

这里有一个关键设计选择:mode: "closed"。

为什么用 closed 而不是 open?因为我们要保证游戏之间的完全隔离------游戏 A 的 JS 不应该能拿到游戏 B 的 shadowRoot,更不应该能操作 B 的 DOM。closed 模式下,只有创建这个 shadowRoot 的人能拿到引用,外面完全访问不到。

4.4 沙盒带来的额外好处

除了样式和脚本隔离,Shadow DOM 还给我们带来了几个意想不到的好处:

第一,浏览器扩展天然兼容。 Chrome 扩展的 Content Script 环境里,页面的 CSS 和扩展的 CSS 是互相隔离的。但如果我们用 Shadow DOM 做游戏沙盒,扩展注入的游戏和原生网页完全不冲突------甚至你可以在任何网站上打开 OmniGame 的悬浮球,游戏永远在你的 Shadow DOM 里,不会污染网页。

第二,崩溃隔离。 如果某一款游戏的 JS 出了 bug,最多就是这个游戏的 Shadow DOM 里白屏,不会影响 OmniGame 的主界面,更不会影响其他游戏。用户只要关掉这个游戏的沙盒实例,一切照常。

第三,样式完全可控。 因为 Shadow DOM 里的 CSS 是完全独立的,我们可以给每一款游戏注入统一的基础样式------重置 CSS、设置字体、配置主题色------而不用担心影响外面。


五、核心技术三:100% 离线优先架构

5.1 为什么要做离线优先

很多人不理解:现在谁还会断网?为什么要花力气做离线优先?

因为我们的目标用户是上班族。上班族的网络环境是什么样的?------公司 Wi-Fi 不稳定、开会要断网、出差在飞机上、甚至老板走过来的时候你要秒切页面。如果游戏必须联网才能玩,那这个产品的使用场景就砍掉了 80%。

更重要的是:离线优先意味着更快。

一个需要联网的游戏,首屏要等服务器响应、要下载资源、要建立 WebSocket 连接------用户至少要等 2-5 秒。一个离线优先的游戏,所有资源都已经打包在本地了,点击就开,0 秒等待。

5.2 资源打包策略

OmniGame 的所有游戏资源------HTML、JS、CSS、图片------全部打包在浏览器扩展里。用户安装扩展的那一刻,100+ 款游戏就已经全部下载到本地了。

这听起来体积会很大?其实不会。我们做了严格的体积控制:

  • 每一款游戏的平均大小不到 50KB
  • 100 款游戏加起来不到 5MB
  • 加上引擎层、UI 层、联机模块,整个扩展的最终体积不到 8MB

对比一下:一个微信小程序包都要 2MB,一个普通的网页游戏平台首页就 5MB。我们这已经是极致优化了。

我们的优化手段包括:

  1. 游戏 HTML 内联:每一款游戏的 HTML/JS/CSS 全部内联成一个字符串,不发任何额外的 HTTP 请求
  2. 图片全部用内联 SVG:没有 PNG/JPG,所有图标、游戏素材都是 SVG 矢量图,体积小、不失真
  3. Tree Shaking:构建的时候自动删掉游戏里没用到的代码------很多经典游戏(比如贪吃蛇)的原始代码有 10KB,我们最终打包下来不到 3KB

5.3 本地持久化双轨制

游戏进度存在哪?我们做了双轨方案:

  • Chrome 扩展环境 :用 chrome.storage.local,容量大(5MB 起步)、同步可靠
  • 普通网页环境 :用 localStorage,容量小(5MB)、但所有浏览器都支持

上层 API 完全一致,底层自动根据环境切换。这样同一份游戏代码,在扩展和网页里都能正常保存进度。

天梯分数、最高分、游戏设置这些关键数据,我们还做了增量备份------每次保存的时候,同时写两份到不同的存储里。哪怕其中一个存储出问题,还有另一份备份。


六、自研游戏代表作深度解析

6.1 重力方阵(Gravitas-4):重新发明连珠棋

重力方阵是我们的旗舰自研作品。表面上它是一个"四子棋",但实际上我们加了一个核心机制:棋盘可以旋转。

普通四子棋的规则是:棋子从上方落下,沉到最底下,先连成四个的赢。

重力方阵在这个基础上加了一层:每回合你除了落子,还可以选择把整个棋盘顺时针旋转 90°。

旋转的那一刻,所有棋子会因为重力重新坍塌------可能原本不相连的棋子,旋转后突然就凑成了四连绝杀。也可能你精心布置的防线,被人一转直接破掉。

这个机制的技术难点在哪?

第一,坍塌物理模拟。 旋转棋盘之后,所有棋子要逐列计算新的位置------每一列的棋子都要掉到最底下,中间不能有空隙。这个计算看起来简单,但要做到 60 帧流畅旋转效果,需要在 16ms 内完成整个棋盘的重排、补间动画、碰撞检测。

我们的做法是:旋转的 300ms 动画期间,不真的移动棋子------只是用 CSS transform 把整个棋盘旋转 90°。动画结束的那一刻,再瞬间把棋子重置到新的重力位置。用户视觉上感觉是"旋转之后棋子哗啦一下掉到底",但实际上整个过程是 GPU 加速的,完全没有卡顿。

第二,联机状态同步。 旋转棋盘是一个"全局状态变更",两个人看到的棋盘必须完全一致。我们的做法是:旋转操作由房主发起,房主计算好新的棋盘状态,然后把整个新状态广播给所有玩家。这样不管网络延迟多少,所有人看到的棋盘永远是一致的。

6.2 方块死斗:双人消行对轰

方块死斗是俄罗斯方块的双人对战版。你消行,就给对方加垃圾行;对方消行,垃圾行就砸到你这边。谁先堆到顶谁输。

这个游戏的技术难点是实时性。俄罗斯方块这种游戏,晚 100ms 就可能死人。我们用 WebRTC DataChannel 的 unreliable 模式(不可靠传输、不保证顺序、但延迟最低),把每一次的落子、旋转、消行事件实时同步给对方。

因为局域网内延迟只有 2-5ms,两个人的操作几乎是同时的------就像在同一个机器上玩一样。

6.3 暗箱轮盘:心理博弈的 AI 对手

暗箱轮盘是一个"恶魔轮盘"风格的心理战游戏。左轮手枪里装了若干发子弹,两个人轮流开枪------要么自己中弹,要么把枪转给对方。你要根据前面的轮次,推算出剩下的子弹数量,做出最优决策。

这个游戏最有意思的地方是 AI 对手。我们实现了一个简化版的蒙特卡洛树搜索 AI:每一个决策点,AI 都会模拟接下来的所有可能路径,计算出胜率最高的选择。在困难模式下,AI 的胜率大概在 82% 左右------普通人想赢它非常难。


七、摸鱼场景的工程设计

7.1 悬浮球:网页上的常驻入口

OmniGame 的一个核心交互是网页悬浮球。你在任何网站上,右下角都会有一个小小的游戏按钮,点一下就能呼出 OmniGame 小窗,直接玩游戏。

这个功能听起来简单,实现起来要解决一堆问题:

第一,不能污染网页。 悬浮球的 DOM 必须挂在 Shadow DOM 里,不能影响网页本身的 DOM 结构。而且悬浮球的样式要适应各种网页------深色网页上用浅色悬浮球,浅色网页上用深色悬浮球,自动反色。

第二,不能被网页屏蔽。 很多网站会有自己的悬浮按钮、弹窗、广告。我们的悬浮球要能和这些元素共存,不能被网页的 JS 删掉,也不能被网页的 CSS 盖住。我们的做法是:悬浮球的 z-index 设到 2147483647(浏览器允许的最大值),并且每 5 秒检查一次自己有没有被意外移除------如果被删了,立刻重新挂载。

第三,拖拽要流畅。 悬浮球可以拖到屏幕的任何位置。我们用 Framer Motion 的 drag 实现,支持惯性拖拽、边缘吸附------拖到屏幕边缘的时候,悬浮球会自动贴到边上,不会挡着网页内容。

7.2 老板键:毫秒级伪装

老板走过来的时候,按一个键,OmniGame 瞬间消失------屏幕变成一张看起来非常专业的"分布式系统堆栈排查终端",满屏的日志滚动,看起来就像你在认真排查生产环境问题。

再按一下,游戏瞬间恢复,刚才的进度一分不少。

这个功能的技术核心是状态保存与恢复。按老板键的那一刻,我们把所有游戏实例的状态序列化保存到内存里,然后把悬浮球和游戏窗口全部隐藏,弹出伪装终端。按第二次的时候,再把状态恢复回来。整个过程在 10ms 以内完成------比你眨眼睛还快。

伪装终端也不是随便做的------我们真的写了一套模拟的生产日志生成器,输出的日志格式和真实的 Kubernetes 集群日志一模一样:Pod 名称、时间戳、错误级别、堆栈信息......老板站你旁边看三秒,绝对看不出这是假的。


八、Web Audio 原生音效系统

8.1 为什么不用音频文件

一般的网页游戏,音效都是预先录好的 MP3/WAV 文件,打包进项目里。这样做的问题是:

  • 体积大:一个简单的点击音效都要几十 KB
  • 加载慢:要发 HTTP 请求下载,还要解码
  • 风格不统一:不同音效可能录的时候参数不一样,混在一起听着很怪

我们用的是 Web Audio API 实时合成。所有音效都是代码生成的------悬浮的"滴"声、点击的"咔哒"声、胜利的"叮咚"声、老板键的"唰"声------全部是用振荡器 + 增益节点实时合成的。

好处太多了:

  • 零体积:整个音效系统加起来不到 2KB 代码
  • 零加载:打开就有声音,不用等
  • 可调参数:想要更尖一点的声音?改一下振荡器频率就行。想要更闷一点?加个低通滤波器
  • 完全无版权风险:所有声音都是我们自己合成的,不存在任何版权问题

8.2 音效合成的几个例子

最简单的悬浮音效(Hover Blip):一个正弦波振荡器,频率从 800Hz 快速滑到 1200Hz,持续 50ms,音量从 0 渐变到 0.1 再降到 0------就是那种很轻很清脆的"滴"一声。

胜利音效(Bonus Chime):三个音符依次播放(C5 → E5 → G5),每个音符持续 200ms,叠加一点混响------听着就有奖励的感觉。

老板键音效(Shutdown Sweep):一个白噪声经过低通滤波器,频率从 2000Hz 快速降到 100Hz,持续 150ms------就是那种"唰"一下切断的感觉,非常有紧迫感。


九、部署与运维架构

9.1 官网部署栈

OmniGame 官网本身是一个 Next.js 静态生成站点。我们的部署架构是:

scss 复制代码
用户 ←→ Cloudflare CDN ←→ Nginx 反向代理 ←→ Next.js (pm2 进程)
  • Cloudflare:CDN 加速 + DDoS 防护 + SSL 终结。所有静态资源(JS/CSS/图片)全部缓存在 Cloudflare 的全球节点,用户访问的时候就近取,延迟最低可以压到 20ms 以内。
  • Nginx:反向代理 + 负载均衡。负责把 80/443 端口的请求转发到 Next.js 的 8098 端口。
  • PM2:进程管理。保证 Next.js 进程崩溃了能自动重启,服务器重启了能自动启动。

整个栈的成本非常低:一台最低配的云服务器(1核2G)就能跑,加上 Cloudflare 免费版,每个月服务器成本不到 50 块钱。

9.2 信令服务器:轻到极致

WebRTC 联机需要一个信令服务器做 SDP 交换。我们的信令服务器有多轻?

  • 用 Node.js 写的,总共不到 200 行代码
  • 只做一件事:把 WebSocket 收到的消息,转发给同一个房间的其他玩家
  • 不存储任何用户数据、不记录任何游戏内容、不做任何鉴权
  • 一台 1 核 512MB 的小机器,就能同时支撑几万个房间

因为游戏数据完全走 P2P,信令服务器只是个"传话筒"------哪怕信令服务器挂了,已经建立的 P2P 连接完全不受影响,游戏继续玩。

9.3 监控与可观测性

我们没有上复杂的监控系统------对这个量级的产品来说太重了。我们用的是最简单的方案:

  • PM2 自带的日志:所有进程 stdout/stderr 都自动落盘
  • Nginx access log:所有请求的状态码、响应时间、User-Agent 自动记录
  • Cloudflare Analytics:流量、攻击、缓存命中率全部在 Cloudflare 面板里看

出问题的时候,先看 Cloudflare 面板------如果是全局错误,那就是源服务器挂了;如果只有部分用户出问题,那就是他们本地网络的问题。90% 的问题都能在 1 分钟内定位。


十、路线图:从扩展到开放平台

OmniGame 现在只是第一步。我们的完整路线图分四个阶段:

Phase 1:浏览器扩展基石(已完成)

  • 100+ 款离线游戏
  • 局域网雷达 + 6 位房间直连
  • 悬浮球 + 老板键
  • Elo 天梯本地持久化

这是目前的版本。所有核心功能已经跑通,用户可以离线玩、可以局域网联机、可以摸鱼。

Phase 2:Web 在线大厅(进行中)

  • 免装插件,打开网页就能玩
  • 全网房间匹配:不只是局域网,全互联网的用户都能匹配到一起
  • 在线排行榜:所有人的天梯分数实时排名

这一步要解决的问题是:WebRTC 在普通网页环境下,打洞成功率不如扩展环境。很多运营商的 NAT 类型比较严格,P2P 打洞失败的话,就要走 TURN 中继服务器。我们正在部署全球节点的 TURN 服务器,保证打洞失败的用户也能正常联机------只是延迟会高一点(经过中继),但至少能玩。

Phase 3:云存档与跨设备同步

  • 游戏进度、天梯分数跨设备同步
  • 手机、电脑、平板,随时接着玩
  • 赛季制天梯:每个月一个赛季,段位重置,排行榜清零

到这一步,OmniGame 就从一个"摸鱼工具"变成了一个真正的游戏平台。你在公司电脑上玩到一半,回家打开手机,进度一模一样。

Phase 4:创作者工坊与开放 SDK

  • 开放游戏开发 SDK
  • 任何人都可以给 OmniGame 开发新游戏
  • 创作者可以上传自己的游戏,其他用户可以下载玩

最终的目标是:OmniGame 不只是我们做的 100 款游戏,而是一个开放的生态------成千上万的开发者在上面做游戏,用户永远有新东西玩。


十一、技术选型的得与失

做了这么久,我们也踩了不少坑。诚实地总结一下技术选型的得失:

11.1 选对了的

Shadow DOM 沙盒:这是我们最正确的一个决定。它带来的隔离性、性能、兼容性,比 iframe 方案好太多了。没有它,我们根本不可能在一个页面里跑 100 款游戏而不崩。

WebRTC P2P 架构:不仅省钱,而且体验好。局域网 5ms 延迟是任何 WebSocket 方案都做不到的。用户第一次在办公室用雷达秒连对战的时候,那种"怎么可能这么快"的惊讶,就是我们产品的核心竞争力。

零第三方依赖:整个产品除了 React 和 Next.js,几乎没有引入其他第三方库。音效是自己合成的,沙盒是自己写的,联机是自己封装的。好处是:体积小、bug 少、版本升级不用追着第三方库跑。

11.2 踩过的坑

Chrome 扩展 Manifest V3 的限制:V3 对 Content Script 的权限收得很紧,很多 V2 时代能用的 API 现在要额外申请权限。比如 mDNS 发现功能,我们申请了三个月才过审。未来如果 V4 再收紧,可能要做更多的兼容工作。

WebRTC 调试的痛苦:WebRTC 的问题是"有的用户能连上,有的用户连不上"------而且你根本复现不了。我们花了大量时间写日志、抓包、分析各种 NAT 类型,才把打洞成功率从 60% 提升到 95%。剩下的 5% 是极端网络环境,只能靠 TURN 中继兜底。

离线优先的同步问题:所有数据先写本地,等联网了再同步------听起来简单,但冲突怎么处理?比如你离线的时候玩了两局,赢了 50 分,同时你的朋友在线上也用你的账号打了一局,输了 30 分------这时候云端和本地的数据就冲突了。我们现在用的是"本地优先"策略:本地的分数永远比云端新,同步的时候直接覆盖云端。简单粗暴,但对这个量级的用户来说够用了。


十二、结语:技术是为体验服务的

做 OmniGame 的过程中,我们越来越坚信一个道理:技术不是用来炫技的,是用来解决问题的。

WebRTC 不是因为它酷才用的------是因为它能把联机延迟压到 5ms,让用户在办公室里秒连同事对战,这个体验是 WebSocket 永远做不到的。

Shadow DOM 不是因为它新才用的------是因为 100 款游戏装在一个页面里,没有沙盒根本跑不起来,样式和脚本全乱套。

离线优先不是因为它"高级"才做的------是因为上班族就是会在地铁上、在飞机上、在断网的会议室里想玩游戏,你必须让他打开就有。

我们做的所有技术选择,最终都指向同一个目标:让用户玩得爽。

不是"我们用了什么技术栈",不是"我们的架构有多先进",而是------用户点开 OmniGame 的那一刻,游戏立刻就开了;同事坐在隔壁,点一下雷达里的名字就开连;老板走过来,按一下键就消失。

这才是技术真正的价值。

OmniGame 还很早,离我们想象中的样子还差很远。但我们很高兴------我们已经把最硬的骨头啃下来了:联机、沙盒、离线、摸鱼场景。接下来就是把更多游戏做出来、把联机体验做得更顺、把开放平台搭起来。

欢迎你和我们一起,把这个产品做下去。


OmniGame

摸鱼无界 · 对决瞬发

100+ 款离线游戏 · WebRTC P2P 联机 · 零广告零追踪

相关推荐
ss2731 小时前
AI全栈实战 | 2.1-02 CSS 布局:垂直居中为什么是经典面试题?所有布局在解决什么矛盾
前端·css
律宏阔1 小时前
Flutter Riverpod 3:统一打印 Provider 错误与自动重试日志
前端·flutter
周洲08301 小时前
STM32片内Flash读写深度详解|掉电参数保存、底层原理
linux·前端·stm32
data analyse 4561 小时前
埋点工具的私有化部署成本高吗?
前端·数据分析·github
data analyse 4562 小时前
埋点工具的埋点查询语言难学吗?
前端·数据分析
DianSan_ERP2 小时前
多平台订单自动下载与回传的技术实现:从消息推送到状态闭环引言
java·linux·服务器·前端·网络·架构·自动化
恋猫de小郭2 小时前
Shopify 回应为什么从 RN 回到原生,为什么不用 KMP ?
android·前端·flutter
小蜗 strong3 小时前
和电脑猜拳(随机程序应用)
服务器·前端·python
下家4 小时前
为了脱离前端鄙视链,于是自己写个框架 - React 党看完沉默了
前端·前端框架