尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Unity Input System实现移动端手势交互:单指旋转与双指缩放

Unity Input System实现移动端手势交互:单指旋转与双指缩放 1. 项目概述为什么是时候告别 Input.GetTouch 了如果你是一个Unity移动端开发者还在用Input.touchCount和Input.GetTouch(i)来吭哧吭哧地处理手势那感觉就像是在智能手机时代还在用九宫格按键发短信。它能用但效率低、代码臃肿、维护起来头疼而且很难优雅地处理复杂的手势组合。今天要聊的就是如何用Unity全新的Input System这套“现代化武器”彻底重构移动端的核心手势交互单指旋转和双指缩放。Input System 不是简单的API替换而是一套从底层重构的输入处理架构。它最大的价值在于将“原始输入数据”如屏幕坐标和“高层交互逻辑”如“旋转”、“缩放”解耦。对于移动端手势这意味着你不再需要手动计算两帧之间的手指位移差来判断是拖拽还是滑动也不再需要自己写逻辑去区分单指和双指状态然后小心翼翼地处理手指抬起、按下的事件序列。Input System 通过可配置的Input Action Asset让你能用可视化的方式定义“一个旋转手势应该由单指在屏幕上移动来触发”然后直接在代码里响应一个干净的OnRotate回调事件里面直接包含了旋转的角度增量。这带来的直接好处是代码可读性、可维护性和可扩展性的巨大提升也是项目迈向更专业架构的必经之路。这篇文章我会从一个实际开发者的角度带你从零开始用 Input System 实现一套稳健、高性能且易于扩展的移动端手势交互系统。无论你是正在为新项目做技术选型还是打算重构老项目的输入模块这篇内容都能给你提供可直接“抄作业”的完整方案和避坑指南。2. Input System 核心概念与项目配置在动手写代码之前我们必须先理解 Input System 的几个核心“积木”。这能让你明白我们不是在死记硬背步骤而是在搭建一个合理的工程结构。2.1 核心概念Actions、Bindings 与 Processors你可以把Input Action Asset想象成一个“输入动作清单”配置文件。在这个清单里你定义各种“动作”Actions比如“移动”、“跳跃”、“开火”以及我们今天要做的“旋转”和“缩放”。每个“动作”需要关联到一个或多个绑定Bindings。绑定指明了这个动作由什么具体的物理输入触发。对于移动端绑定的就是Touchscreen设备上的各种路径比如Touchscreen/position触摸位置、Touchscreen/delta触摸位移、Touchscreen/primaryTouch主触摸点等。处理器Processors是绑定的“后厨”负责对原始输入数据进行加工。比如触摸屏的delta位移是像素值直接用来旋转物体可能太快或太慢。我们可以给它加一个Scale Vector2 Processor乘上一个系数如0.01把像素位移转换成更合适的旋转速度。这个配置过程完全是可视化的无需硬编码。2.2 项目初始配置与包管理首先你需要确保项目已经安装了 Input System 包。打开 Unity进入Window Package Manager。在 Package Manager 窗口左上角确保选择的是Unity Registry。在列表中找到Input System包点击安装。安装过程中Unity会提示你禁用旧的 Input Manager选择“Yes”或“Disable and Replace”。这是关键一步意味着项目将完全转向新的输入系统。安装完成后我们开始创建核心的配置文件。在 Project 窗口右键选择Create Input Actions我将其命名为MobileTouchControls。双击这个文件会打开Input Actions Editor窗口这就是我们配置所有手势的“作战指挥中心”。3. 手势交互的详细设计与动作映射我们的目标是定义两个核心动作Rotate单指旋转和Zoom双指缩放。它们的触发逻辑完全不同需要在 Input Action Asset 中进行精心设计。3.1 创建与配置 Rotate单指旋转动作在 Input Actions Editor 中点击号创建一个新的 Action Map可以命名为TouchGestures。Action Map 是一组相关动作的集合方便我们整体启用或禁用。在TouchGestures这个 Map 下点击创建第一个 Action命名为Rotate。它的Action Type需要设置为Value。这是因为旋转是一个连续的过程我们需要在每一帧只要手指在移动都获取到一个表示旋转方向和速度的二维向量值Vector2。接下来为Rotate动作添加绑定Binding点击Rotate动作下号旁边的下拉箭头选择Add Binding。在右侧的Path中点击搜索图标找到Touchscreen然后选择Touchscreen/delta。delta代表的是触摸点从上一帧到当前帧的位移向量以像素为单位。这个绑定默认是监听“主触摸点”即第一个按下的手指。这正是我们想要的单指旋转逻辑用第一根手指的移动来驱动旋转。但是直接使用像素位移delta数值太大旋转会非常快。我们需要添加处理器。在绑定列表中找到刚创建的Touchscreen/delta绑定在它下方点击号选择Add Processor然后选择Scale Vector2 Processor。在 Inspector 中将 X 和 Y 的 Scale Factor 都设置为一个较小的值比如0.2或0.3。这个值需要根据你的物体大小和摄像机距离来微调它的作用是将像素位移“缩放”到一个合理的灵敏度。注意这里有一个关键点。Touchscreen/delta在手指按住不动时其值就是(0,0)。我们的Rotate动作是Value类型这意味着只要绑定有输入即使为0它就会持续触发事件。这非常适合用来在Update循环中平滑地应用旋转。3.2 创建与配置 Zoom双指缩放动作缩放动作的逻辑更复杂一些因为它需要计算两根手指之间的距离变化。Input System 没有直接提供“双指捏合”的原生绑定但我们可以通过一个巧妙的组合来实现。在TouchGesturesAction Map 下再创建一个新的 Action命名为Zoom。它的Action Type同样设置为Value因为缩放也是一个连续过程我们需要一个表示缩放比例变化float的值。为Zoom动作添加绑定。这次我们需要手动输入路径。点击添加绑定在 Path 输入框中直接键入Touchscreen/position。这还没完。我们需要告诉 Input System这个绑定需要读取两根手指的信息。在绑定列表中找到这个Touchscreen/position绑定在右侧 Inspector 中找到Interactions部分。虽然这里通常用于点击、长按等但我们需要关注另一个属性Processors和Bindings旁边的设置。 实际上更标准的做法是使用“Vector2 Composite”。但针对双指缩放的特定算法我推荐一种更直接、更易理解的自定义处理方式我们创建一个Value动作但通过 C# 脚本从 Input System 直接读取两个触摸点的位置然后自己计算距离差。不过为了保持 Action Asset 的整洁和逻辑统一我们可以换一种思路创建两个Value类型的动作PinchPositionA和PinchPositionB。将PinchPositionA绑定到Touchscreen/touch0/position第一根手指。将PinchPositionB绑定到Touchscreen/touch1/position第二根手指。然后在脚本中同时监听这两个动作实时计算它们当前位置的距离并与上一帧的距离比较其差值或比值就构成了我们自定义的Zoom数值。虽然第二种方法更底层但第一种方法在脚本中计算对于理解手势本质更有帮助也更容易调试。在本方案中我将采用在脚本中实时计算双指距离的方式这样我们可以更清晰地控制缩放的速度、平滑度和死区。因此在MobileTouchControls这个 Asset 中我们只需要确保能获取到多个触摸点的位置信息即可。我们可以创建一个名为MultiTouchPosition的Pass-Through类型的 Action它不做任何处理只是将所有触摸数据传递出来供脚本解析。4. 核心交互脚本的完整实现与解析配置文件准备好后就到了编写驱动逻辑的 C# 脚本环节。我们将创建一个名为TouchGestureController的 MonoBehaviour 脚本。4.1 脚本初始化与 Input Action 引用首先我们需要引用刚才创建的 Input Action Asset并声明各个动作的引用。using UnityEngine; using UnityEngine.InputSystem; public class TouchGestureController : MonoBehaviour { // 引用我们创建的 Input Action Asset [SerializeField] private InputActionAsset touchControlsAsset; // 定义各个 Input Action private InputAction rotateAction; private InputAction pinchPositionAAction; private InputAction pinchPositionBAction; // 旋转控制的参数 [Header(“旋转设置”)] [SerializeField] private float rotationSpeed 0.5f; [SerializeField] private Transform targetTransform; // 要旋转的物体 // 缩放控制的参数 [Header(“缩放设置”)] [SerializeField] private float zoomSpeed 0.01f; [SerializeField] private float minZoomDistance 2.0f; [SerializeField] private float maxZoomDistance 20.0f; private Camera mainCamera; // 假设通过摄像机推拉实现缩放 private float initialPinchDistance 0f; private bool isPinching false; private void Awake() { // 从 Asset 中找到对应的 Action Map 和 Actions var touchMap touchControlsAsset.FindActionMap(“TouchGestures”); rotateAction touchMap.FindAction(“Rotate”); // 假设我们采用了第二种方案定义了获取手指位置的动作 pinchPositionAAction touchMap.FindAction(“PinchPositionA”); pinchPositionBAction touchMap.FindAction(“PinchPositionB”); mainCamera Camera.main; if (targetTransform null) { targetTransform transform; // 默认旋转自身 } } private void OnEnable() { // 启用 Action Map 和所有 Actions touchControlsAsset.FindActionMap(“TouchGestures”).Enable(); rotateAction.Enable(); pinchPositionAAction.Enable(); pinchPositionBAction.Enable(); } private void OnDisable() { // 禁用 Action Map 和所有 Actions rotateAction.Disable(); pinchPositionAAction.Disable(); pinchPositionBAction.Disable(); touchControlsAsset.FindActionMap(“TouchGestures”).Disable(); } }4.2 单指旋转逻辑的实现旋转的逻辑相对直接。我们在Update循环中读取rotateAction的值它是一个经过我们处理器缩放后的Vector2。private void Update() { HandleRotation(); HandleZoom(); } private void HandleRotation() { // 读取旋转动作的输入值经过Scale Processor处理后的delta Vector2 rotationInput rotateAction.ReadValueVector2(); // 如果输入不为零即手指在移动则应用旋转 if (rotationInput ! Vector2.zero) { // 典型的3D物体旋转绕世界Y轴水平旋转绕自身X轴垂直旋转 float horizontalRot -rotationInput.x * rotationSpeed; // 注意方向根据需求调整正负 float verticalRot -rotationInput.y * rotationSpeed; targetTransform.Rotate(Vector3.up, horizontalRot, Space.World); targetTransform.Rotate(Vector3.right, verticalRot, Space.Self); } }实操心得这里rotationInput.x和rotationInput.y的正负方向可能与你的直觉不符。通常手指向右滑动我们希望物体向左转像转动一个转盘。所以这里用了负号-。这个方向感需要根据你的具体场景是观察者旋转还是物体自转来调整务必在真机上测试手感。4.3 双指缩放逻辑的完整实现缩放逻辑是我们实现的重点和难点。我们需要实时追踪两根手指计算它们每帧之间的距离变化。private void HandleZoom() { // 读取两个手指的当前位置 Vector2 touchAPos pinchPositionAAction.ReadValueVector2(); Vector2 touchBPos pinchPositionBAction.ReadValueVector2(); // 检查是否至少有两根手指按下位置不为零 bool hasTwoTouches touchAPos ! Vector2.zero touchBPos ! Vector2.zero; if (hasTwoTouches) { float currentDistance Vector2.Distance(touchAPos, touchBPos); if (!isPinching) { // 刚开始捏合记录初始距离不进行缩放 initialPinchDistance currentDistance; isPinching true; } else { // 计算当前距离与初始距离的差值 float deltaDistance currentDistance - initialPinchDistance; // 根据距离差计算缩放量 float zoomAmount deltaDistance * zoomSpeed; // 应用缩放这里以摄像机前后移动为例 ApplyCameraZoom(zoomAmount); // 更新初始距离为当前距离为下一帧计算做准备 // 这种实现是“增量式”的每一帧都基于上一帧的状态变化操作更跟手。 initialPinchDistance currentDistance; } } else { // 手指少于两根重置捏合状态 isPinching false; initialPinchDistance 0f; } } private void ApplyCameraZoom(float zoomDelta) { if (mainCamera null) return; // 假设是透视摄像机通过改变位置来实现缩放 // 计算摄像机向前朝向目标移动的方向 Vector3 zoomDirection mainCamera.transform.forward; // 计算新的位置 Vector3 newPosition mainCamera.transform.position zoomDirection * zoomDelta; // 计算新位置到目标点的距离假设目标点在原点或某个固定点 float distanceToTarget Vector3.Distance(newPosition, Vector3.zero); // 这里目标点简化了 // 钳制距离在最小和最大值之间 distanceToTarget Mathf.Clamp(distanceToTarget, minZoomDistance, maxZoomDistance); // 重新计算摄像机位置保持看向目标 // 这里需要一个更稳健的算法例如让摄像机沿其与目标的连线移动 // 简化版直接设置位置效果可能不完美适用于轨道摄像机 // 更好的做法是使用 Camera.transform.Translate 或修改 Field of View mainCamera.transform.position mainCamera.transform.position zoomDirection * zoomDelta; // 更推荐的做法对于观察一个焦点的摄像机使用以下逻辑 // Vector3 toCamera mainCamera.transform.position - targetTransform.position; // float desiredDistance toCamera.magnitude - zoomDelta; // desiredDistance Mathf.Clamp(desiredDistance, minZoomDistance, maxZoomDistance); // mainCamera.transform.position targetTransform.position toCamera.normalized * desiredDistance; }核心细节解析缩放算法这里提供了两种思路。一种是简单的每帧增量移动操作直观但可能不够平滑且容易超出边界。另一种是基于“摄像机与目标点距离”的计算这是更常见的3D观察器缩放模式能严格将距离限制在[minZoomDistance, maxZoomDistance]范围内体验更佳。具体选择取决于你的项目是2D UI缩放、3D模型查看器还是场景漫游。5. 高级优化、问题排查与平台适配实现基础功能只是第一步要让手势交互真正“跟手”、“顺滑”且“稳定”还需要一系列优化和问题处理。5.1 手势冲突的解决与输入优先级管理在一个复杂的UI界面中你可能同时存在需要滚动的列表、可拖拽的卡片和我们的3D模型观察器。这就产生了手势冲突一个在列表上的双指捏合到底应该滚动列表还是缩放模型Input System 提供了强大的解决机制输入动作的回调上下文Callback Context和交互Interactions。但对于手势冲突更实用的策略是在逻辑层进行管理。基于UI射线检测的屏蔽在HandleRotation和HandleZoom开始时使用EventSystem.current.IsPointerOverGameObject()或GraphicRaycaster检测当前触摸点是否在需要优先响应的UI元素如ScrollRect上。如果是则直接return不执行模型的手势操作。private bool IsTouchingUI() { // 这是一个简化示例实际中可能需要处理多个触摸点 PointerEventData eventData new PointerEventData(EventSystem.current); eventData.position Mouse.current.position.ReadValue(); // 对于触摸需要用 Touchscreen 的位置 ListRaycastResult results new ListRaycastResult(); EventSystem.current.RaycastAll(eventData, results); return results.Count 0; } private void HandleRotation() { if (IsTouchingUI()) return; // 如果触摸点在UI上不处理旋转 // ... 原有的旋转逻辑 }动作禁用/启用你可以准备多个 Input Action Asset 或 Action Map在不同的游戏状态如在菜单中、在游戏中启用不同的映射。当打开一个全屏UI时直接禁用TouchGestures这个 Action Map。5.2 性能优化与平滑处理移动设备性能敏感输入处理必须高效。避免在 Update 中频繁查找 Action我们的代码在Awake中已经缓存了InputAction引用这是最佳实践。不要在Update里通过字符串名称去FindAction。缩放平滑阻尼直接使用每帧的距离差进行缩放在低帧率或快速操作时可能会产生跳跃感。可以使用Mathf.SmoothDamp对最终的zoomDelta或摄像机距离进行平滑插值。private float currentZoomVelocity; // 平滑阻尼的当前速度 private float smoothedZoomDelta; private void ApplyCameraZoom(float rawZoomDelta) { // 使用 SmoothDamp 平滑缩放输入 smoothedZoomDelta Mathf.SmoothDamp(smoothedZoomDelta, rawZoomDelta, ref currentZoomVelocity, 0.05f); // 使用 smoothedZoomDelta 进行实际的缩放计算... }减少不必要的计算在HandleZoom中当!hasTwoTouches时我们及时重置了状态避免了无效的状态维持和计算。5.3 跨平台与真机调试要点在编辑器中我们可以用鼠标模拟触摸Input System 默认支持但真机表现才是关键。编辑器模拟在 Play 模式下你可以用鼠标左键拖拽模拟单指触摸按住Ctrl(Windows) 或Cmd(Mac) 键的同时拖拽模拟第二指。这非常便于快速调试。真机测试必做项帧率影响在真机上特别是中低端设备帧率可能波动。测试你的旋转和缩放逻辑在帧率下降时是否依然平滑会不会出现卡顿或跳变。多点触控测试三指及以上触摸时你的逻辑是否健壮我们的逻辑只处理前两指是合理的。确保不会因为第三指的落下或抬起导致状态错乱。手势边界快速、大幅度的滑动手势以及非常缓慢的微操作都要测试。检查旋转速度是否线性缩放是否有迟滞或过冲。与系统手势的冲突在某些手机特别是iOS和某些Android定制系统上从屏幕边缘滑动会触发返回或全局手势。你需要考虑是否要在你的应用内禁用这些手势或者确保你的手势操作区域不会轻易触发它们。6. 常见问题排查与实战技巧实录即使按照步骤操作你也可能会遇到一些“坑”。这里记录了几个我实战中遇到过的问题和解决方法。问题1旋转/缩放没有反应。检查清单Input Action Asset 是否赋值在 Inspector 中确认TouchGestureController脚本上的touchControlsAsset字段已经拖入了我们创建的MobileTouchControls文件。Action Map 和 Actions 是否启用确保OnEnable方法被正确调用脚本所在的 GameObject 需要处于激活状态。可以在OnEnable里加一句Debug.Log(“Touch controls enabled.”)来验证。绑定路径是否正确双击MobileTouchControls文件在 Input Actions Editor 中确认Rotate动作绑定到了Touchscreen/delta并且处理器参数设置得当。目标 Transform 或 Camera 是否赋值检查targetTransform和脚本获取的mainCamera是否有效。问题2旋转方向反了或者缩放是反向的。解决方案这是最常见的问题。调整HandleRotation中horizontalRot和verticalRot计算时的正负号。例如把-rotationInput.x改成rotationInput.x。对于缩放检查ApplyCameraZoom中zoomDelta的符号。通常手指分开距离变大应导致摄像机拉近Zoom In这时zoomDelta应为负值你可能需要zoomDirection * -zoomDelta或调整zoomSpeed的正负。问题3双指缩放时如果先放下一根手指再按下状态会错乱。问题分析我们的逻辑基于touch0和touch1。如果用户先放下touch0第一根手指系统会将还在屏幕上的那根手指重新分配为touch0。而我们代码里pinchPositionAAction和pinchPositionBAction仍然固定绑定到touch0和touch1这就对不上了。优化方案不要固定绑定到touch0和touch1。改为在HandleZoom中通过Touchscreen.current.touches这个控件数组动态获取屏幕上所有有效的触摸点然后选择其中两个例如最先按下的两个或最近移动的两个来计算距离。这需要更复杂的触摸点追踪管理但能提供更稳健的多点触控体验。问题4在UI按钮上操作仍然触发了模型旋转。解决方案这就是前面提到的冲突管理。确保你的IsTouchingUI()方法能正确工作。对于触摸输入获取位置需要使用Touchscreen.current.primaryTouch.position.ReadValue()或其他活跃触摸点的位置而不是Mouse.current.position。同时确保你的UI元素如Button、ScrollRect拥有GraphicRaycaster组件并且Blocking Objects设置正确。一个实用的调试技巧在开发过程中创建一个简单的调试UI实时显示rotateAction.ReadValueVector2()的数值以及两根手指的位置和计算出的距离。这能让你直观地看到输入系统是否按预期工作以及你的算法是否正确处理了这些数据。可以在OnGUI或使用 UGUI Text 组件来显示这些信息。从古老的Input.GetTouch迁移到现代的 Input System初期确实需要一点学习成本来理解其基于“动作”的哲学。但一旦掌握你会发现它带来的代码清晰度、可维护性和跨平台一致性是巨大的。本文实现的单指旋转和双指缩放是一个坚实的起点。基于这个框架你可以轻松地扩展出更多手势如长按、双击、双指旋转扭转等只需要在 Input Action Asset 中定义新的动作和绑定并在脚本中添加相应的处理逻辑即可。记住好的输入系统应该让玩家感觉不到它的存在却又无比跟手。
返回列表