ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Unity编辑器触摸模拟器:用鼠标+Ctrl调试双指手势的完整方案

Unity编辑器触摸模拟器:用鼠标+Ctrl调试双指手势的完整方案 做手游的朋友应该都有过这种经历功能在手机上跑得好好的一回到编辑器就什么都测不了。尤其是我最近在做的这个项目捏合缩放地图、双指旋转相机、滑动操作列表这些交互全部写在了旧式Input系统的触摸接口上。代码里清一色的Input.touchCount、Input.GetTouch在编辑器一跑touchCount永远是0逻辑直接掉进“无触摸”分支。想在PC上验证触摸逻辑要么改代码加鼠标分支要么抱着手机来回预览要么干脆睁一只眼闭一只眼。我之前试过Unity Remote也试过给业务代码补一套鼠标兼容分支但都不够顺。Unity Remote要连着手机跑自动化测试和反复调参数都很费劲鼠标兼容分支写多了代码里到处是if (Input.touchCount 0) ... else if (Input.GetMouseButton(0))这种双轨逻辑维护起来想哭。后来我干脆自己写了个轻量级的“编辑器触摸模拟器”在编辑器里用鼠标加键盘模拟出两个触摸点喂给旧式Input风格的读取代码捏合、旋转、单指拖拽都能直接在Game视图里调。这篇文章就把这套模拟器的设计思路和完整代码分享出来项目还在用旧式Input系统的朋友可以直接搬。1. 先搞明白旧式Input的触摸数据到底长什么样老规矩动手写之前先把底层的API和数据结构盘一遍。很多人写模拟器翻车不是模拟逻辑有问题而是对旧式Input系统本身的触碰数据模型理解不到位。1.1 触摸相关API和数据结构Unity旧式Input系统提供的触摸接口其实非常精简日常会用到的就三样Input.touchCount当前帧的触摸点数量。Input.GetTouch(int index)按索引拿一个Touch结构体。Input.touches返回当前帧所有触摸点的数组方便foreach。真正承载触摸信息的是Touch结构体在Unity源码里它是这样定义的字段类型说明fingerIdint触摸点的唯一ID从按下到抬起保持不变positionVector2当前触摸位置屏幕像素坐标左下角为原点rawPositionVector2未经过任何处理的原始位置deltaTimefloat距离上一帧的时间间隔tapCountint点击次数用于识别双击phaseTouchPhase触摸相位见下方说明pressurefloat按压力度0~1maximumPossiblePressurefloat设备支持的最大压力radiusfloat触摸接触面积半径radiusVariancefloat半径方差altitudeAnglefloat触笔高度角azimuthAnglefloat触笔方位角typeTouchType触摸类型Direct 直接触摸、Indirect 间接触摸、Stylus 触笔phase是整个模拟器的核心它表示触摸点当前处于什么生命周期。TouchPhase有五个取值Began手指刚刚接触屏幕这一帧会触发一次按下事件。Moved手指按住并且在移动。Stationary手指按住但没有移动位置没变化。Ended手指从屏幕抬起这一帧之后就没有该触摸点了。Canceled触摸被系统打断比如来电、窗口失焦、多点触控冲突被系统回收。真实设备上一个手指从按下到抬起通常的相位序列是Began - Moved/Stationary 交替 - Ended。系统可能在某几帧连续发Stationary也可能连续几帧没有更新触摸点。我们写模拟器本质上就是把鼠标的按下、移动、抬起翻译成这套相位序列。1.2 我在编辑器里遇到的三个典型痛点具体到编辑器调试旧式Input系统的坑比想象中多。我把自己踩过的三个典型痛点先列出来后面所有设计都在想办法绕开它们。第一编辑器下Input.touchCount永远是0。这句话在绝大多数没有触摸屏的Windows / macOS开发机上成立。Unity编辑器本身不做触摸模拟鼠标事件被封装成了Input.GetMouseButton那一套独立的API跟触摸数据井水不犯河水。所以项目里那套用触摸接口写的手势识别代码在编辑器里根本不会被执行你只能看到相机一动不动。第二就算你在代码里写好了鼠标兼容分支双指手势依然没法调试。鼠标只有一个点单指滑动、点击的逻辑用鼠标分支尚且能凑合但只要涉及两个触摸点同时操作比如捏合缩放、双指旋转鼠标分支就彻底抓瞎。你不可能用一只鼠标同时按下两个位置。第三代码复杂度会迅速失控。为了调试给业务加鼠标分支时间久了代码里全是“触摸和鼠标双轨处理”的痕迹。触摸逻辑和鼠标逻辑各有细微差别测试时经常漏测某一条路径。偶尔改一处手势识别结果鼠标分支和触摸分支表现不一致bug产生了。这套模拟器的目标就是让业务代码永远只走触摸分支不再为编辑器额外写一套鼠标逻辑。2. 四种“编辑器模拟触摸”方案横评在决定自己动手写之前我把市面上能想到的方案都过了一遍。这里做个横向对比方便你根据自己的项目情况选型。2.1 Unity Remote实时预览适合验证不适合反复调试Unity Remote是老牌方案手机上装一个App通过USB线连电脑运行时把手机屏幕内容实时镜像到编辑器同时把手机触摸事件回传到编辑器里的Game视图。对新Input System和旧式Input都有一定的支持。优点是触摸数据完全来自真实设备相位、压力、触点坐标都是真的比任何模拟都靠谱。缺点是每次都要接手机、装App而且它本质是“串流预览”你没法用编辑器代码去控制触摸点更没法做自动化回归。如果只为了临时看一眼效果好Unity Remote完全够如果是反复调试一个手势识别的边界条件比如“两个手指相距多远算捏合”你会被手机和电脑之间的来回切换搞疯。2.2 新Input System的Touch Simulation功能全但帮不到旧系统Unity的新Input System包里提供了一个叫TouchSimulation的类配合EnhancedTouchSupport可以把鼠标模拟成触摸点还有编辑器内置的Device Simulator支持模拟手机屏幕. 功能上确实很完整操作面板、多点触控模拟都有。但问题是这个方案绑定在新Input System的API体系里。如果你项目还是旧式Input系统代码里全是Input.GetTouch新Input System的模拟数据不会自动进入旧式接口。除非你迁移整套输入系统否则这条路走不通。对于一个线上稳定运行的老项目为调试一个双指手势去做大规模输入系统迁移成本太高了。2.3 给业务代码加鼠标分支能凑合但坑很多很多团队用这个方案发现Input.touchCount为0就在逻辑里补一个else if (Input.GetMouseButton)分支让鼠标模拟单指。单指操作确实立刻能用了但问题也接踵而至。首先双指操作没法解鼠标只有一个位置信息。其次触摸和鼠标的差异不只是坐标来源还有相位和ID管理。鼠标没有fingerId没有Began/Moved/Ended这种明确的相位序列很多业务代码在鼠标分支里只能重新写一套状态机。长此以往同一套手势逻辑维护两遍迟早会出问题。我在项目里就吃过这个亏鼠标分支里忘了处理某个状态转换结果触屏正常、鼠标操作时UI卡死。所以这个方案只适合临时应急不适合长期调试。2.4 自定义触摸模拟器绕开Input接管读取层最终我选择了自己写模拟器核心思路是不去碰Unity引擎内部的那套原生Input数据而是自己维护一个“模拟触摸列表”然后在业务读取层做一个统一入口。业务代码不再直接调Input.touchCount而是调TouchInput.touchCount后者在编辑器模拟开启时返回模拟数据否则返回真实系统的触摸数据。这相当于在原生Input之上加了一个“数据源代理层”好处有三个业务代码只需要维护一套触摸处理逻辑不需要二套鼠标分支。支持多个模拟触摸点双指手势能调试。不影响真实设备运行构建到手机上时完全走原生Input接口。3. 模拟器的核心设计思路动手写代码前先把几个关键设计决策讲清楚。这部分决定了模拟器好不好用值不值得抄。3.1 把鼠标事件翻译成触摸相位我们的数据源是鼠标所以核心工作就是把鼠标事件翻译成TouchPhase序列。映射关系是这样的鼠标事件模拟生成的触摸相位鼠标左键按下Began鼠标左键按住且位置变化Moved鼠标左键按住且位置不变Stationary鼠标左键松开Ended鼠标左键按住但编辑器失焦Canceled这里有一个容易忽略的细节手指按住屏幕但没移动时真机上是会持续输出Stationary的。模拟器也要在鼠标保持按住、位置没变化时输出Stationary而不是直接不发这帧数据。因为有些业务代码会依赖每一帧都能拿到触点数据来做插值、相机制动等计算如果某个帧触点突然消失就会出现一卡一卡的现象。我自己的实现里Moved和Stationary的判断用的是位置是否发生微小变化阈值取0.01像素。如果小于这个阈值就认为手指静止输出Stationary。3.2 触摸ID一个容易被忽略的细节真实触摸点的fingerId在手指按下那一刻分配持续到手指抬起期间不会改变。业务代码经常用fingerId区分不同手指比如判断哪根手指先按下、做双指手势的时候跟踪两个触点的历史位置。模拟器也要保持这个语义。我定义了两个固定ID第一指永远用ID1第二指永远用ID2。为什么不用递增分配的方案两个原因第一编辑器模拟场景下固定ID足够用第二固定ID在识别“哪根手指是第二指”时更直观。如果你想做得更严谨可以设计一个自增ID池按需分配但实际调试中真的没必要。3.3 双指映射用Ctrl鼠标模拟第二根手指这是整个模拟器最关键的设计。既然鼠标只有一个位置怎么模拟出两个触摸点我的方案是这样的第一指直接跟随鼠标左键第二指由Ctrl 鼠标左键激活激活的瞬间第二指的位置被锁定在“鼠标当前位置 一个固定偏移”。激活之后第二指保持那个位置不动而第一指跟随鼠标自由移动。这样做的好处非常明显你可以用鼠标画一条弧线绕过固定的第二指模拟“双指旋转”也可以把鼠标靠近第二指模拟“捏合缩小”往远拖就是“放大”。整个过程只需要一只鼠标和一个Ctrl键手感非常顺。偏移值我默认取(-120, 120)像素也就是第二指默认出现在第一指的左上方。你可以根据屏幕分辨率自己调整代码里我把它做成了可配置字段。3.4 坐标体系触摸位置和鼠标位置怎么对齐这里必须说清楚否则坐标会偏。旧式Input的Touch.position和鼠标的Input.mousePosition都是屏幕像素坐标原点在左下角单位是像素。所以把鼠标位置直接塞给模拟触摸理论上是不需要转换的。但有一个前提鼠标必须保持在Game视图内并且Game视图处于焦点状态。Unity在编辑器里运行时Input.mousePosition返回的是Game视图局部坐标。如果鼠标跑到Game视图外面的Inspector面板上这个坐标依然会更新但数值已经超出了Game视图的分辨率范围。业务逻辑如果没做边界裁剪就会出现触摸点跑到屏幕外的情况。所以我的调试习惯是把Game视图设为“播放时最大化”也就是Project Settings里的Game View Maximize on Play或者直接点Game视图右上角的三个横线勾选。这样鼠标在屏幕上的位置和Game视图坐标的对应关系最直观模拟效果最接近真机。编辑器里还有一个EditorGUIUtility.pixelsPerPoint的概念它是编辑器界面缩放比例。在Windows 150%缩放、macOS Retina屏上这个值大于1。我自己的模拟器直接使用Input.mousePosition实测在Windows下准确性很高没有做额外转换。如果你在编辑器窗口可视化触点时发现位置偏了优先检查是不是漏了pixelsPerPoint换算。4. 完整实现可以直接抄的TouchSimulator下面给出一套可以直接搬进项目的完整实现。代码分四块核心模拟器、运行时驱动、业务接入层、编辑器调试面板。4.1 TouchSimulator核心实现先创建一个TouchSimulator.cs放在Assets/Scripts/TouchSim/下。这个类不依赖任何编辑器API可以正常编译进分发版本只是在真机上没有实际作用。using System.Collections.Generic; using UnityEngine; namespace TouchSim { public static class TouchSimulator { public static bool Enabled true; public static bool ShowDebug true; public static Vector2 secondFingerInitialOffset new Vector2(-120f, 120f); private struct SimTouchData { public int fingerId; public Vector2 position; public Vector2 lastPosition; public TouchPhase phase; public float deltaTime; public int tapCount; } private static bool _finger1Active; private static SimTouchData _finger1; private static bool _finger2Active; private static SimTouchData _finger2; private static Vector2 _finger2LockedPos; private static readonly ListSimTouchData SimTouches new ListSimTouchData(2); public static int touchCount SimTouches.Count; public static Touch GetTouch(int index) { if (index 0 || index SimTouches.Count) return default; var data SimTouches[index]; var touch new Touch { fingerId data.fingerId, position data.position, rawPosition data.position, deltaTime data.deltaTime, tapCount data.tapCount, phase data.phase, pressure 1f, maximumPossiblePressure 1f, type TouchType.Direct, radius 100f, radiusVariance 0f, altitudeAngle 90f * Mathf.Deg2Rad, azimuthAngle 0f }; return touch; } public static void Update() { SimTouches.Clear(); if (!Enabled) { Reset(); return; } UpdateFinger1(); UpdateFinger2(); } public static void Reset() { if (_finger1Active) { _finger1.phase TouchPhase.Canceled; SimTouches.Add(_finger1); _finger1Active false; } if (_finger2Active) { _finger2.phase TouchPhase.Canceled; SimTouches.Add(_finger2); _finger2Active false; } SimTouches.Clear(); } private static void UpdateFinger1() { var mousePos Input.mousePosition; float dt Time.unscaledDeltaTime; if (Input.GetMouseButtonDown(0)) { _finger1Active true; _finger1.fingerId 1; _finger1.position mousePos; _finger1.lastPosition mousePos; _finger1.phase TouchPhase.Began; _finger1.deltaTime dt; _finger1.tapCount 1; SimTouches.Add(_finger1); } else if (_finger1Active Input.GetMouseButton(0)) { _finger1.deltaTime dt; if (Vector2.Distance(_finger1.position, mousePos) 0.01f) { _finger1.phase TouchPhase.Moved; } else { _finger1.phase TouchPhase.Stationary; } _finger1.lastPosition _finger1.position; _finger1.position mousePos; SimTouches.Add(_finger1); } else if (_finger1Active Input.GetMouseButtonUp(0)) { _finger1.phase TouchPhase.Ended; _finger1.position mousePos; _finger1.deltaTime dt; SimTouches.Add(_finger1); _finger1Active false; } } private static void UpdateFinger2() { bool secondActive Input.GetKey(KeyCode.LeftControl) Input.GetMouseButton(0); float dt Time.unscaledDeltaTime; if (secondActive !_finger2Active) { _finger2Active true; _finger2.fingerId 2; _finger2LockedPos Input.mousePosition secondFingerInitialOffset; _finger2.position _finger2LockedPos; _finger2.lastPosition _finger2LockedPos; _finger2.phase TouchPhase.Began; _finger2.deltaTime dt; _finger2.tapCount 1; SimTouches.Add(_finger2); } else if (secondActive _finger2Active) { _finger2.deltaTime dt; _finger2.lastPosition _finger2.position; _finger2.position _finger2LockedPos; _finger2.phase Vector2.Distance(_finger2.position, _finger2.lastPosition) 0.01f ? TouchPhase.Moved : TouchPhase.Stationary; SimTouches.Add(_finger2); } else if (!secondActive _finger2Active) { _finger2.phase TouchPhase.Ended; _finger2.position _finger2LockedPos; _finger2.deltaTime dt; SimTouches.Add(_finger2); _finger2Active false; } } } }这里解释几个关键决策SimTouches用ListSimTouchData而不是直接放Touch结构体主要是为了在内部维护lastPosition方便相位判断。对外暴露GetTouch时再转成Touch避免调用方修改内部状态。finger1Active、finger2Active两个状态位表示“当前这根手指是否处于按住状态”。千万不能用Input.GetMouseButton代替这个状态位因为鼠标在编辑器失焦时可能收不到抬起事件状态位能帮我们在重置时正确发送Canceled。第二指的锁定位置在激活瞬间固定这样你按住Ctrl并按下鼠标左键时画面里会出现第二个触点。之后移动鼠标第一指跟着鼠标跑第二指纹丝不动双指距离和角度就都有了。4.2 驱动脚本进入Play后自动驱动TouchSimulator是静态类需要有人每帧调它的Update()。我建议用一个专门的MonoBehaviour驱动并且只在编辑器下自动创建。using UnityEngine; namespace TouchSim { public class TouchSimulatorDriver : MonoBehaviour { private static TouchSimulatorDriver _instance; [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] private static void AutoCreate() { if (_instance null Application.isEditor) { var go new GameObject([TouchSimulator]); DontDestroyOnLoad(go); _instance go.AddComponentTouchSimulatorDriver(); } } private void Update() { if (Application.isFocused false) { TouchSimulator.Reset(); return; } TouchSimulator.Update(); } private void OnApplicationFocus(bool hasFocus) { if (!hasFocus) TouchSimulator.Reset(); } } }RuntimeInitializeOnLoadMethod让这个脚本在场景加载后自动创建不需要往场景里手动拖物体。Application.isFocused false时直接重置是为了避免切到别的窗口时鼠标按住状态残留。有一点需要注意这个Driver在真机上也会被创建但Application.isEditor为假根本不会走自动创建逻辑所以不影响发布版本。如果你的项目整体都在严格省资源也可以把整个类用#if UNITY_EDITOR包起来不过我个人觉得没必要。4.3 业务接入层TouchInput有了模拟器业务代码还不能直接用因为老的代码都在调Input.touchCount和Input.GetTouch。我们需要一个统一入口把“模拟”和“真实”的切换集中起来。using UnityEngine; namespace TouchSim { public static class TouchInput { public static bool useSimulation true; public static int touchCount { get { #if UNITY_EDITOR if (useSimulation TouchSimulator.Enabled TouchSimulator.touchCount 0) return TouchSimulator.touchCount; #endif return Input.touchCount; } } public static Touch GetTouch(int index) { #if UNITY_EDITOR if (useSimulation TouchSimulator.Enabled TouchSimulator.touchCount 0) return TouchSimulator.GetTouch(index); #endif return Input.GetTouch(index); } public static int GetTouchCountWithRealFallback() { int simCount TouchSimulator.touchCount; int realCount Input.touchCount; return simCount 0 ? simCount : realCount; } } }这段代码的逻辑是编辑器下且模拟器正在输出模拟触点时返回模拟数据否则返回真实Input数据。真机上因为#if UNITY_EDITOR直接走Input一行多余代码都不会编译进去。替换的时候把项目里的Input.touchCount替换成TouchInput.touchCount把Input.GetTouch(i)替换成TouchInput.GetTouch(i)如果你有foreach (var touch in Input.touches)这种写法我建议顺手改成for循环因为模拟器的触点列表是内部维护的每次GetTouch都会产生一个Touch结构体用foreach反而绕。改成int count TouchInput.touchCount; for (int i 0; i count; i) { Touch touch TouchInput.GetTouch(i); // 业务处理 }4.4 编辑器控制台与快捷操作模拟器运行时我建议加一个小的EditorWindow用于快速开关、看当前触点状态。using UnityEditor; using UnityEngine; namespace TouchSim.EditorTools { public class TouchSimulatorWindow : EditorWindow { [MenuItem(Tools/Touch Simulator)] public static void Open() { var window GetWindowTouchSimulatorWindow(Touch Simulator); window.minSize new Vector2(320, 240); } private void OnGUI() { TouchSimulator.Enabled EditorGUILayout.Toggle(Enable Simulation, TouchSimulator.Enabled); TouchInput.useSimulation EditorGUILayout.Toggle(Use Simulation in TouchInput, TouchInput.useSimulation); TouchSimulator.ShowDebug EditorGUILayout.Toggle(Show Debug in SceneView, TouchSimulator.ShowDebug); GUILayout.Space(8); EditorGUILayout.LabelField(Control, EditorStyles.boldLabel); EditorGUILayout.HelpBox(Finger 1: Mouse Left\nFinger 2: Ctrl Mouse Left (locked), MessageType.Info); if (GUILayout.Button(Reset Simulation)) { TouchSimulator.Reset(); } GUILayout.Space(8); EditorGUILayout.LabelField(Current Touches, EditorStyles.boldLabel); for (int i 0; i TouchSimulator.touchCount; i) { var touch TouchSimulator.GetTouch(i); EditorGUILayout.LabelField($id{touch.fingerId} pos{touch.position} phase{touch.phase}); } Repaint(); } } }这个窗口在Play模式运行时也会刷新你可以随时看到当前两个手指的坐标和相位。对于排查“触摸点卡在某个phase不切换”这种问题特别直观。4.5 在场景视图可视化触点光靠文本列表还是不够直观最好能在Game视图或者Scene视图上直接看到触点。这里我给SceneView加一个绘制红色、绿色、黄色分别代表不同相位方便判断当前触摸状态。using UnityEditor; using UnityEngine; namespace TouchSim.EditorTools { [InitializeOnLoad] public static class TouchSimulatorSceneViewDebug { static TouchSimulatorSceneViewDebug() { SceneView.duringSceneGui OnSceneGUI; EditorApplication.playModeStateChanged OnPlayModeStateChanged; } private static void OnPlayModeStateChanged(PlayModeStateChange state) { if (state PlayModeStateChange.ExitingPlayMode) TouchSimulator.Reset(); } private static void OnSceneGUI(SceneView sceneView) { if (!TouchSimulator.ShowDebug || TouchSimulator.touchCount 0) return; Handles.BeginGUI(); for (int i 0; i TouchSimulator.touchCount; i) { var touch TouchSimulator.GetTouch(i); Vector2 screenPos new Vector2( touch.position.x, Screen.height - touch.position.y); Rect viewRect sceneView.position; if (!viewRect.Contains(screenPos)) continue; Vector2 localPos new Vector2( screenPos.x - viewRect.x, screenPos.y - viewRect.y); Vector2 guiPos localPos / EditorGUIUtility.pixelsPerPoint; Color color touch.phase switch { TouchPhase.Began Color.yellow, TouchPhase.Moved Color.green, TouchPhase.Stationary Color.cyan, TouchPhase.Ended Color.red, _ Color.gray }; EditorGUI.DrawRect(new Rect(guiPos.x - 25f, guiPos.y - 25f, 50f, 50f), new Color(color.r, color.g, color.b, 0.3f)); GUI.Label(new Rect(guiPos.x 20f, guiPos.y - 20f, 200f, 40f), $Finger {touch.fingerId} {touch.phase}); } Handles.EndGUI(); sceneView.Repaint(); } } }注意这里touch.position是屏幕像素坐标Screen.height - touch.position.y转成左上角原点再与sceneView.position做差得到SceneView局部坐标。因为Unity编辑器的UI缩放最后除以pixelsPerPoint。这套转换在大多数Windows机器上可用macOS Retina下如果发现偏了多试试这个系数。5. 接入旧项目批量替换Input调用的实战记录理论说完代码也给了接下来就是你自己的动手环节。这一章我把接入旧项目时的具体操作步骤和踩坑经验记录下来。5.1 替换前先确认现状动手之前先打开项目搜索Input.touchCount和Input.GetTouch看看一共散落多少处。我接的这个项目里大概有60多处集中在手势识别、相机控制、UI拖拽几个模块里。如果只有三五处说明触摸代码还没有大量使用如果一两百处那建议先评估一下是否值得做一个这么大的改动。另一个要确认的是项目里是否已经有鼠标兼容分支。如果有接入模拟器后可以考虑逐步删掉这些分支让业务只走触摸逻辑。不过我的建议是不要一次删完先让模拟器跑起来各个手势验证没问题后再清理鼠标分支降低风险。5.2 替换步骤与常用正则替换顺序一定是从底层往上推先替换TouchInput这个封装类再替换Input.touchCount最后替换Input.GetTouch。我用的是VS Code加正则批量替换规则如下搜索\bInput\.touchCount\b替换为TouchInput.touchCount。搜索\bInput\.GetTouch\(替换为TouchInput.GetTouch(。搜索\bInput\.touches\b这类通常存在于foreach (var t in Input.touches)中需要手工处理。最后一步最容易踩坑。Input.touches返回的是数组可以直接foreach我们的TouchInput没有提供touches属性刻意不提供是为了避免每帧分配数组。遇到这种代码手工改成for循环是最稳妥的方案。替换完成后先编译一次把报错逐个修掉编译通过了再进Play模式验证。5.3 替换后容易踩的编译坑这种批量替换常见的报错有几种一个是命名空间冲突。如果你的项目里有别的地方定义了TouchInput类替换后可能产生歧义。我当时的做法是把TouchInput类放到一个独立的命名空间TouchSim里然后在需要使用的文件顶部加using TouchSim;。如果报冲突就在调用处写全限定名TouchSim.TouchInput.touchCount。另一个是性能隐患。TouchInput.GetTouch里每次都会走一遍模拟器判断再加上Touch结构体构造频率高了确实会比直接Input.GetTouch多一些开销。我在手机上跑过基准测试60多处的调用量下基本可以忽略。但如果你在Update里每帧反复调用几千次那最好缓存touchCount和指针索引别每帧重复查。替换完成后的第一件事是打开我们的EditorWindow点开“Enable Simulation”再按Play。你会发现Game视图里如果鼠标左键按住不动编辑器面板里就会看到第一个触点出现相位从Began切换到Stationary。这时候把鼠标挪一挪相位变成Moved手放开变成Ended。单指逻辑通了再按住Ctrl试双指。6. 常见问题与排查技巧实录这部分是我实际用这套模拟器调试了半个月之后沉淀下来的问题清单基本覆盖了所有人的高频翻车点。6.1 UGUI不响应模拟触摸第一个要坦白的坑模拟器生成的触摸点UGUI的EventSystem默认是完全不知道的。原因在于UGUI用的是StandaloneInputModule它在编辑器环境下读的是鼠标事件而不是触摸事件。就算TouchInput.touchCount返回了模拟数据UI的OnPointerDown、OnDrag等接口也接收不到。我的处理方式是能用鼠标点的地方就继续用鼠标游戏内容的双指手势走模拟器。比如按钮点击、列表滚动这些UI操作在编辑器下用鼠标直接操作不影响测试游戏逻辑。如果你的项目里存在“必须由触摸接口触发”的UI逻辑那就需要扩展StandaloneInputModule把TouchInput的触点注入进去这属于另一个课题这里先不展开。6.2 有触摸屏的电脑上模拟和真实触摸打架我自己有一台带触摸屏的Windows笔记本第一次用模拟器时发现奇怪现象明明没有按键编辑器面板上却出现了真实触摸点。这是因为Unity在Windows上如果检测到触摸设备会把设备事件作为真实触摸传给Input.touches编辑器并不做过滤。处理方式看你的需要。如果开发机自带触摸屏直接用真实触摸调试反而更接近真机如果不想被真实触摸干扰就在我们EditorWindow里关掉“Use Simulation in TouchInput”或者把TouchSimulator.Enabled关掉。用Input.touchSupported来判断是否支持触摸通常不可靠因为编辑器环境下很多配置都会返回true。最实用的判断方法就是看EditorWindow里当前有没有莫名出现的触摸点。6.3 切窗口、退出播放后触摸状态卡死这是个非常经典的坑。鼠标按着左键然后按住AltTab切到浏览器看个资料再切回来你会发现游戏里那个触点就像“粘”在屏幕上一样一直处于Moved或Stationary状态怎么松鼠标都清不掉。原因很简单鼠标抬起事件在窗口失焦期间丢掉了Input.GetMouseButtonUp没被触发于是_finger1Active一直停在true。我的解决方案已经在Driver里写了OnApplicationFocus(false)时调用TouchSimulator.Reset()同时Update里看到Application.isFocused false也重置一次。这样切回来之后触点状态是干净的不会残留。6.4 Stationary相位不触发导致的逻辑抖动模拟器刚写完的前两天我发现一个诡异现象鼠标按住不动游戏里物体的位置却在一帧帧小幅度抖动。排查半天最后发现是业务代码只处理Moved遇到Stationary时直接忽略于是上一次的触摸目标被当成已经移除重新初始化了一次。真机上同样有这个问题只是之前从没在编辑器里复现过。修法有两个层面业务代码层面把所有“没看到Moved就当作触摸结束”的逻辑改成“看到Stationary也认为手指还在”模拟器层面UpdateFinger1里已经把Stationary正常输出到SimTouches列表这就已经符合设备行为。归根结底还是业务代码对Stationary相位太不敏感了建议全局搜索一下TouchPhase.Moved的判断把Stationary纳入正常活跃状态。6.5 常见问题速查表现象原因解决办法编辑器里Input.touchCount一直为0旧式Input没有内置模拟用TouchInput替换裸调Input启用模拟器鼠标松开后触点不消失编辑器失焦导致抬起事件丢失调用TouchSimulator.Reset()监听OnApplicationFocus(false)UGUI按钮点不到模拟触点不进入UGUI事件系统编辑器调试时继续用鼠标点UI或扩展StandaloneInputModule双指捏合/旋转无效第二指没激活按住Ctrl再按鼠标左键第二指会出现在左上方触点坐标明显不对鼠标跑到Game视图外打开Game视图的 Maximize on Play保持鼠标在Game视图内带触摸屏设备上出现莫名触点真实触摸被系统传入编辑器关掉模拟器开关直接用真实触摸调试触点大小、角度固定模拟器没有物理接触面积业务代码如需radius/pressure数据请用真机验证最后一点经验这套模拟器用了一段时间之后我又给它加了个不那么起眼但是很顺手的功能在SceneView里把第二指画成一个半透明的圈第一指画成实心圈这样盯着Game视图就能猜到当前手势是什么形态不用去读代码。模拟器本身其实没什么高深技术真正有价值的是“把鼠标翻译成一套严格的触摸生命周期”这个思路。我个人最深的体会是模拟器只是把触摸逻辑从“不可调试”变成“可调试”它永远代替不了真机。真机上的触摸响应延迟、多点触控坐标抖动、压力感应这些物理特性在编辑器里模拟不出来。所以我的团队现在的工作流是功能开发期用这套模拟器在编辑器里把逻辑跑通提测前一定拿真机过一遍触摸联调。这样一来真机上的回退次数少了很多编辑器里能做的验证也基本能覆盖到核心路径。如果你也是被“编辑器测不了双指”折磨过的人希望这套方案能帮你把调试效率提上去。
返回列表