2026 年,我为 Ruffle 等 Web 游戏模拟器写了一套高保真虚拟输入引擎:OmniPad

7 阅读2分钟

引言

大家好,最近为了复活以前常玩的经典网页游戏(特别是使用 Ruffle 模拟器运行的 Flash 游戏),我发现了一个痛点:在手机浏览器上玩 PC 网页游戏,交互体验简直是一场灾难。

市面上虽然有 nipple.js 这样的虚拟摇杆库,但它们在应对现代 Web 模拟器时显得力不从心:无法穿透 Shadow DOM、容易引起焦点丢失、缺乏对鼠标绝对/相对坐标的模拟。

于是,我决定自己造一个轮子:OmniPad —— 一个基于 Vue 3 + TypeScript 的 Headless(无头)虚拟输入引擎。

omnipad1.gif

目前 MVP 版本 (v0.4.x) 已完成并发布在 NPM。借此机会,和大家分享一下开发过程中趟过的几个底层“深水区”。

痛点 1:如何击穿 Shadow DOM 的“叹息之墙”?

Ruffle 等现代 Web 组件通常运行在 Shadow DOM 中。当你在屏幕上放置一个虚拟按键并模拟派发 MouseEvent 时,传统的 document.elementFromPoint(x, y) 只能摸到最外层的 <ruffle-player> 标签,导致内部的 Canvas 根本收不到点击。

我的解法:实现了一个递归穿透查询器。利用 document.elementsFromPoint 获取坐标下的元素栈,并逐层剥开 shadowRoot 寻找真正的渲染载体。同时,为了避免真实手指的 pointerId: 1 与虚拟信号打架,在 dom.ts 中强制进行 ID 劫持和物理事件隔离。

痛点 2:如何用摇杆完美模拟“鼠标”?

手机屏幕没有“悬停 (Hover)”概念,很多依赖鼠标瞄准的游戏根本没法玩。 我的解法:在 Core 层内置了极其轻量的 GestureRecognizer(手势识别状态机)和 rAF 节流器。

  • 支持将摇杆向量转化为持续的 mousemove 相对位移(光标模式)。
  • 支持“双击并按住”触发拖拽操作。

痛点 3:优雅的 Headless 架构与配置驱动

作为组件库,我将项目设计为了 Monorepo:

  • @omnipad/core:纯 TS 逻辑,没有任何 DOM 依赖(甚至抽象了 AbstractPointerEvent),负责数学计算、状态机和信号分发。
  • @omnipad/vue:纯视觉适配层。提供 VirtualButton 等基本组件,通过 CSS 变量和 Slot 实现样式高度自定义。 所有的摇杆、十字键、按钮,全部通过一套扁平化的 JSON 配置树驱动,支持动态召唤和多手柄(Gamepad API)映射。

🎮 成果展示

你可以点击下方链接,直接体验极其顺滑的物理回弹与多点触控(无需下载),也可以接入 XBox 手柄体验: 👉 omnipad-demo.coocoodaegap.com/

omnipad1.gif

omnipad2.gif

📦 项目地址

目前开源仓库已建立: 👉 github.com/omnipad-js/…

结语

由于个人精力有限,在完成了这个“完全体” MVP 后,项目目前已进入 Maintenance Mode(维护模式),短期内不再增加新功能。 如果你正在开发基于 Godot、Phaser 导出的 HTML5 游戏,或者也在做 Web 模拟器前端,欢迎 Fork 或直接使用。更欢迎大佬来提 PR(比如贡献一个 React Adapter)!