HeapPlay

如何为只支持键盘的网页游戏加上触屏操作?

在游戏上方叠加一个独立的触控层,把游戏本来就能理解的输入交给它。本文介绍我们在 KittenGalaxy 上的做法,以及尚未测试的部分。

加一个独立的屏幕按键层,把触摸转换成你的游戏本来就能处理的输入,并且为游戏的每个界面显示不同的按键。你不需要重写游戏。我们就是这样让 KittenGalaxy 可以在手机上玩的,本文会逐一讲解其中的决定,包括我们还没能测试的部分。

游戏原本期待什么样的输入?

KittenGalaxy 是一款街机射击游戏,写成一个在 canvas 上绘图的 JavaScript 文件。它的全部输入来自三种方式:用于飞行、开火和菜单的按键;用于输入飞行员名字的字符;以及用于商店列表和数学挑战中画在 canvas 上的数字键盘的鼠标点击。

在手机上,这就让玩家无键可按。没有按键,而画在 canvas 上的数字键在 720 单位宽的游戏区域中宽 38 单位,到了 375 像素宽的手机上只有大约 20 个 CSS 像素宽。这太小了,很难稳定地点中。

触摸应该设置共享的输入状态,还是发送按键事件?

常见的设计有两种。

方式工作原理适用情况
共享输入状态键盘和触摸的处理函数都设置 left、fire 这样的标志;游戏循环只读取这些标志游戏代码是你自己的,可以改变它读取输入的方式
合成按键事件触控层派发 keydown 和 keyup 事件;游戏已有的监听器会收到它们游戏代码需要保持原样

共享状态是更干净的设计,如果你今天才开始做游戏,就这样做。我们选择合成按键事件只有一个原因:KittenGalaxy 是在别处开发的,我们把它的文件逐字节按收到的样子保留,这样换用新版本时就不必重做我们的改动。脚本创建的事件确实会到达普通的监听器。它们不会触发浏览器自身的默认行为,而游戏也用不到那些行为。

触控层还需要知道游戏正处在哪个界面。为此,我们的构建步骤添加了一个小小的只读对象,暴露少量的值,例如当前状态以及是否装了火箭。其中没有任何东西能改变游戏。

手指的触摸进入触控层,触控层向未经修改的游戏发送按键事件并读取它的状态
触控层所在的位置。

每个界面需要哪些按键?

固定的一组按键通常是第一次尝试,而游戏一旦有菜单,它就行不通了。请列出每个界面,以及玩家在那里必须能做的事。

界面玩家需要什么我们显示什么
菜单开始、切换或添加飞行员一个大的主按钮,外加只在适用时才出现的小按钮
为飞行员命名输入名字一个真正的文本框,让手机弹出键盘
飞行移动、开火、使用附加装备、暂停一个拇指摇杆、自动开火,附加装备的按钮只在拥有后才出现
商店浏览和购买标签页和方向按钮;点一行是选中,再点一次是购买
数学挑战输入数字一个全尺寸的数字键盘
游戏结束、结局继续或离开两个按钮

有三个细节比我们预想的更重要。

文字输入要用真正的文本框。只有获得焦点的文本框才会让手机弹出键盘。我们不让这个文本框自己的按键传到游戏里,而是把写好的名字一次性输入游戏。

手指无法悬停。用鼠标时,指向商店的某一行是选中,点击才是购买。而一次点按会同时完成两者,很容易误买。触控层把对某一行的第一次点按变成选中,第二次才放行为购买。

隐藏不适用的东西。装了火箭时才出现火箭按钮,持有炸弹时才出现炸弹按钮。按钮越少,按钮就能越大。

如何做一个拇指摇杆?

游戏中的飞船像方向键一样朝八个方向移动,所以摇杆只需要决定哪些方向键处于按下状态。我们的摇杆出现在拇指落下的位置,忽略 12 像素死区内的移动,当拇指在某个轴上移动得足够远时,就把那个方向视为按下:

