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 的三条技术铁律:
- 100% 离线优先:零外部 CDN 依赖、零网络追踪、零广告 SDK。所有游戏资源打包进浏览器扩展,断网也能玩。
- P2P 联机原生支持:WebRTC DataChannel 直连,不经过我们的服务器,延迟最低可以压到 1ms 以内。
- 物理隔离沙盒:每一款游戏都跑在独立的 Shadow DOM 里,CSS 不污染、JS 不泄露、崩溃不影响主页面。
这三条原则听起来简单,但要全部落地,需要解决一大堆工程问题。接下来我们逐层拆解。
二、整体技术架构
2.1 架构分层
OmniGame 的整体架构可以分为四层:
┌─────────────────────────────────────────────────┐
│ 应用层 │ 官网 / 扩展商店页 / 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 的架构是这样的:
玩家 A ←→ 中转服务器 ←→ 玩家 B
所有数据都要经过我们的服务器中转。如果两个人在同一个办公室、连同一个 Wi-Fi,数据包也要绕一圈公网再回来——延迟至少 30-50ms,还浪费服务器带宽。
WebRTC 的架构是这样的:
玩家 A ←→ P2P 直连 ←→ 玩家 B
一旦连接建立,数据直接在两台设备之间传输,不经过任何服务器。局域网内延迟可以压到 1-5ms,跨网直连(通过 STUN 打洞)通常也在 50ms 以内。而且因为不经过我们的服务器,带宽成本是零——用户越多,我们越省钱。
当然,WebRTC 不是没有缺点。它的 API 非常底层:ICE 候选收集、STUN/TURN 服务器配置、SDP 协商、DataChannel 配置……一堆细节要处理。我们做的事情,就是把这些底层细节全部封装起来,给上层游戏开发者暴露一个极其简单的 API:
// 创建房间
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),把这个数字发给朋友,朋友输入就能加入。
背后的原理是:
- 房主创建房间后,我们的信令服务器(非常轻量,只做消息转发,不传输游戏数据)会生成一个 6 位房间号
- 房主和信令服务器建立 WebSocket 连接,等待加入
- 朋友输入 6 位房间号,信令服务器把两个人的 SDP Offer/Answer 互相转发
- 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,负责所有游戏实例的生命周期管理:
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。我们这已经是极致优化了。
我们的优化手段包括:
- 游戏 HTML 内联:每一款游戏的 HTML/JS/CSS 全部内联成一个字符串,不发任何额外的 HTTP 请求
- 图片全部用内联 SVG:没有 PNG/JPG,所有图标、游戏素材都是 SVG 矢量图,体积小、不失真
- 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 静态生成站点。我们的部署架构是:
用户 ←→ 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 联机 · 零广告零追踪