☰
UE4/UE5触摸屏手势交互实战:单指拖拽与双指缩放旋转方案
2026/10/7 10:43:27 网站建设 项目流程

简介:这份资源面向使用UE4/UE5开发触屏交互的开发者,聚焦触摸屏上的移动、旋转、缩放三类手势事件处理。内容围绕触摸开始、移动、结束等事件类型展开,讲解如何通过UUserWidget重写触摸回调、注册触摸处理函数,并在蓝图中借助TouchInputComponent管理多点触摸,利用双指距离与角度变化实现平移、旋转与缩放,同时涉及防抖动与输入刷新率等优化思路。资源包共349个文件,以288个uasset资产、6个umap关卡、34个ini配置为主,另含少量png、bin、json、log及工程文件,压缩包约610.21MB,可直接导入引擎查看与复用。目前已有6432人学习下载,适合希望快速掌握触屏手势交互、对照工程结构查漏补缺的中级开发者参考。

1. 触摸屏上的 UE4/UE5 手势交互:从单指拖拽到双指缩放,一套能落地的输入方案

在展厅大屏、车载中控、工业 HMI 这类触摸屏项目里,用 UE4 或 UE5 做交互时,最常被问到的一件事就是:怎么让模型跟着手指移动、旋转、缩放?很多人第一反应是去 Enhanced Input 里找现成的触摸事件,结果发现单指能拖、双指就乱,旋转和缩放互相打架。这个标题讲的正是这套东西——在触摸屏上把移动、旋转、缩放三个手势拆开、识别、映射到 Actor 变换上。它适合做数字孪生、展馆互动、触控一体机的开发者,尤其是那些已经会用蓝图但被多点触控坑过的人。下面我按自己踩过的顺序,把原理、蓝图节点、参数和翻车点讲清楚。

2. 触摸输入在 UE 里到底是怎么进来的:从 Finger Index 到手势判定

2.1 触摸事件和鼠标事件不是一回事

UE 的输入系统里,触摸(Touch)和鼠标(Mouse)走的是两套分发逻辑。鼠标只有一个指针,触摸可以有十个 Finger Index。你在 Project Settings → Input 里看到的Touch 1、Touch 2这些 Action Mapping,本质上是把某个手指的按下/抬起绑定成事件,但它不告诉你手指在屏幕上的坐标变化趋势。真正做手势,得用InputTouch系列事件或者Get Input Touch State节点去轮询。

常见做法是:在 PlayerController 或 Pawn 的蓝图里,用InputTouch事件拿到FingerIndex和Location,然后自己维护一个手指状态数组。为什么不用现成的Touch 1映射?因为映射只能告诉你“第一根手指按下了”,但双指缩放需要同时知道两根手指的实时坐标,映射做不到。

提示:UE5 的 Enhanced Input 对触摸的支持在 5.1 之后才比较完整,但很多触摸屏项目仍然跑在 UE4.27 上,所以下面给的方案是两版通用的。

2.2 手指索引和屏幕坐标的获取

先看一段蓝图逻辑对应的伪代码,方便理解数据流。实际在蓝图里是用节点连的,但写成代码更好说清参数:

// 在 PlayerController 的 Tick 或 InputTouch 事件中 TArray<FTouchState> ActiveTouches; void OnTouchStart(int32 FingerIndex, FVector2D ScreenPos) { FTouchState State; State.Index = FingerIndex; State.StartPos = ScreenPos; State.LastPos = ScreenPos; State.Delta = FVector2D::ZeroVector; ActiveTouches.Add(State); } void OnTouchMove(int32 FingerIndex, FVector2D ScreenPos) { for (auto& T : ActiveTouches) { if (T.Index == FingerIndex) { T.Delta = ScreenPos - T.LastPos; // 本帧位移 T.LastPos = ScreenPos; break; } } }

这段逻辑的关键参数是Delta,它代表这一帧手指移动了多少像素。移动、旋转、缩放全都基于这个 Delta 和两根手指之间的距离变化来算。StartPos用来判断是点击还是拖拽,一般位移超过 10 像素才认为是拖拽,否则算点击。

2.3 单指、双指、三指的分工

在触摸屏交互里,我一般这样分配:

手指数手势映射操作
单指拖拽平移 Actor
双指同向拖拽平移(更稳)
双指捏合/张开缩放改 Scale
双指旋转旋转改 Yaw
三指预留重置视角

这里有个坑:双指同时做缩放和旋转时,两个计算会互相干扰。常见做法是给缩放和旋转各设一个阈值,比如距离变化超过 5% 才触发缩放,角度变化超过 2 度才触发旋转,避免手指轻微抖动就乱转。

