UE5鼠标点击移动实现:屏幕坐标转世界坐标与AI导航
2026/9/23 17:12:44 网站建设 项目流程

简介:这是一份面向UE4/UE5开发者的鼠标点击寻路交互示例工程,适合具备蓝图基础、希望快速实现点击地面移动角色的学习者参考。资源围绕射线碰撞检测、模型边缘高亮、鼠标样式自定义切换以及DoTween移动动画四个技术点展开,并附有说明:若未购买DoTween插件,删除蓝图中对应移动方法即可正常运行。压缩包共572个文件,以413个uasset蓝图与资源文件、14个umap关卡地图、41个ini配置及92个log日志为主,另含uproject工程文件与少量bin、json等辅助数据,整体约675.41MB,工程结构完整可直接打开研究。目前已有4416人学习下载,读者可从中掌握点击寻路的完整实现链路、高亮与光标切换的细节处理,以及借助DoTween平滑移动的动画思路,便于迁移到RPG、策略或塔防类项目中。

1. 鼠标点地移动:从玩家直觉到引擎里的一条射线

在第三人称或俯视角项目里,玩家最自然的操作就是「鼠标点哪,角色走哪」。这个需求听起来像一行代码,实际落地时会牵出三件事:屏幕坐标怎么变成世界坐标、角色怎么绕过障碍走到目标点、以及移动过程中怎么和动画、输入系统对齐。很多新手第一次做,直接拿鼠标位置当世界坐标用,结果角色往天上飞或者原地抽搐,本质是没理解屏幕空间和世界空间之间隔着相机投影。

这套方案适合做 RTS、MOBA、俯视角 RPG、点击式冒险,也适合 UE5 里做原型验证。UE4 和 UE5 在这条链路上差异不大,主要区别在 UE5 默认启用了 Enhanced Input,以及导航系统对 Large World Coordinates 的处理。核心链路是:鼠标点击事件 → 屏幕坐标 → 去投影射线 → 与地面碰撞 → 得到世界坐标 → 交给 AI 导航移动。下面按这条链路拆开讲,每一段都能直接抄进项目里跑。

2. 屏幕坐标转世界坐标:Deproject 与射线检测的取舍

2.1 为什么不能直接用鼠标坐标当世界坐标

鼠标事件给的是屏幕空间坐标,原点在视口左上角,单位是像素。世界坐标是三维空间里的厘米单位。两者之间差了一个相机投影矩阵和一次深度求解。把屏幕坐标直接赋给SetActorLocation,Z 值会是 0 或者视口高度,角色自然乱跑。

常见做法有两种:一种是用DeprojectScreenPositionToWorld把屏幕点转成世界空间的一条射线,再和地面求交;另一种是用GetHitResultUnderCursorByChannel直接让引擎帮你做射线检测。前者灵活,能自己控制射线长度和碰撞通道;后者省事,适合快速原型。

2.2 用 GetHitResultUnderCursorByChannel 拿到地面点

这是最省代码的方式,适合俯视角和第三人称。在 PlayerController 里绑定输入,然后调用:

// AMyPlayerController.cpp void AMyPlayerController::MoveToCursor() { FHitResult HitResult; // 用 Visibility 通道检测,地面需要设置成 Block Visibility bool bHit = GetHitResultUnderCursorByChannel( UEngineTypes::ConvertToTraceType(ECC_Visibility), true, // bTraceComplex,复杂碰撞更精确但更慢 HitResult); if (bHit && MoveTargetActor) { // HitResult.Location 就是鼠标点到的世界坐标 MoveTargetActor->MoveToLocation(HitResult.Location, 50.f); } }

ECC_Visibility是碰撞通道,地面 StaticMesh 的碰撞预设要能挡住 Visibility,否则射线穿过去打不到。bTraceComplex设为 true 时会用复杂碰撞体,地形起伏大时更准,但每帧调用会有开销,点击事件里用一次无所谓。MoveToLocation的第二个参数是接受半径,50 表示离目标点 50 厘米内就算到达,避免角色在目标点附近抖动。

2.3 用 DeprojectScreenPositionToWorld 自己控制射线

需要控制射线长度、忽略某些物体、或者做多层检测时,用这个:

