简介:面向 UE4/UE5 开发者的触摸屏交互示例工程包,围绕移动、旋转、缩放三类高频触控操作,讲解 FInputEvent 与 FTouchEvent 的触发机制,并演示单指平移、双指捏合缩放与旋转的计算思路。适合移动端游戏或交互应用开发者参考,能帮助快速掌握多触点输入的处理流程与蓝图实现方式。工程包共 349 个文件,总大小 610.21MB,以 uasset 和 umap 资源为主,包含可打开查看的关卡与蓝图资产;另有 ini 配置文件、png 说明图等。文件结构清晰,便于对照场景理解触摸事件绑定关系。已有 6429 人学习浏览。资源价值在于可直接在工程中查看 TouchInputComponent、GetPinchZoomDelta 等节点的用法,配合配置文件和示意图,能缩短触摸输入调试周期;同时可通过示例场景验证防抖与响应速度优化,是一份兼顾原理与实操的参考资料。
1. UE4/UE5 在触摸屏上移动、旋转、缩放事件:为什么真机上总与你想象的不一样
UE4/UE5 在触摸屏上移动、旋转、缩放事件,表面看是三个手势,本质是同一套触摸输入在不同映射策略下的差异。很多项目在 PC 上用鼠标模拟触摸调通了,一上真机就发现点不准、旋转跳变、缩放被系统手势抢走。这篇文章直接拆开触摸事件怎么进 UE、坐标怎么算、三套手势怎么写,以及我踩过的几个坑。适合做交互大屏、车载触摸应用和可视化预览的开发者,也适合从传统 HMI 组态屏转过来、第一次接触 UE 触摸输入的人。
2. 触摸事件接入 UE:输入系统的绑定、坐标口径与平台差异
2.1 触摸事件的生命周期与 FingerIndex
UE 在移动端从系统拿到触摸坐标后,先交给 PlayerController 的 InputTouch 通道,再通过 InputComponent 分发到绑定的回调。整个链路比鼠标事件多一个东西:FingerIndex。它用来区分第几根手指,枚举值从 Touch1 到 Touch10。实际项目中大多数设备最多同时识别 5 到 10 指,但 UI 逻辑通常只需要第一指和第二指。
触摸事件的生命周期分三段:按下(IE_Pressed)、移动(IE_Repeat)、抬起(IE_Released)。注意,按住不动时引擎不会持续触发 IE_Repeat,只有手指位置变化超过一个像素阈值才会触发。这个行为和鼠标拖动不同,鼠标在屏幕上不动也会持续产生 MouseMove,而触摸不会。所以在做旋转和缩放手势时,不能依赖事件持续触发来完成动画,必须在触摸移动的每一帧里采样位置并计算增量。
下面是我一般在 PlayerController 里绑定触摸事件的最小 C++ 骨架:
// MyTouchPlayerController.h #pragma once #include "CoreMinimal.h" #include "GameFramework/PlayerController.h" #include "MyTouchPlayerController.generated.h" UCLASS() class AMyTouchPlayerController : public APlayerController { GENERATED_BODY() protected: virtual void SetupInputComponent() override; void OnTouchStart(ETouchIndex::Type FingerIndex, FVector Location); void OnTouchMove(ETouchIndex::Type FingerIndex, FVector Location); void OnTouchEnd(ETouchIndex::Type FingerIndex, FVector Location); TMap<ETouchIndex::Type, FVector> TouchPositions; };// MyTouchPlayerController.cpp #include "MyTouchPlayerController.h" void AMyTouchPlayerController::SetupInputComponent() { Super::SetupInputComponent(); // 触摸按下、移动、抬起分别绑定到同一条输入链路上 InputComponent->BindTouch(IE_Pressed, this, &AMyTouchPlayerController::OnTouchStart); InputComponent->BindTouch(IE_Repeat, this, &AMyTouchPlayerController::OnTouchMove); InputComponent->BindTouch(IE_Released, this, &AMyTouchPlayerController::OnTouchEnd); } void AMyTouchPlayerController::OnTouchStart(ETouchIndex::Type FingerIndex, FVector Location) { // Location 是屏幕像素坐标,原点在左上角,X 向右,Y 向下,Z 为 0 TouchPositions.FindOrAdd(FingerIndex) = Location; } void AMyTouchPlayerController::OnTouchMove(ETouchIndex::Type FingerIndex, FVector Location) { if (TouchPositions.Contains(FingerIndex)) { TouchPositions[FingerIndex] = Location; } } void AMyTouchPlayerController::OnTouchEnd(ETouchIndex::Type FingerIndex, FVector Location) { TouchPositions.Remove(FingerIndex); }这里有两个关键点。第一,回调里的 Location 是屏幕像素坐标,不是视口坐标,也不是世界坐标,后面做 deproject 之前要先做单位换算。第二,TouchPositions 这个 Map 必须自己维护,因为在 iOS 和 Android 上,当第一根手指抬起后,引擎未必会把第二根手指重新编号为 Touch1,如果你只在代码里硬编码处理 Touch1 和 Touch2,大概率会在断指重连后出现旋转跳变。
2.2 蓝图绑定触摸事件的另一种方式
如果项目偏蓝图,也可以不走 C++。在 Actor 的 Class Settings 里勾选 Auto Receive Input,选择 Player 0,然后在 Event Graph 里添加 Input Touch 节点。这个方案适合快速原型,但要从触摸事件里拿到坐标会绕一点。
常见做法是添加输入动作映射:打开 Project Settings -> Input -> Action Mappings,新建一个 Action 名为 Touch,底层绑定到 Touch1 或 Touch2。然后在蓝图中监听这个 Action 的 Pressed / Released,再通过 Get Input Touch State 节点查询当前触摸位置。这个方案的优点是纯蓝图、可视化,缺点是 Get Input Touch State 只能查指定手指的状态,当手指数量动态变化时,蓝图分支会非常复杂。
触摸事件本身不分鼠标还是触摸,但 UE 里有一个模拟开关。项目设置里 Simulate Touch 开启后,鼠标点击会按触摸路径走一遍,方便在编辑器里测试。这个选项只影响编辑器预览,不影响真机,真机上系统会直接上报触摸坐标。但要注意,开启模拟触摸后,编辑器里鼠标拖动会被当成单指触摸,双指手势无论如何都模拟不出来,所以旋转和缩放逻辑必须在真机上验证。
2.3 传统输入与增强输入怎么选
UE5 的增强输入系统把触摸抽象成了 InputAction 和 Trigger,理论上更规范,但增加了一层指针 ID 到手指索引的映射。我实际做过几个交互屏项目,增强输入对触摸的适配在 UE5 早期版本里并不顺手,尤其是多指手势的状态管理,比传统 BindTouch 繁琐。项目允许的情况下,我一般直接用传统输入系统,理由很简单:触摸是天然的即时手势,传统输入的回调里直接能拿到屏幕坐标和手指索引,不需要在一堆 Trigger 配置里找事件来源。
选型时可以参考下表:
| 维度 | 传统输入 | 增强输入 |
|---|---|---|
| 触摸坐标获取 | 回调参数直接给 | 需要从 ActionValue 解析 |
| 多指状态管理 | FingerIndex 明确 | 指针 ID 与手指映射较绕 |
| 蓝图兼容性 | 老项目通用 | 新项目模板默认开启 |
| 双指手势复杂度 | 低 | 中高,适合复杂组合键 |
2.4 平台差异:iOS、Android 和 Windows 触摸屏
不同触控设备上报坐标的时机不一样。iOS 的触摸事件是事件驱动的,手指移动时高频回调,抬起时也可靠;Android 的触摸事件在某些低端设备上会丢帧,尤其是快速滑动时,上一帧和下一帧的坐标差会非常大,所以计算增量时要做阈值过滤。Windows 触摸屏则走 Win32 Touch Input 或 Pointer Input,UE 拿到的坐标已经经过系统窗口 DPI 缩放,如果没有处理 DPI Scale,会出现点偏到按钮上方的情况。
所以我在项目一开始就会定义一个全局的触摸坐标归一化工具函数:把屏幕像素坐标先除以 ViewportScale,再做 deproject。这一步在项目设置里没有现成开关,必须自己写在坐标转换层里。
3. 移动、旋转、缩放事件的核心逻辑:从单指到多指的三套实现
3.1 单指移动事件:触摸位置增量转 Actor 位移
单指移动是最简单的一类触摸事件,也是很多交互项目的基础。触摸按下的位置记录了起点,之后每次移动事件计算与上一次位置之间的 Delta,把 Delta 映射到世界空间的移动量。
void AMyTouchActor::HandleTouchMove(ETouchIndex::Type FingerIndex, FVector Location) { if (FingerIndex != ETouchIndex::Touch1) { return; } FVector2D Current(Location.X, Location.Y); FVector2D Delta = Current - LastTouchPosition; // 过滤掉过小的位移,避免手指轻抖导致 Actor 抖动 if (Delta.Size() < 0.5f) { return; } // MoveSpeed 是世界单位 / 屏幕像素的比例系数,需要按项目场景分辨率调整 FVector WorldDelta(Delta.X * MoveSpeed, Delta.Y * MoveSpeed, 0.0f); AddActorWorldOffset(WorldDelta); LastTouchPosition = Current; }这里最难调的是 MoveSpeed。它不是一个标准值,和相机距离、屏幕 DPI、场景尺寸都有关。一般做法是在项目里放一个可调变量,先在 1920x1080 的窗口下调到手感合适,再上真机根据实际分辨率修正。如果场景是一个 1:1 的 CAD 模型,MoveSpeed 可能在 0.5 到 2 之间;如果是俯视的 2D 地图,可能需要 5 到 10。
3.2 双指旋转事件:角度增量计算与灵敏度设定
旋转手势的核心是计算两指连线与水平方向的夹角。按下时记录初始角度,移动时计算当前角度,两者差值就是旋转增量。
float CurrentAngle = FMath::Atan2(Y2 - Y1, X2 - X1); float DeltaAngle = FMath::RadiansToDegrees(CurrentAngle - LastAngle); // NormalizeAxis 把角度限制在 -180 到 180,避免跨 0 度时产生 360 度跳变 float FinalAngle = FRotator::NormalizeAxis(DeltaAngle); AddActorLocalRotation(FRotator(0.0f, FinalAngle * RotationSensitivity, 0.0f)); LastAngle = CurrentAngle;RotationSensitivity 一般取 1.0,表示手指转多少度物体转多少度。如果觉得转得太快,可以降到 0.6 到 0.8;如果是对称模型或地图旋转,建议 1.0 以上。这个参数在真机上感受差异很大,因为真机的触摸采样率通常只有 60Hz 到 120Hz,而编辑器里鼠标模拟的采样率更高,所以编辑器调好的灵敏度在真机上往往会偏滑。
3.3 双指缩放手势:两指距离变化转缩放比例
缩放手势的核心是两指间距。按下时记录初始距离,移动时当前距离除以初始距离,得到缩放因子,再应用到 Actor 的 Scale。
float CurrentDistance = FVector2D::Distance(Position1, Position2); float ScaleFactor = CurrentDistance / InitialDistance; FVector NewScale = InitialScale * ScaleFactor; NewScale.X = FMath::Clamp(NewScale.X, MinScale, MaxScale); NewScale.Y = FMath::Clamp(NewScale.Y, MinScale, MaxScale); NewScale.Z = FMath::Clamp(NewScale.Z, MinScale, MaxScale); SetActorScale3D(NewScale);这里有一个容易被忽略的问题:缩放中心。默认 SetActorScale3D 是围绕 Actor 原点缩放的,但用户双指缩放时,手势中心往往在屏幕中间,不在 Actor 原点。常见的做法是把 Actor 位置朝向两指中心点做一个补偿:计算缩放前后手指中心对应的世界坐标,将 Actor 平移,使该点保持不动。否则用户会觉得画面不光缩放,还会漂移。
3.4 触摸手势状态机:从按下到抬起的完整切换
单指移动、双指旋转、双指缩放不能并行,它们是一套状态机。我一般用如下状态:
enum class ETouchGestureState { None, SingleTouchMove, TwoFingerRotate, TwoFingerScale };按下第一根手指时进入 SingleTouchMove;按下第二根手指时退出移动,进入 TwoFingerRotate;如果两指距离变化大于阈值,再切到 TwoFingerScale。第二根手指抬起后,如果第一根手指还在,需要重新记录位置并切回 SingleTouchMove,否则手势会断开。
这个状态切换是所有触摸交互项目的核心,比如 UE 里熟悉的手势交互,本质上都是同样的状态机。你在任何一个商业引擎的触摸交互插件里都能看到类似结构,区别只在边界条件的处理。
4. 触摸坐标与视角转换:为什么点不准、怎么调
4.1 从屏幕坐标到世界坐标的 deproject 流程
UE 的触摸事件坐标是屏幕像素坐标,想把它落到 3D 场景里,必须用 DeprojectScreenPositionToWorld。这个函数接收两个 float,返回一条从相机出发的世界射线。射线和场景里的物体做碰撞检测,才能拿到对应的世界坐标。
float ViewX = TouchLocation.X / UWidgetLayoutLibrary::GetViewportScale(this); float ViewY = TouchLocation.Y / UWidgetLayoutLibrary::GetViewportScale(this); FVector WorldOrigin; FVector WorldDirection; UGameplayStatics::GetPlayerController(this, 0) ->DeprojectScreenPositionToWorld(ViewX, ViewY, WorldOrigin, WorldDirection);ViewportScale 是 DPI 缩放系数。在 4K 触摸屏上,系统会把分辨率缩放到逻辑分辨率,触摸原始坐标是物理像素,如果不除以 ViewportScale,射线会偏移。这个偏移在鼠标上不明显,因为鼠标本身已经经过系统 DPI 校正,但触摸事件的原始坐标很多设备不会校正,必须手动处理。
4.2 在 UI 上处理触摸与在 3D 场景里处理触摸的差异
如果是纯 UI 项目,触摸事件可以直接用 UMG 的 OnTouch 事件,不需要做 deproject。但 UMG 的触摸事件只有在 Input Mode 设置为 UI Only 时才生效,而且 UMG 控件不会同时把触摸事件穿透给 PlayerController。我常见的做法是:UI 按钮使用 UMG 原生触摸响应,3D 物体的拖拽使用 PlayerController 的触摸回调,两者之间用系统焦点切换。
4.3 触摸屏驱动工程师视角下的坐标偏移
做工业触摸屏项目时,我遇到过和硬件驱动层相关的坐标偏位。UE 拿到的坐标是操作系统已经校准过的坐标,但校准矩阵在系统层面保存,如果触摸屏驱动没校准好,所有应用包括 UE 都会偏。这类问题排查时,先用系统自带的触摸校准工具画十字线,确认系统层面是否准确,再回 UE 里看 deproject 是否正确。不要一上来就改 UE 代码,否则最后会发现硬件偏移和软件偏移叠加在一起,越调越乱。
5. 触摸屏开发避坑记录:现象、原因与解决
5.1 第一根手指抬起后,第二根手指拖动导致旋转跳变
现象:双指旋转时,其中一根手指离开屏幕,剩下那根手指继续滑动,模型角度瞬间跳到另一个位置。
原因:引擎在系统层面会重新分配手指索引,也可能不会。你的代码里如果仍按原来的 Touch2 取值,第二根手指的位置其实是第一根手指的位置,距离和角度全乱了。
解决:不要依赖固定的 FingerIndex,自己维护已按下的手指队列。每次 EigenTouchEnd 时把对应的位置从队列移除,再检查队列里剩余手指的数量。如果从两根变一根,立即把剩余那根手指的位置作为新的单指起点。
5.2 真机与编辑器模拟坐标不一致
现象:编辑器中调整好的点击位置,打包到安卓或 Windows 触摸一体机上后整体偏移了 10 到 50 像素。
原因:编辑器的模拟触摸坐标会经过窗口 DPI 处理,真机触摸屏上报的是物理像素坐标,两者的 ViewportScale 不同。
解决:统一在触摸回调入口处调用 GetViewportScale 做归一化。凡是进入手势逻辑的坐标,都先转成视口比例坐标,再传出去。不要在 gesture 处理函数里混合使用屏幕像素和视口坐标。
5.3 触摸事件与 UMG 按钮打架
现象:点击屏幕上一个 Button 时,场景里的模型也跟着移动了。
原因:Button 消费了点击事件,但 PlayerController 的触摸路由仍然被触发,两者同时响应。
解决:在 PlayerController 里检测当前鼠标位置对应的 Widget 是否在响应。命中 UI 时直接跳过本帧的手势处理。具体做法是用 FWidgetPath 做一次 UMG 命中测试,或者简单地在 UI 打开时设置 Input Mode 为 UI Only,关闭时切回 Game And UI。
5.4 iOS 边缘滑动触发系统手势
现象:在 iPad 上做双指旋转时,手指刚移到屏幕边缘,系统返回手势就弹出来了,应用被切走。
原因:iOS 系统本身有侧滑返回和边缘手势,触摸事件在应用之前先被系统抢走。
解决:应用内隐藏 Home Indicator,使用全屏沉浸模式;另外在手势检测时对起始位置加一个边缘禁区,比如距离屏幕左右 15 像素内不启动旋转或缩放。这个边缘禁区在 UE 里就是判断触摸坐标的 X 值范围。
5.5 旋转抖动、缩放突然放大
现象:双指旋转时画面左右抖动,缩放时偶尔瞬间放大到最大。
原因:角度差没有经过 NormalizeAxis,当角度从 179 度变到 -179 度时,差值被算成 358 度而不是 -2 度;缩放距离在手指稍有抖动时比例异常放大。
解决:所有角度增量都走 FRotator::NormalizeAxis;缩放比例用指数平滑或低通滤波,比如 lerp 到目标值,而不是直接把 ScaleFactor 设给 Actor。这个坑是我在所有触摸项目中都会提前写进通用组件里的处理逻辑。
6. 触摸手感的最后一步:灵敏度系数、事件日志与真机验证
6.1 用灵敏度系数和阻尼调手感
手势逻辑跑通后,手感是决定项目成败的关键。旋转灵敏度建议拆成三个参数:RotationSensitivity 用于控制角度映射比例,默认 1.0;RotationDamping 用于控制松手后的惯性,0 表示无惯性;RotationSmoothness 用于对角度增量做指数平滑。
float Alpha = FMath::Clamp(Smoothness * DeltaTime, 0.0f, 1.0f); SmoothedAngle = FMath::Lerp(SmoothedAngle, RawAngle, Alpha);阻尼和滑动的取舍要按场景来。机械设备预览项目建议零惯性,地图浏览项目可以加一点惯性,但惯性过大时用户会觉得失控。真机上的采样率不一致,先用慢速拖动验证角度方向,再逐渐提高速度测试是否跟手。
6.2 用手写日志验证触摸事件流
我一般在开发阶段加一个调试函数,把每帧的触摸状态打印出来:
if (bDebugTouch) { UE_LOG(LogTemp, Warning, TEXT("Touch %d: Type=%d, X=%.1f, Y=%.1f, State=%d"), (int32)FingerIndex, (int32)TouchType, Location.X, Location.Y, (int32)GestureState); }日志能直接看出坐标是否连续、状态机是否切错、事件是否丢帧。真机上用 PC 串口或者局域网日志排查,比在蓝图上断点更高效。我的习惯是:先左手在屏幕上画圈,右手看日志里角度增量是不是平滑递增;再做两次快速两指开合,看缩放有没有超过预期范围。这条验证路径我在每个触摸项目上都会走一遍,能挡掉至少一半的玄学问题。
6.3 参数对比与真机测试清单
| 参数 | 默认值 | 调试建议 |
|---|---|---|
| MoveSpeed | 视场景而定 | 从 1 开始,上下浮动 |
| RotationSensitivity | 1.0 | 低于 0.6 会显得迟钝 |
| ScaleSmoothness | 0.2 | 太高会导致缩放跟不上手指 |
| DPI Scale | 自动 | 真机必须手动核对 |
最后一条经验:触摸事件这类交互,八成问题出在状态管理,两成出在坐标转换。遇到翻车别急着调参数,先看触摸事件日志,把手指状态和坐标位置确认清楚,再动代码。希望这些坑和写法能帮到你。
本文还有配套的精品资源,点击获取