3. 把屏幕位移映射到世界坐标:移动和旋转的换算逻辑

3.1 屏幕 Delta 转世界位移

屏幕坐标是二维的,世界坐标是三维的。单指拖拽时,你希望物体在平行于屏幕的平面上移动。最直接的做法是用Deproject Screen to World节点,把手指的屏幕坐标转成世界空间的一条射线,然后和物体所在平面求交。

// 蓝图里对应:Deproject Screen to World -> Line Plane Intersection FVector WorldOrigin, WorldDirection; UGameplayStatics::DeprojectScreenToWorld(PlayerController, ScreenPos, WorldOrigin, WorldDirection); // 假设物体在 Z=ObjectZ 的平面上移动 float t = (ObjectZ - WorldOrigin.Z) / WorldDirection.Z; FVector WorldPoint = WorldOrigin + WorldDirection * t;

参数说明:ObjectZ是物体当前的世界 Z 坐标。如果物体不在水平面上移动,比如在墙面上,那就得换成用物体法线做平面。t是射线参数,如果WorldDirection.Z接近 0,说明射线平行于平面,这时候要跳过这一帧,否则会算出无穷大。

移动量就是WorldPoint - LastWorldPoint,然后把这个增量加到 Actor 的 Location 上。注意不要直接 Set Location,而是用AddActorWorldOffset,这样不会破坏物体的旋转。

3.2 双指旋转的角度计算

双指旋转的核心是算两根手指连线在屏幕上的角度变化。用Atan2求角度:

FVector2D Dir = TouchB.LastPos - TouchA.LastPos; float CurrentAngle = FMath::Atan2(Dir.Y, Dir.X); // 弧度 float DeltaAngle = CurrentAngle - LastAngle; // 转成角度 float DeltaDegrees = FMath::RadiansToDegrees(DeltaAngle);

这里有个玄学问题:Atan2的返回值在 -180 到 180 之间,当角度跨过 ±180 边界时会跳变。血泪经验是,如果DeltaAngle的绝对值大于 180 度,要手动修正,比如减去 360 或加上 360。否则物体会突然转一大圈。

修正后,把DeltaDegrees加到 Actor 的 Yaw 上。如果物体需要绕其他轴转,就改对应的 Rotator 分量。

3.3 双指缩放的比例因子

缩放最简单,用两根手指当前距离除以上一帧距离:

float CurrentDist = FVector2D::Distance(TouchA.LastPos, TouchB.LastPos); float LastDist = FVector2D::Distance(TouchA.PrevPos, TouchB.PrevPos); float ScaleFactor = CurrentDist / LastDist;

然后NewScale = OldScale * ScaleFactor。但要注意,缩放应该以两根手指的中点为锚点,而不是物体原点。如果直接改 Scale,物体会以自身原点缩放,手指中点如果不在物体中心,看起来就会“跑偏”。解决办法是同时调整 Location,让中点对应的世界点保持不动。这个计算稍微复杂,但效果差别很大。

注意:缩放因子要 Clamp,比如限制在 0.5 到 2.0 之间,防止用户一下子把物体缩到看不见或者撑爆场景。

4. 蓝图落地:一套可复用的触摸手势组件

4.1 创建 ActorComponent 管理触摸状态

不要把所有逻辑塞进 Level Blueprint 或 Pawn 里。我一般建一个TouchGestureComponent,挂在需要交互的 Actor 上。组件里维护ActiveTouches数组,在TickComponent里处理手势判定。

// TouchGestureComponent.h 关键成员 UPROPERTY() TArray<FTouchState> ActiveTouches; UPROPERTY(EditAnywhere, Category="Gesture") float DragThreshold = 10.0f; // 像素 UPROPERTY(EditAnywhere, Category="Gesture") float RotateThreshold = 2.0f; // 度 UPROPERTY(EditAnywhere, Category="Gesture") float ScaleThreshold = 0.05f; // 比例

这些阈值暴露到 Details 面板,不同项目可以调。展厅大屏手指动作大,阈值可以设高一点;手机屏幕小,阈值要低。

4.2 手势判定的状态机

在TickComponent里,根据ActiveTouches.Num()分支:

  • 0 个手指:重置所有 Last 状态
  • 1 个手指:进入拖拽模式,算世界位移
  • 2 个手指:算距离和角度,分别和阈值比较,决定是否缩放/旋转
void UTouchGestureComponent::TickComponent(float DeltaTime, ...) { if (ActiveTouches.Num() == 1) { HandleSingleDrag(); } else if (ActiveTouches.Num() == 2) { HandlePinchAndRotate(); } }

