1. 项目概述:为什么Unity WebGL的输入是个“老大难”?
如果你做过Unity WebGL项目,尤其是那些需要复杂交互的,比如网页上的3D编辑器、在线游戏或者数据可视化大屏,那你一定对输入问题深有体会。Unity WebGL的输入支持,用“薛定谔的猫”来形容再贴切不过了——它好像有,又好像没有。默认情况下,Unity WebGL模块对键盘、鼠标、触摸屏甚至游戏手柄都提供了基础支持,但这份支持就像一份“毛坯房”合同,框架给你了,但门窗漏风、水电不通,想直接住进去?门都没有。
核心痛点非常具体:输入焦点丢失。你的应用在网页里跑得好好的,用户一点击网页其他部分,或者切个标签页,Unity的输入就“失联”了,键盘按了没反应,鼠标点击没反馈。移动端适配灾难。虚拟键盘弹不出来,或者弹出来又瞬间消失,触摸输入延迟、不跟手,多点触控更是奢望。输入法兼容性差。中文用户想输入个汉字?很可能打出来的是一串拼音字母直接上屏,或者输入框根本获取不到焦点。这些问题每一个都足以让用户体验跌入谷底,让开发者抓狂。
“WebGLInput”这个项目,就是冲着解决这些“老大难”问题来的。它不是Unity官方输入系统的简单封装,而是一个从底层通信机制到上层应用逻辑进行全面加固和扩展的终极解决方案。它的目标很明确:让Unity WebGL应用的输入体验,无限接近甚至超越原生桌面或移动应用的水平。无论你是要将复杂的桌面工具搬到网页端,还是开发一个面向移动触屏的互动应用,这个方案都能提供一套稳定、可靠、功能完整的输入支持框架。
2. 核心设计思路:在Unity与浏览器之间架起一座“双向高速桥”
要解决问题,得先看清问题的本质。Unity WebGL应用运行在浏览器的沙盒环境中,其输入事件本质上来源于浏览器。Unity的默认实现,是通过Emscripten将浏览器的DOM事件(如keydown,mousemove,touchstart)映射到它内部的输入系统中。这座“桥”太简陋了,而且是单向的、时断时续的。
WebGLInput的设计核心,就是重建这座“桥”,并把它升级为一座双向、全时、高带宽的“高速桥”。它的架构可以拆解为三个层次:
2.1 浏览器层:建立稳固的事件监听与通信枢纽
这一层是基石,用JavaScript实现。它的任务不再是简单转发事件,而是成为浏览器事件的“总调度中心”。
全局、强绑定的事件监听:不再依赖Unity默认的、可能被网页其他元素干扰的事件绑定。WebGLInput的JS脚本会以最高优先级(如
addEventListener的capture阶段)监听整个文档(document)或特定容器上的所有输入事件。这意味着即使用户点击了Unity Canvas之外的区域,只要事件冒泡到文档层面,我们依然能捕获到,为处理焦点问题打下基础。输入状态持久化与同步:对于键盘,维护一个当前按下的键位状态表;对于鼠标,持续跟踪坐标、按钮状态;对于触摸,管理每个触点的生命周期(开始、移动、结束)。这个状态表是独立于Unity帧更新的,确保输入数据的连续性和实时性。
与Unity的主动、双向通信:通过
SendMessage或更高效的Direct Call(il2cpp的[DllImport(“__Internal”)])方式,将处理好的输入数据(如“A键按下”、“鼠标在(100,200)位置”、“第2个触摸点移动”)主动、批量地发送给Unity。同时,也接收来自Unity的指令,比如“请求显示虚拟键盘”、“设置输入框焦点”。
注意:这里有一个关键选择,为什么不用
UnityEngine.Application.ExternalCall?在较新版本的Unity WebGL构建中,直接调用C#函数(通过[DllImport(“__Internal”)]声明)的性能和可靠性更高,避免了字符串函数名解析的开销和潜在错误。这是方案选型时的一个细节优化。
2.2 Unity C#层:构建统一、可扩展的输入服务
收到来自JS层的数据后,需要在Unity内部进行消化和整合。这一层的目标是向上提供一个干净、统一、易于使用的API,同时向下兼容不同的输入源。
输入数据解析与分发:C#层有一个核心的
WebGLInputService单例类。它监听来自JS的消息,将原始数据(可能是JSON字符串或二进制流)解析成结构化的InputEvent对象。然后,它并不直接修改Unity的Input类,而是维护一套自己的输入状态机,并通过C#事件(event Action<InputEvent>)将输入事件分发给所有注册的监听器。与Unity原生输入系统的协同(可选):为了兼容现有大量依赖
Input.GetKey或Input.touches的代码,WebGLInput可以提供一个“模拟层”。这个模拟层会根据自己维护的状态,在每一帧的Update()中,通过反射或接口调用的方式,去设置UnityInput类的内部状态。这样,旧代码无需修改就能工作。但这会带来一定的性能开销和潜在冲突,因此通常建议新项目直接使用WebGLInput提供的新API。平台特定功能的抽象:例如,“显示虚拟键盘”这个操作,在iOS、Android、桌面浏览器上的实现方式差异巨大。C#层会定义一个
IVirtualKeyboard接口,然后在运行时根据浏览器UA识别平台,注入对应的JS实现。这保证了业务逻辑代码的纯净性。
2.3 应用层:提供开箱即用的组件与最佳实践
有了稳定可靠的基础设施,还需要让开发者能快速用起来。这一层提供了一系列预制件(Prefab)和组件(Component)。
WebGLInputModule (UI):一个替换标准
EventSystem下的StandaloneInputModule或TouchInputModule的组件。它直接挂钩到上述的WebGLInputService,确保UI按钮、滑动条等元素能正确响应所有输入事件,完美解决UI点击失效的问题。WebGLInputField:一个增强版的
InputField。它解决了原生输入框在WebGL中的焦点获取、虚拟键盘呼出、输入法合成文本处理等一系列问题。其内部会与JS层紧密协作,确保文本输入流程符合用户预期。可配置的输入映射与组合键:提供一套配置系统,允许开发者轻松地将键盘按键、鼠标按钮、手柄按键映射到游戏内的逻辑操作(如“跳跃”、“攻击”),并支持组合键(如Ctrl+S)的检测。所有配置可以在Inspector中完成,也支持运行时动态加载。
这个三层架构,确保了从底层事件捕获到高层业务逻辑的畅通无阻,每一层都职责清晰,并且为应对各种边界情况留足了扩展空间。
3. 关键技术细节与实现难点剖析
理论架构很美好,但魔鬼藏在细节里。实现这套方案,需要攻克几个关键的技术难点。
3.1 焦点管理的艺术:让输入“永不丢失”
焦点管理是WebGL输入的头号敌人。我们的目标是:当用户点击Unity Canvas时,输入焦点必须牢牢锁定;当用户点击外部时,我们需要智能地决定是释放焦点还是保持。
实现策略:
- 全屏模式下的强制捕获:对于需要沉浸式体验的应用(如游戏),可以在JS层监听
pointerdown事件,并通过event.preventDefault()和canvas.requestPointerLock()(对于鼠标)来尝试锁定指针,甚至阻止浏览器默认行为(如右键菜单)。但这需要用户手势触发,且不能滥用,否则会影响用户体验。 - 非全屏模式的智能焦点:更通用的方案是,在JS层维护一个“输入激活”状态。当用户点击Canvas时,状态设为激活,并主动调用
canvas.focus()。当检测到点击外部、页面隐藏(visibilitychange)时,状态设为非激活。在非激活状态下,我们依然可以捕获事件(如知道用户又点击回来了),但可以选择不发送给Unity,或者发送一个特殊的“焦点丢失”事件,让Unity有机会保存状态或显示提示。 - 输入框焦点的特殊处理:当
WebGLInputField被点击时,除了激活Canvas焦点,还必须通过JS调用inputElement.focus()和inputElement.click()来真正唤起浏览器的文本输入焦点,这是虚拟键盘能弹出来的前提。同时,需要监听input和composition事件来处理输入法组合输入。
// 简化的JS端焦点管理逻辑示例 let isInputActive = false; const canvas = document.getElementById('unityCanvas'); canvas.addEventListener('mousedown', (e) => { // 尝试获取焦点 canvas.focus(); isInputActive = true; // 通知Unity输入已激活 unityInstance.SendMessage('WebGLInputManager', 'OnInputActivated'); }); document.addEventListener('visibilitychange', (e) => { if (document.hidden) { isInputActive = false; unityInstance.SendMessage('WebGLInputManager', 'OnInputDeactivated', 'visibility'); } }); // 在发送输入事件给Unity前检查状态 function sendMouseEventToUnity(x, y, button) { if (isInputActive) { unityInstance.SendMessage('WebGLInputManager', 'OnMouseEvent', `${x},${y},${button}`); } }3.2 移动端触摸与虚拟键盘的深度适配
移动端是另一个重灾区。核心问题是延迟和不确定性。
触摸延迟优化:
- 使用
touch-action: noneCSS样式来禁用浏览器对触摸事件的默认处理(如滚动、缩放),将控制权完全交给我们的脚本。 - 监听
touchstart、touchmove、touchend事件,并立即调用preventDefault()来确保事件的响应速度。将触摸点坐标、标识符、力度等信息高频率地发送给Unity。 - 在Unity端,实现一个触摸预测或插值算法。因为JS事件频率和Unity帧率可能不同步,利用上一帧的数据来平滑当前帧的触摸移动轨迹,可以有效减少卡顿感。
虚拟键盘的可靠呼出与收起:
- 这是一个“黑盒”难题,因为不同浏览器、不同输入法对
input.focus()的反应不一致。我们的策略是“多管齐下”:- 在收到Unity的“显示键盘”请求后,首先确保一个隐藏的HTML
input或textarea元素获得焦点(focus())。 - 紧接着,尝试调用
input.click()或textarea.click(),这是一个更强烈的焦点请求。 - 对于iOS Safari等特定环境,可能需要在用户触摸事件的处理函数中同步触发焦点获取,否则会被系统阻止。
- 监听键盘的显示与隐藏事件(如
visualViewport的变化、resize事件),并将键盘高度实时反馈给Unity,以便调整UI布局(例如将输入框上移,避免被键盘遮挡)。
- 在收到Unity的“显示键盘”请求后,首先确保一个隐藏的HTML
3.3 输入事件的高性能通信与序列化
JS和Unity之间的通信是有成本的。如果每一个鼠标移动事件(一秒钟可能触发数十次)都单独发送一次消息,性能开销会很大。
优化方案:
- 批量处理:在JS端,将一帧(或一个短时间窗口)内发生的多个输入事件(如连续的鼠标移动坐标)收集到一个数组或特定的数据结构中。在
requestAnimationFrame的回调中,将这个批量数据一次性发送给Unity。在Unity端,再按顺序解析和处理这一批事件。 - 高效序列化:避免使用冗长的JSON字符串来传输简单的坐标数据。可以考虑使用
ArrayBuffer和TypedArray(如Int32Array)来打包数据。例如,将10个鼠标移动事件打包成一个[x1, y1, x2, y2, ...]的数组,以二进制形式发送。Unity C#端使用Marshal.Copy等方法来高效解析。这对于需要低延迟的实时应用(如绘画应用)至关重要。 - 差分更新:对于游戏手柄等状态输入设备,不需要每帧发送所有按钮和轴的状态。可以只发送状态发生变化的部分(如“A键从按下变为抬起”、“左摇杆X轴值变化超过阈值”)。
4. 集成与使用:一步步将WebGLInput融入你的项目
理论讲完了,我们来点实际的。如何在一个现有的Unity WebGL项目中集成并使用这套方案?
4.1 环境准备与基础集成
- 获取WebGLInput资源包:通常它是一个
.unitypackage文件。在你的Unity项目中,通过Assets -> Import Package -> Custom Package进行导入。 - 初始化预制件:导入后,你会找到一个名为
WebGLInputInitializer的预制件。将其拖入你的初始场景(通常是启动场景)中。这个预制件上挂载的脚本会在游戏启动时自动实例化WebGLInputService单例,并加载必要的JS桥接代码。 - 替换默认输入模块:在你的UI Canvas所使用的
EventSystem游戏对象上,将Standalone Input Module组件移除,替换为WebGLInputModule组件。这是确保UI交互正常的第一步。
4.2 处理键盘与鼠标输入
对于新的输入逻辑,推荐直接使用WebGLInputService提供的API。
// 在需要监听输入的MonoBehaviour中 void OnEnable() { WebGLInputService.Instance.OnKeyDown += HandleKeyDown; WebGLInputService.Instance.OnMouseClick += HandleMouseClick; } void OnDisable() { WebGLInputService.Instance.OnKeyDown -= HandleKeyDown; // 务必记得取消注册,防止内存泄漏 } void HandleKeyDown(KeyCode keyCode, bool isRepeat) { if (keyCode == KeyCode.Space && !isRepeat) { // 处理空格键按下(非连发) PlayerJump(); } } void HandleMouseClick(Vector2 screenPosition, int button) { if (button == 0) { // 左键 Ray ray = Camera.main.ScreenPointToRay(screenPosition); if (Physics.Raycast(ray, out RaycastHit hit)) { // 处理3D物体点击 hit.collider.GetComponent<MyInteractiveObject>()?.OnClick(); } } }如果你有大量遗留代码依赖Input.GetKey,可以启用“模拟原生Input”的选项(在WebGLInputInitializer上配置)。启用后,WebGLInputService会在每帧LateUpdate中同步状态到Unity的Input类。但要注意,这可能会与Unity原有的WebGL输入产生冲突,通常建议在新项目中关闭此选项,逐步迁移到新API。
4.3 为移动端启用增强触摸与虚拟键盘
触摸输入:
WebGLInputService提供了GetTouch和touchCount等接口,用法与Unity原生Input类似,但数据来源更可靠。同时,它还会发布OnTouchBegin、OnTouchMove等更精细的事件。集成WebGLInputField:
- 将场景中需要文本输入功能的
InputField组件,替换为WebGLInputField组件。 - 在Inspector中,你可以配置一些选项,比如“移动端自动弹出键盘”、“键盘类型”(默认、数字、邮箱等,这会影响移动端虚拟键盘的布局)。
- 它的使用方式与普通
InputField完全一致,你可以像以前一样绑定onValueChanged或onEndEdit事件。
public WebGLInputField usernameInputField; void Start() { usernameInputField.onEndEdit.AddListener(OnUsernameSubmitted); }- 将场景中需要文本输入功能的
处理键盘高度:当移动端虚拟键盘弹出时,可能会遮挡输入框。
WebGLInputService提供了OnKeyboardHeightChanged事件。void Start() { WebGLInputService.Instance.OnKeyboardHeightChanged += AdjustUIForKeyboard; } void AdjustUIForKeyboard(float keyboardHeightInPixels) { // 将像素高度转换为你的UI坐标系中的高度 float heightInUnits = ...; // 调整你的输入框或面板的位置 if (keyboardHeightInPixels > 10) { // 键盘弹出,将输入框上移 } else { // 键盘收起,恢复原位 } }
4.4 配置输入映射(适用于游戏)
对于游戏项目,你可以在编辑器中通过创建InputActionAsset(一种ScriptableObject)来配置输入映射。
- 在Project窗口右键
Create -> WebGLInput -> Input Action Asset。 - 双击新建的Asset,打开配置窗口。在这里你可以创建“动作”(Action),如“Move”、“Jump”、“Attack”。
- 为每个动作绑定一个或多个“控制路径”,例如“Jump”可以绑定到“键盘/Space”和“游戏手柄/Button South”。
- 在代码中,通过
WebGLInputService.Instance.GetAction("Jump").WasPressedThisFrame()来查询动作状态。这种方式将输入设备与游戏逻辑解耦,未来更换按键配置或支持新的手柄都会非常方便。
5. 实战中遇到的坑与解决方案实录
再完善的方案,在千奇百怪的浏览器和设备面前也会遇到挑战。下面是我在多个项目中实际踩过的一些坑和解决办法。
5.1 常见问题速查表
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 点击Canvas无任何反应 | 1.WebGLInputModule未正确挂载或启用。2. JS桥接脚本未成功加载或执行。 3. Canvas的Raycaster被禁用。 | 1. 检查EventSystem上的输入模块是否为WebGLInputModule。2. 浏览器开发者工具(F12)查看Console是否有JS错误。检查 WebGLInputInitializer预制件是否在场景中。3. 检查Canvas上的 Graphic Raycaster组件是否启用。 |
| 键盘输入在部分浏览器失效 | 1. 焦点未成功锁定在Canvas。 2. 浏览器安全策略阻止了某些键的默认行为(如F1、F12)。 3. 输入法处于组合输入状态。 | 1. 在JS端加强焦点获取逻辑,尝试在mousedown和touchstart事件中都调用canvas.focus()。2. 对于功能键,在JS端监听 keydown事件并调用preventDefault()需谨慎,可能会影响浏览器快捷键。通常只对游戏常用键(WASD、空格等)这样做。3. 监听 compositionstart和compositionend事件,在输入法组合期间,暂缓将按键事件发送给Unity。 |
| 移动端虚拟键盘不弹出 | 1. 焦点未成功转移到HTML输入元素。 2. 触摸事件被上层UI元素拦截。 3. iOS特定版本或浏览器的Bug。 | 1. 确保WebGLInputField组件被使用,并且其内部的调用栈成功执行了inputElement.focus()和click()。2. 检查是否有全屏遮罩UI挡住了输入框,但其 Raycast Target却是开启的,这会导致触摸事件被它“吃掉”。关闭不必要的Raycast Target。3. 尝试在 touchstart事件的回调函数中,同步调用焦点获取函数,这是一个针对iOS的常见Workaround。 |
| 输入有明显延迟或卡顿 | 1. JS到Unity的通信过于频繁。 2. Unity端每帧处理输入事件的逻辑过于复杂。 3. 浏览器性能瓶颈。 | 1. 启用WebGLInput设置中的“输入事件批量处理”选项。 2. 优化C#端的事件处理函数,避免在每帧的输入处理中进行昂贵的查找或计算。 3. 在浏览器性能面板中分析,看是否是其他脚本或渲染造成的主线程阻塞。降低Unity WebGL的图形设置也可能有帮助。 |
| 游戏手柄连接后无响应 | 1. 浏览器不支持Gamepad API,或需要用户先与手柄交互(“用户手势”要求)。 2. 手柄映射配置错误。 | 1. 在页面中添加提示,告知用户“请先按下手柄上的任意按钮以激活”。Gamepad API通常需要用户先与页面交互才能访问。 2. 检查WebGLInput的手柄映射配置。不同品牌手柄的 gamepad.buttons和axes索引可能不同,可能需要提供多种预设配置供用户选择。 |
5.2 独家避坑技巧
“首帧输入丢失”问题:Unity WebGL应用启动后,第一帧或前几帧,输入可能无效。这是因为JS桥接脚本的初始化与Unity Player的初始化存在时序问题。解决方案:在
WebGLInputInitializer的启动脚本中,加入一个小的延迟(例如用Invoke延迟0.5秒)再正式启用输入监听,或者监听Unity的Application.isFocused状态,在确认应用获得焦点后再开启输入流。处理“失焦”状态的优雅降级:当用户点击浏览器地址栏或切换到其他标签页时,我们通常希望游戏暂停或输入停止。除了监听
visibilitychange,还可以监听window的blur事件。在blur事件中,除了暂停游戏,最好还能向Unity发送一个“重置所有输入状态”的事件,将所有按键按下状态、鼠标按钮状态归零。这样可以防止出现“切回游戏后,角色还在自动向前跑”的灵异现象。移动端“触摸穿透”问题:当你的UI有一个半透明的遮罩层,并且你希望点击它能穿透到后面的3D物体时,需要小心处理。
WebGLInputModule默认会处理所有触摸事件。你需要为可穿透的UI元素设置一个特定的Layer,并在WebGLInputModule组件中配置“忽略的Layer”,或者在该UI元素的触摸事件处理函数中,在处理完自身逻辑后,手动将事件“转发”给后续的射线检测。自定义光标样式的坑:如果你想在WebGL中改变鼠标光标样式(比如变成一把剑),直接设置
css cursor: url(‘sword.cur’), auto;可能在某些浏览器下不生效,尤其是自定义图片格式。一个更可靠的方法是,在Unity中渲染一个跟随真实鼠标位置的Sprite来模拟光标,并隐藏浏览器的原生光标(cursor: none)。这给了你最大的控制权,但也要处理好鼠标锁定和坐标转换。
6. 性能调优与进阶用法
当你的应用变得复杂,或者对输入响应要求极高时(如60FPS的节奏游戏),就需要考虑更深层次的优化。
6.1 输入系统的性能剖析
使用Unity的Profiler(记得在WebGL构建时启用Deep Profiling)来监控:
WebGLInputService.Update()的耗时:它负责从JS接收数据并分发事件。如果耗时超过1ms,就需要检查是否在处理过于复杂的事件逻辑或监听者过多。- JS与C#的通信频率:在浏览器的开发者工具Performance面板中,记录一段时间,查看
SendMessage或相关函数调用的频率。如果每帧调用数十次,就应该启用批量处理。
一个实用的技巧是,为输入事件设置不同的优先级。例如,将“射击”、“跳跃”这类瞬时动作设为高优先级,立即处理;将“移动”、“视角转动”这类连续状态设为低优先级,可以合并到一帧的最后处理,甚至可以进行插值平滑。
6.2 支持更多输入设备
WebGLInput的架构很容易扩展以支持新设备。
- 更多游戏手柄:Gamepad API标准支持多个手柄。在JS端循环遍历
navigator.getGamepads()数组,为每个连接的手柄创建数据流,并通过同一个通信通道(但带上手柄ID标识)发送给Unity。在C#端,维护一个Dictionary<int, GamepadState>来管理多个手柄的状态。 - 陀螺仪与加速度计:通过JS的
DeviceOrientation和DeviceMotion事件可以获取这些数据。这些事件频率很高,需要做节流处理。通常用于移动端的视角控制或平衡类游戏。在Unity端,可以将这些数据转换为Input.gyro的模拟值,或者提供新的API。 - 绘图板压力感应:部分浏览器支持Pointer Events API中的
pressure属性。通过监听pointermove事件,可以获取笔触压力,实现更自然的绘画效果。需要在JS端判断输入设备类型(pointerType === ‘pen’)并转发压力值。
6.3 与UI框架(如UI Toolkit)的整合
如果你的项目使用了较新的UI Toolkit(原名UIElements)来构建编辑器扩展或运行时UI,你需要让WebGLInput也能支持它。UI Toolkit有自己独立的事件系统。
整合思路:
- 事件转发:在
WebGLInputService分发事件时,除了调用传统的OnMouseClick等C#事件,同时将事件数据封装成符合UI ToolkitEventBase体系结构的事件(如PointerDownEvent)。 - 手动派发:获取当前面板的
IPanel,然后使用panel.SendEvent(event)方法,将创建好的事件手动派发到UI Toolkit的事件队列中。这样,UI Toolkit构建的UI元素就能接收到来自WebGLInput的输入事件了。这个过程需要仔细处理坐标转换(屏幕坐标到面板局部坐标)和事件目标查找。
7. 测试策略:确保输入在各种环境下都坚如磐石
输入系统的稳定性必须通过严格的测试来保障。建议建立多层次的测试方案。
- 单元测试(关键逻辑):为
WebGLInputService的核心逻辑编写单元测试,例如输入事件的数据解析、状态机转换、组合键检测算法等。这可以在Editor模式下完成,不依赖WebGL环境。 - 集成测试(模拟浏览器环境):使用如Unity的Play Mode测试框架,配合一个模拟的JS桥接层(Mock)。这个Mock层可以模拟发送各种键盘、鼠标、触摸事件序列,验证整个C#端的响应是否正确,UI是否能被正确点击。
- 端到端(E2E)自动化测试(可选但强大):这是最接近真实用户场景的测试。可以使用像Playwright或Cypress这样的浏览器自动化工具,编写脚本控制Chrome或Firefox,加载你的WebGL构建页面,然后模拟用户点击、打字、拖拽等操作,并断言页面上的反馈(如分数增加、角色移动)。这能发现那些只在真实浏览器中才会出现的兼容性问题。
- 真机云测试:对于移动端兼容性,可以考虑使用如BrowserStack或Sauce Labs提供的真机云测试服务。在几十台不同的iOS和Android设备上自动运行你的测试用例,一次性收集所有设备的输入表现报告。
最后,我个人在多个重度依赖WebGL输入的项目中实践下来的体会是,没有一个方案是银弹。WebGLInput提供了坚实的基础和解决大多数问题的工具,但面对极其特殊的业务场景或刁钻的客户端环境,可能仍然需要你根据其架构进行微调或打上针对性的补丁。关键是要理解其设计原理,这样当问题出现时,你就能快速定位到是JS层、通信层还是C#逻辑层的问题,并运用提供的扩展点去解决它。把输入这个“基础设施”打牢,你的WebGL应用在用户体验上就成功了一大半。