const DEAD = 12;
function steer(dx, dy) {
  const far = Math.hypot(dx, dy) > DEAD;
  const want = {
    ArrowLeft:  far && dx < 0 && Math.abs(dx) >= Math.abs(dy) * 0.41,
    ArrowRight: far && dx > 0 && Math.abs(dx) >= Math.abs(dy) * 0.41,
    ArrowUp:    far && dy < 0 && Math.abs(dy) >= Math.abs(dx) * 0.41,
    ArrowDown:  far && dy > 0 && Math.abs(dy) >= Math.abs(dx) * 0.41,
  };
  // press the keys that became true, release the ones that became false
}

0.41 是 22.5 度的正切值,它把圆分成八个相等的扇区。当拇指移出边缘时,摇杆的中心会跟着移动,所以反向只需要很小的动作,而不是很长的一段。

我们使用了指针事件,它用一套处理函数覆盖触摸、触控笔和鼠标;还使用了指针捕获,这样拇指滑出摇杆后,摇杆仍能继续收到它。默认自动开火,并为喜欢开火按钮的玩家提供了开关:整局游戏一直按住一根拇指很累,而射击游戏持续开火也损失不了什么。

怎样避免浏览器和游戏“打架”?

除非另有说明,浏览器会把触摸当作滚动、缩放和选择文字。

  • 在游戏及其按键上设置 [touch-action: none](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/touch-action),这样拖动就归你处理。
  • 关闭游戏区域上的文字选择和长按菜单。
  • 窗口失去焦点时,松开所有按住的键。否则通知占据屏幕之后,飞船还会一直向左飞。
  • 由一次真实的点按来启动声音。浏览器只有在玩家与页面交互之后才允许播放音频,所以第一次点按也会唤醒游戏的声音。
  • 用定时器更新按键,而不只是在绘制循环里更新,这样在画出第一帧之前,正确的按钮就已经在那里了。
  • 利用游戏留空的边条。竖屏时,我们的正方形游戏区域位于中间,所以按键放在上下的黑色边条里。横屏时,它们放在角落并保持半透明。

我们测试了什么,没测试什么?

在发布当天,即 2026 年 10 月 8 日,我们在设置为手机尺寸的桌面浏览器中检查了触控层,包括竖屏(375 × 812)和横屏(812 × 375),并用脚本模拟触摸来操作:通过文本框创建飞行员、开始一局、摇杆的全部八个方向、商店,以及在数字键盘上回答一道数学题。

我们还没有在真正的手机或平板上玩过。模拟无法体现摇杆在真实拇指下的手感、每款手机键盘的表现,或者较慢的设备能否应付。游戏页面在“手机浏览器”这一游玩方式下正是这样写的,等试过真机之后我们会更新。如果你在手机上玩了,写一条评价告诉我们设备和实际情况,就是你能发来的最有用的东西。

常见问题

触屏操作需要游戏引擎吗?不需要。按钮是放在 canvas 上方的普通 HTML 元素,由浏览器判断点到了哪一个。

按钮应该画在 canvas 上,还是用 HTML 做?大多数情况下用 HTML。命中检测、多点触控和无障碍标签都不用自己写,还可以用 CSS 调整样式。

触屏按钮应该多大?游戏过程中要按的按钮,在竖屏下每边至少 46 个 CSS 像素,主要按钮更大;角落里的几个功能按钮特意做得小一些。我们替换掉的 canvas 按键大约只有 20。

玩家会发现隐藏的手势吗?假设不会。我们触控层中的每个操作都有一个看得见、带文字的按钮。

手机操作还没做完,可以先把游戏收录到 HeapPlay 吗?可以。每种游玩方式都单独列出、单独核对,所以你现在就可以带着电脑端的方式提交游戏,以后再添加手机端。之后,经过验证的开发者可以自己记录每个版本;参见开发者能得到什么。

touch-controls mobile browser-games

正在做游戏?把它提交到 HeapPlay,并了解开发者能得到什么。