HandleSingleDrag里用 Deproject 算世界点,HandlePinchAndRotate里算距离和角度。每个函数只做一件事,方便单独调试。

4.3 把变换应用到 Actor

算出来的位移、角度、缩放,最后统一应用到 Owner Actor:

Owner->AddActorWorldOffset(WorldDelta); Owner->AddActorWorldRotation(FRotator(0, DeltaDegrees, 0)); Owner->SetActorScale3D(Owner->GetActorScale3D() * ScaleFactor);

注意AddActorWorldRotation接受的是 Rotator,不是角度值。如果只绕 Yaw 转,就构造FRotator(0, DeltaDegrees, 0)。缩放用SetActorScale3D直接设,但记得乘上当前 Scale,否则会重置。

提示:如果 Actor 有物理模拟,直接改 Transform 会和物理打架。触摸交互的物体一般关掉 Simulate Physics,或者用 Kinematic 模式。

5. 避坑与排查:触摸屏手势最常见的 5 个翻车现场

5.1 双指缩放时物体跳动

现象:两根手指捏合,物体不是平滑缩放,而是一跳一跳的。原因:LastDist没有每帧更新,或者更新时机不对。如果只在 TouchMove 事件里更新,而 TouchMove 的触发频率和 Tick 不一致,就会累积误差。解决:在 Tick 里统一更新LastDist和LastAngle,不要依赖事件触发。每帧算完手势后,把当前值存为 Last。

5.2 旋转方向反了

现象:手指顺时针转,物体逆时针转。原因:屏幕坐标的 Y 轴向下,世界坐标的 Y 轴可能向前,Atan2的符号和旋转方向不匹配。解决:在DeltaDegrees前面加负号,或者交换Atan2的参数顺序。这个没有统一答案,和项目坐标系有关,试一次就知道了。

5.3 单指拖拽时物体飞出屏幕

现象:手指快速滑动,物体瞬间消失。原因:Deproject算出的世界点如果射线和平面夹角很小,t会非常大,导致位移巨大。解决:给WorldDelta加一个最大长度限制,比如WorldDelta.GetClampedToMaxSize(100.0f)。另外检查WorldDirection.Z的绝对值,小于 0.1 就跳过这一帧。

5.4 触摸事件在 UI 上被拦截

现象:手指点在按钮上,物体也跟着动。原因:UE 的 UI 会消费触摸事件,但如果你在 PlayerController 里直接读Get Input Touch State,它不管 UI 是否消费。解决:用InputTouch事件而不是轮询,或者在轮询前判断IsOverGameObject。更稳妥的做法是把触摸交互做成 UI 的一部分,用WidgetInteraction组件。

5.5 多指触摸时 Finger Index 复用

现象:三根手指同时按,抬起一根后再按,索引乱了。原因:UE 的 Finger Index 是复用的,抬起后可能分配给新手指。解决:不要用 Finger Index 作为唯一标识,用StartPos或者自己生成一个 ID。在 TouchStart 时分配 ID,TouchEnd 时移除。

6. 进阶:用触摸手势驱动 3D UI 和相机轨道

6.1 触摸手势控制相机环绕

上面讲的是移动物体,但很多场景是物体不动、相机动。把同样的手势逻辑映射到 SpringArm 上:单指拖拽改 SpringArm 的 Yaw/Pitch,双指缩放改 SpringArm 的 TargetArmLength。注意 Pitch 要 Clamp 在 -80 到 80 度之间,防止翻转。

// 相机环绕 SpringArm->AddRelativeRotation(FRotator(0, DeltaDegrees, 0)); SpringArm->TargetArmLength = FMath::Clamp( SpringArm->TargetArmLength * ScaleFactor, 100.0f, 1000.0f);

6.2 手势和 3D UI 的交互

UE5 的 3D UI(Widget Component)在触摸屏上有个模糊问题,尤其是缩放后文字发虚。这是因为 Widget Component 的渲染分辨率没跟上。解决办法是在缩放时动态调整WidgetComponent->SetDrawSize,或者用Retainer Box提高渲染质量。这个坑在展厅项目里很常见,调一次能省半天返工。

6.3 验证手势是否跟手

最后说一个我自己的习惯:在屏幕上画调试线。用Draw Debug Line把两根手指的连线画出来,再把计算出的旋转中心点画个球。这样一眼就能看出手势识别对不对。别小看这个,很多“玄学”问题都是因为手指坐标没取对。我一般会在项目里留一个bDebugTouch开关,打包时关掉,编辑器里打开。

这套方案我在几个触摸屏项目里跑过,从 55 寸到 86 寸都稳。核心就一句话:别依赖现成映射,自己维护手指状态,每帧算 Delta,阈值给够。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询