ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Unity Editor模拟多点触控:Input.touches虚拟触摸方案与调试实践

Unity Editor模拟多点触控:Input.touches虚拟触摸方案与调试实践 做移动端交互最烦的事情之一就是手势逻辑只能在真机上验证。尤其是老项目还在用旧式 Input Manager也就是 Input.GetTouch、Input.touchCount 这一套 API 的时候想在 Unity Editor 里模拟一下双指缩放、双指旋转常规手段基本抓瞎。我前阵子接手一个维护了两年的手势模块里头全是 Input.touches新输入系统一时半会儿迁不过去老板又天天催着出可演示的版本没办法只能硬着头皮在 Editor 里把多点触控变出来。这一篇把我实际试过的三条路都记录下来编辑器自带的鼠标模拟触摸、往 GameView 里注入 Touch 事件、以及我最终落地的输入访问层加虚拟 Touch 方案。如果你也在维护旧式 Input 系统的项目或者只是想快速验证手势算法应该能少走不少弯路。1. 编辑器自带的鼠标模拟触摸先说清楚它到底几斤几两先说结论官方对这个需求不是没有感知Project Settings 里专门埋了一个开关但用起来真的憋屈。在 Edit Project Settings Editor 面板下能找到Simulate Touch Input From Mouse Or Pen这个选项中文版一般叫通过鼠标或触控笔模拟触摸输入。勾上之后你在 Play Mode 里用鼠标左键Unity 就会把它当成一根手指左键按下生成一个 Began 状态的 Touch按住拖动生成 Moved 状态坐标跟着鼠标走松开生成 Ended 状态代码层面等价于把Input.simulateTouchWithMouse设为 true这个属性也可以在运行时动态改。但不管怎么开它都只生成一根手指。Input.touchCount最理想的情况下就是 1想靠鼠标左键加右键凑两根手指是行不通的后面按钮、触摸板全都用不上。那这个自带的模拟能干什么单击、双击、单指拖拽、滑动列表这些场景它完全够用而且不用写任何额外代码。问题是项目一旦涉及双指缩放、双指旋转、三指手势它就直接废了。我最初的想法很简单既然选项里写了touch input干脆双指也用鼠标模拟算了结果折腾半天发现官方压根没给这个能力。还有个隐藏坑要注意某些 Unity 版本里哪怕你勾了这个选项Input.touchSupported在桌面平台上仍返回 false。别拿这个字段判断当前是否处于触摸模拟状态不然你会得到一堆莫名其妙的判断分支。2. 往 GameView 里注入 Touch 事件能跑但先做好版本兼容的心理准备既然官方只给单指那就得自己动手。网上能搜到不少Unity Editor 模拟多点触控的代码核心思路高度一致Unity 的 GameView 本质上是一个 EditorWindow而编辑器窗口本身是靠 Event 驱动的。EventType 里躺着一组触摸事件类型TouchDown、TouchMove、TouchUp 全都有那直接把构造好的触摸事件发给 GameViewInput 系统是不是就会以为真的有手指按上来了这个方向是对的实际操作也确实能让Input.touches出现模拟触摸。我分别在 2019.4、2020.3、2021.3 上做过验证结果只能说差强人意。核心代码大概长这样#if UNITY_EDITOR using System; using UnityEditor; using UnityEngine; public static class GameViewTouchInjector { public static void InjectTouchDown(int touchId, Vector2 position) { Send(new Event { type EventType.TouchDown, mousePosition position }); } public static void InjectTouchMove(int touchId, Vector2 position, Vector2 delta) { var evt new Event { type EventType.TouchMove, mousePosition position, delta delta }; Send(evt); } public static void InjectTouchUp(int touchId, Vector2 position) { Send(new Event { type EventType.TouchUp, mousePosition position }); } private static void Send(Event evt) { var gameViewType Type.GetType(UnityEditor.GameView, UnityEditor); if (gameViewType null) return; var gameView EditorWindow.GetWindow(gameViewType); if (gameView null) return; // 关键在于让事件带上触摸 ID 和 Touch 指针类型 // 不同 Unity 版本字段名可能不同这里用反射帮你兜底 var flags System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic; var fingerField typeof(Event).GetField(m_FingerID, flags); if (fingerField ! null) fingerField.SetValue(evt, touchId); var pointerField typeof(Event).GetField(m_PointerType, flags); if (pointerField ! null) pointerField.SetValue(evt, (int)PointerType.Touch); gameView.SendEvent(evt); } } #endif我为什么开头就说差强人意因为这套方案在实际使用中很容易翻车主要坑有三个。第一个坑是坐标原点。Event 里的 mousePosition 是 GUI 坐标而 Input.GetTouch 返回的 position 是屏幕像素坐标两者不仅存在编辑器缩放比例的问题y 轴方向在部分版本里还是反的。我当时在 GameView 上写了个探针把鼠标位置和 Input.GetTouch(0).position 同时打出来一比才发现差了一个EditorGUIUtility.pixelsPerPoint系数。这个系数会在编辑器缩放 100%、200% 时变化你在低缩放屏上调好的参数换到 Retina 外接显示器上可能又偏了。第二个坑是焦点。GameView 必须处于聚焦状态SendEvent才有机会把事件送进去。如果焦点在 SceneView 或 Inspector 上事件要么丢失要么被别的窗口消费掉。你可以先调用gameView.Focus()强制抢焦点但这也意味着整个测试过程中不能随便切窗口很影响看日志和调参。第三个坑是版本兼容。Unity 内部字段名、Event 事件源的实现改动的频率比公开 API 高得多。我 2019.4 上跑得好好的代码换到 2020.3 的小版本里 touchId 就传不进去了得重新反编译看字段名。2021.2 之后又是一套逻辑。每次升级 Unity这个工具都可能静默失效而且是不报错的那种失效你只会发现 touchCount 一直是 0排查起来特别浪费时间。所以我的结论是GameView 注入方案适合作为原理验证或临场演示不适合作为正式项目的长期测试工具。真要把它做成每天都要用的东西你还得针对每个 Unity 版本维护一套反射字段成本太高。3. 真正救了我的是输入访问层自己造 Touch 数据不让引擎卡脖子折腾完上面的方案之后我把思路反过来想了想我为什么非要让Input.touches自己返回模拟数据业务代码真正关心的是现在有几根手指、每根手指在哪、处于什么阶段而不是数据到底从哪个系统过来。于是我在项目里加了一层访问层把触摸输入的入口收拢到一个静态类里。真机环境下它照常读UnityEngine.Input.touches编辑器环境下它会把虚拟触摸和真实触摸合并起来一起返回。这样一来编辑器里能模拟多指真机上又能保持原逻辑业务层看到的还是 Touch 结构体旧式 Input 系统的习惯一点都不用变。using System.Collections.Generic; using UnityEngine; public static class GameTouch { private static readonly ListTouch _touches new ListTouch(); public static int TouchCount GetTouches().Count; public static Touch GetTouch(int index) { return GetTouches()[index]; } public static ListTouch GetTouches() { _touches.Clear(); #if UNITY_EDITOR if (EditorTouchSimulator.Enabled) { _touches.AddRange(EditorTouchSimulator.Simulate()); } #endif for (int i 0; i UnityEngine.Input.touchCount; i) { _touches.Add(UnityEngine.Input.GetTouch(i)); } return _touches; } }核心点在于Touch 本身就是一个普通 struct字段全都是公开的可以自己 new 出来手动赋值。这跟伪造输入不一样更像是在输入源前面加了一个合并器。配套的EditorTouchSimulator是我自己写的模拟器交互上做了个很直接的设计按住空格键生成两根虚拟手指围绕当前鼠标位置对称分布滚轮控制两指的间距移动鼠标则整体移动这两根手指。听起来有点抽象实际用起来就是按下空格双指落在鼠标左右两侧模拟双指按下的 Began滚动鼠标滚轮两指间距变大变小就是双指缩放手势按住空格移动鼠标两指整体平移松开空格双指抬起模拟 Ended#if UNITY_EDITOR using System.Collections.Generic; using UnityEngine; public static class EditorTouchSimulator { public static bool Enabled true; private const int FingerA 9901; private const int FingerB 9902; private static readonly Dictionaryint, Vector2 _positions new Dictionaryint, Vector2(); private static float _gap 120f; private static bool _spacePressed; public static ListTouch Simulate() { var result new ListTouch(); bool spaceDown UnityEngine.Input.GetKey(KeyCode.Space); if (!spaceDown) { _spacePressed false; _positions.Clear(); return result; } bool began !_spacePressed; _spacePressed true; float scroll UnityEngine.Input.GetAxis(Mouse ScrollWheel); if (Mathf.Abs(scroll) 0.0001f) { _gap Mathf.Clamp(_gap scroll * 40f, 30f, 600f); } Vector2 mousePos UnityEngine.Input.mousePosition; result.Add(BuildTouch(FingerA, mousePos new Vector2(-_gap * 0.5f, 0f), began)); result.Add(BuildTouch(FingerB, mousePos new Vector2(_gap * 0.5f, 0f), began)); return result; } private static Touch BuildTouch(int fingerId, Vector2 pos, bool began) { _positions.TryGetValue(fingerId, out Vector2 last); _positions[fingerId] pos; var touch new Touch { fingerId fingerId, position pos, rawPosition pos, deltaPosition began ? Vector2.zero : pos - last, deltaTime Time.deltaTime, tapCount 1, type TouchType.Direct, radius 20f, radiusVariance 2f }; if (began) { touch.phase TouchPhase.Began; } else if (touch.deltaPosition.sqrMagnitude 0.001f) { touch.phase TouchPhase.Moved; } else { touch.phase TouchPhase.Stationary; } return touch; } } #endif这套方案的第一个好处是稳定。它不碰 Unity 内部字段不涉及反射也不依赖某一版本的 Event 行为。只要旧式 Input 系统还在Unity 公开的 Touch 结构体没有大变它就一直能跑。第二个好处是可控。我想模拟双指缩放直接调_gap想模拟某根手指静止不动自然就会输出 Stationary 阶段想模拟三指再加个FingerC就行。相比 GameView 注入那种我发了事件但不知道 Unity 消化成什么样的状态这套方案每一步都有明确的数据依据。第三个好处是可以带到真机上复用。如果你的业务代码已经全换成GameTouch那么在真机调试时遇到手势复现不出来的情况甚至可以临时加一个远程面板把虚拟触摸和真实触摸叠加起来做对比验证这在纯原生 Input 体系里很难做到。当然代价也很清楚如果项目里全是一行行散落的Input.GetTouch、Input.touchCount你需要先做一次全局替换。别想着全自动替换手动过一遍更好顺便能把每个改动的上下文都看一遍。我当时的做法是先把手势识别、拖拽、点击判定这些高频文件统一换成GameTouch其他低频文件遇到一个改一个一两个版本迭代下来就迁移干净了。4. 如果 touchCount 死活是 0按这个链路排查模拟方案写完之后最常见的现象就是我明明按了空格业务代码里GameTouch.TouchCount或者Input.touchCount还是 0。这时候别急着怀疑脚本逻辑按顺序检查下面几个环节。第一步先看 Player Settings 里的 Active Input Handling。这个选项在 Project Settings Player Other Settings 里有三个状态Input Manager (Old)、Input System Package (New)、Both。如果你项目里装了新 Input System 包又把 Active Input Handling 设成了 Input System Package (New) 只保留新系统那旧式Input.touches这套 API 可能直接不产生任何数据。这是所有为什么触摸没反应问题里最容易被忽略的前置条件。第二步排除自带模拟鼠标触摸的干扰。如果 Project Settings 里勾了 Simulate Touch Input From Mouse Or Pen同时我们自己写的EditorTouchSimulator也开着那你会在空间按下时同时看到一根来自官方的鼠标模拟触摸加上两根虚拟触摸。单看数量可能没什么问题但手指 ID、坐标来源是不一样的如果业务代码里用了手指 ID 做追踪容易产生诡异的行为。我自己在使用虚拟模拟器时会把UnityEngine.Input.simulateTouchWithMouse显式设回 false。第三步检查焦点和事件送达。用 GameView 注入方案时GameView 必须在屏幕上且处于聚焦状态否则SendEvent之后 nothing happens。我当时调试时踩过最无厘头的一坑是GameView 被最小化了事件全发到了空气里但代码没报任何错。这个容易用一句gameView.Focus()先顶上但副作用是每次进入 Play Mode 窗口焦点都会跳过去。第四步给模拟触摸加一个可视化探针。不要盯着业务层的结果猜直接把当前帧的所有触摸数据画出来。最简单的办法就是在某个 UI 脚本里挂一个 OnGUIprivate void OnGUI() { var touches GameTouch.GetTouches(); for (int i 0; i touches.Count; i) { var t touches[i]; GUILayout.Label($touch {t.fingerId}, pos{t.position}, phase{t.phase}); } }这个探针能帮你快速确认三件事模拟器到底返回了几根手指、坐标是否在屏幕范围内、阶段转换是否符合预期。如果探针显示正常但业务手势还是不对那问题基本就锁定在业务代码的逻辑层而不是输入源了。下面这个表是我自己排查时经常对照的现象可能原因处理方式新 Input 包环境下Input.touches永远为 0Active Input Handling 只选了 New改为 Both或迁移到GameTouchInput.touchCount比预期多 1官方鼠标模拟触摸和自定义模拟同时开启关掉Input.simulateTouchWithMouse事件发出去后 touchCount 仍为 0GameView 未聚焦或窗口被最小化调用gameView.Focus()坐标偏到屏幕外Event 坐标与 Screen 坐标的缩放/原点差异用探针打印坐标补缩放系数触摸一直停在 Began 或 Moved虚拟触摸没有在后续帧持续输出按住空格时每帧返回同一 fingerId 的触摸状态这套排查链路同样适用于你将来接其他模拟方案。核心就一句话输入源有问题先看系统设置系统设置没问题再看事件有没有送到目标窗口最后再看数据有没有被业务层正确消费。5. 这套模拟方案用下来最想提醒你的事我在实际使用中发现不管用哪种方式在 Editor 里模拟多点触控都不要把它当成真机的替代品。Editor 模拟能帮你把手势逻辑的骨架跑通比如双指缩放时两指间距变化、旋转时角度变化、抬指后状态复位这些属于逻辑层的问题模拟器完全能暴露出来。但真机的触摸灵敏度、边缘误触、多指同触的帧间时序、不同 Android 机型上报 touch 的间隔差异这些 Editor 里都测不出来最后一步一定得上真机过一遍。还有个小细节按键映射别选跟输入法冲突的键。我在中文输入法下用空格做虚拟双指开关偶尔会出现按键被输入法截走、Simulate 逻辑没收到 KeyDown 的情况。后来我改成编辑器里可配置的 KeyCode 字段默认用键盘右侧的 Shift 或者返回一个自定义 toggle UI这才稳定下来。另外如果团队里有多个人要一起用这套工具我建议把EditorTouchSimulator做成一个 EditorWindow 面板用 UI 按钮代替键盘映射大家用起来更直观。面板上放两个滑动条控制两指间距和旋转角度比让所有人记快捷键要靠谱得多。代码结构不需要大改本质上只是把 Simulate 函数的输入源从键盘换成 GUI 控件。这套方案我前端时间在项目里落地后手势模块的编辑器内调试效率明显上来了至少不用每次改完手势逻辑都往设备上装一次包。
返回列表