void AMyPlayerController::MoveToCursorManual() { FVector WorldLocation, WorldDirection; // 把鼠标屏幕坐标转成世界空间射线 if (!DeprojectMousePositionToWorld(WorldLocation, WorldDirection)) return; const float TraceDistance = 10000.f; // 射线长度,单位厘米 FVector TraceEnd = WorldLocation + WorldDirection * TraceDistance; FHitResult Hit; FCollisionQueryParams Params; Params.AddIgnoredActor(GetPawn()); // 忽略角色自身 Params.bTraceComplex = true; bool bHit = GetWorld()->LineTraceSingleByChannel( Hit, WorldLocation, TraceEnd, ECC_Visibility, Params); if (bHit) { // 命中点就是目标位置 DrawDebugSphere(GetWorld(), Hit.Location, 20.f, 12, FColor::Green, false, 2.f); } }

DeprojectMousePositionToWorld内部会处理视口缩放和相机投影,比手动算矩阵可靠。TraceDistance给 10000 厘米(100 米)足够覆盖大多数场景,太小会导致远处点不到。AddIgnoredActor把角色自己排除,否则射线可能打在角色胶囊体上。DrawDebugSphere是调试用的,确认命中点位置对不对,正式发布时去掉。

2.4 两种方式的对比与选择

方式代码量可控性适用场景
GetHitResultUnderCursorByChannel快速原型、俯视角
DeprojectScreenPositionToWorld需要过滤、多层检测
手动算投影矩阵最高特殊相机、自定义投影

提示:如果地面是 Landscape 或者复杂地形,bTraceComplex必须开,否则射线会穿过凹陷处打到下面的碰撞体。

3. 让角色真正走过去:AI 导航与 MoveTo 的落地细节

3.1 导航网格是前提,没有 NavMesh 一切白搭

拿到目标点只是第一步,角色要绕开障碍走过去,必须依赖导航系统。在场景里放一个NavMeshBoundsVolume,缩放到覆盖可行走区域,按 P 键能看到绿色导航网格。如果地面没变绿,检查三件事:Volume 是否覆盖、地面碰撞是否设为Blocking Volume或可导航、项目设置里 Navigation Mesh 的 Agent 半径是否合理。

UE5 里导航系统默认开启,但 Large World Coordinates 下如果场景坐标超过百万级别,需要调整Navigation SystemCell SizeAgent Max Step Height。常见做法是保持场景原点附近建模,避免坐标过大。

3.2 用 AIController 的 MoveToLocation 驱动移动

角色蓝图里如果用的是Character,需要给它挂一个AIController,或者在 PlayerController 里直接调用MoveToLocation。更规范的做法是走 AI 导航:

// 在 AIController 或 Character 里 void AMyCharacter::MoveToTarget(FVector TargetLocation) { if (AAIController* AICon = Cast<AAIController>(GetController())) { // 接受半径 50,超时 5 秒,绘制调试路径 EPathFollowingRequestResult::Type Result = AICon->MoveToLocation(TargetLocation, 50.f, true, true, false, true); if (Result == EPathFollowingRequestResult::Failed) { UE_LOG(LogTemp, Warning, TEXT("MoveToLocation failed, check NavMesh")); } } }

MoveToLocation的参数依次是:目标点、接受半径、是否使用路径优化、是否投影到导航网格、是否绘制调试、是否允许部分路径。第三个参数bProjectDestinationToNavigation设为 true 时,如果目标点不在导航网格上,会自动投影到最近的可行走点,这个在点击地面边缘时很有用。

3.3 移动过程中的动画与旋转对齐

角色移动时朝向不对会显得很假。常见做法是在CharacterMovementComponent里开启Orient Rotation to Movement,让角色自动转向移动方向:

// 在 Character 构造函数里 GetCharacterMovement()->bOrientRotationToMovement = true; GetCharacterMovement()->RotationRate = FRotator(0.f, 540.f, 0.f); // 每秒转 540 度 bUseControllerRotationYaw = false; // 关闭控制器控制朝向

RotationRate控制转身速度,540 度每秒比较跟手,太小会转得很慢,太大像陀螺。bUseControllerRotationYaw必须关掉,否则控制器朝向会覆盖移动朝向。动画蓝图里用Velocity的长度驱动 BlendSpace,速度为零时切到 Idle,这样移动和动画就对齐了。

3.4 点击移动的输入绑定与 Enhanced Input

UE5 默认用 Enhanced Input,需要在IMC_Default里建一个IA_MoveTo映射,触发方式选Triggered。然后在 PlayerController 里绑定:

void AMyPlayerController::SetupInputComponent() { Super::SetupInputComponent(); if (UEnhancedInputComponent* EIC = Cast<UEnhancedInputComponent>(InputComponent)) { EIC->BindAction(MoveToAction, ETriggerEvent::Triggered, this, &AMyPlayerController::MoveToCursor); } }

ETriggerEvent::Triggered在按键按下时触发一次,适合点击移动。如果要做按住持续移动,改成Started加定时器。UE4 里用InputComponent->BindAction("MoveTo", IE_Pressed, this, ...)即可,逻辑一样。

4. 实战排错:点不到、走不过去、抖动这三类问题

4.1 鼠标点地面没反应,先查碰撞通道

最常见的原因是地面没有阻挡Visibility通道。打开地面 StaticMesh 的碰撞设置,确认Collision PresetsBlockAll或至少Block Visibility。如果是自定义通道,GetHitResultUnderCursorByChannel里的通道要对应上。另一个原因是 PlayerController 的bShowMouseCursor没开,或者bEnableClickEvents没开,鼠标事件根本传不到。

// 在 PlayerController 初始化里 bShowMouseCursor = true; bEnableClickEvents = true; bEnableMouseOverEvents = true;

bEnableClickEvents控制点击事件是否派发,bEnableMouseOverEvents控制悬停事件。只做点击移动的话,前两个就够。

4.2 角色走到一半停住,检查导航网格和接受半径

如果角色走到障碍物旁边就停,多半是导航网格没覆盖到目标点,或者接受半径太小导致角色在目标点附近反复寻路。把MoveToLocation的接受半径从 50 调到 80 试试。另外检查NavMeshBoundsVolume是否覆盖了目标区域,按 P 键看绿色网格有没有断。

还有一种情况是目标点在导航网格边缘,bProjectDestinationToNavigation设为 false 时直接失败。设为 true 会自动投影到最近可行走点,但投影距离有限,太远的目标点还是会失败。常见做法是在点击时先判断目标点是否在导航网格上,不在就忽略这次点击。

4.3 角色到达后原地抖动,用接受半径和停止逻辑解决

抖动的原因是角色在目标点附近反复修正位置。MoveToLocation的接受半径设大一点,或者在OnMoveCompleted回调里把速度清零:

void AMyCharacter::OnMoveCompleted(FAIRequestID RequestID, const FPathFollowingResult& Result) { if (Result.Code == EPathFollowingResult::Success) { GetCharacterMovement()->StopMovementImmediately(); } }

StopMovementImmediately会立刻停止移动组件,避免惯性导致的微小位移。如果用的是MoveToLocation的返回值判断,注意SuccessBlocked的区别,Blocked表示路径被挡住,需要重新寻路。

4.4 调试手段:DrawDebug 和导航网格可视化

排错时最有用的是把射线和路径画出来。DrawDebugLine画射线,DrawDebugSphere画命中点,DrawDebugPath画导航路径。按 P 键切换导航网格显示,按'键打开 AI 调试。这些手段能快速定位是射线没打中、还是导航没连通、还是移动组件没响应。

注意:调试绘制在 Shipping 包里会被编译掉,正式发布前确认没有依赖调试绘制的逻辑。

5. 进阶:点击移动与多播委托、触摸输入的衔接

5.1 用多播委托解耦点击事件和移动逻辑

点击移动做大了以后,点击事件可能还要触发 UI 高亮、技能指示器、音效。用多播委托把「点击到地面」这个事件广播出去,比在 PlayerController 里堆逻辑干净:

// 在 PlayerController 里声明 DECLARE_MULTICAST_DELEGATE_OneParam(FOnGroundClicked, FVector /*Location*/); FOnGroundClicked OnGroundClicked; // 命中后广播 if (bHit) { OnGroundClicked.Broadcast(HitResult.Location); }

其他系统比如 UI、音效、特效,订阅这个委托即可,不用改 PlayerController。UE5 里也可以用Dynamic Multicast Delegate配合蓝图,让策划也能挂逻辑。

5.2 触摸输入下的点击移动适配

移动端或触屏设备上,鼠标点击变成触摸。UE5 里用bEnableTouchEventsbEnableTouchOverEvents,触摸事件会走和鼠标类似的派发路径。双指触摸蓝图里常见做法是单指点击移动、双指缩放视角,需要在 Input 里区分触摸点数。GetHitResultUnderFingerByChannel是触摸版的命中检测,参数和鼠标版类似,第一个参数传手指索引。

5.3 性能与边界:高频点击和大地图的处理

高频点击时每次都做射线检测和寻路会有开销。常见做法是加一个最小点击间隔,比如 0.1 秒内只响应一次。大地图上导航网格分块加载,点击远处时先判断目标点是否在已加载的导航网格内,不在就忽略或者提示。UE5 的 World Partition 下,导航网格也是流式加载的,跨区域点击需要等导航数据加载完再寻路,否则会失败。

// 简单的点击节流 float LastClickTime = 0.f; if (GetWorld()->GetTimeSeconds() - LastClickTime < 0.1f) return; LastClickTime = GetWorld()->GetTimeSeconds();

这段放在MoveToCursor开头,能避免连点导致的寻路风暴。0.1 秒是经验值,手感上几乎无感,但能挡掉大量重复请求。

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

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

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

立即咨询