引言
大家好,最近为了复活以前常玩的经典网页游戏(特别是使用 Ruffle 模拟器运行的 Flash 游戏),我发现了一个痛点:在手机浏览器上玩 PC 网页游戏,交互体验简直是一场灾难。
市面上虽然有 nipple.js 这样的虚拟摇杆库,但它们在应对现代 Web 模拟器时显得力不从心:无法穿透 Shadow DOM、容易引起焦点丢失、缺乏对鼠标绝对/相对坐标的模拟。
于是,我决定自己造一个轮子:OmniPad —— 一个基于 Vue 3 + TypeScript 的 Headless(无头)虚拟输入引擎。
目前 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/
📦 项目地址
目前开源仓库已建立: 👉 github.com/omnipad-js/…
结语
由于个人精力有限,在完成了这个“完全体” MVP 后,项目目前已进入 Maintenance Mode(维护模式),短期内不再增加新功能。 如果你正在开发基于 Godot、Phaser 导出的 HTML5 游戏,或者也在做 Web 模拟器前端,欢迎 Fork 或直接使用。更欢迎大佬来提 PR(比如贡献一个 React Adapter)!