ARTICLE DETAIL

资讯详情

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

UE4/UE5鼠标点击地面移动:射线检测与NavMesh寻路完整指南

UE4/UE5鼠标点击地面移动:射线检测与NavMesh寻路完整指南 简介面向UE4/UE5开发者的鼠标点击地面移动功能示例包完整演示从屏幕射线检测、命中点位计算到角色平滑移动的交互闭环适用于TopDown视角游戏、RTS选单位移动及原型验证等场景。资源内含572个文件以413个uasset蓝图资产和14个umap地图文件为核心辅以DefaultInput等ini配置文件与日志记录压缩包约675.41MB目录结构清晰便于按模块查阅或直接移植到现有项目。技术实现覆盖四个关键点基于射线碰撞检测的地面交互、模型边缘高亮反馈、自定义鼠标样式切换以及使用DoTween插件实现的移动动画效果若未安装DoTween删除对应蓝图节点即可不影响基础移动逻辑。已有4417人学习下载适合正在学习UE交互逻辑或需要快速落地鼠标点选移动功能的中级开发人员参考可从中提取射线检测与动画控制的通用写法。1. 鼠标点击地面移动UE4/UE5里最容易被轻视的交互链路在UE4和UE5项目里“鼠标点击地面、角色走到指定位置”几乎是MMO、MOBA、RTS这几种可玩角色的标配交互。它看起来只是“鼠标点一下人走过去”实际上背后是一条完整链路屏幕坐标要先转成世界坐标世界坐标要判断是否落在可导航区域确认后角色要沿路径平滑地过去到了之后动画还要按预期停下。UE4_UE5新手第一次做这个功能时很容易照网上的HitResultMoveTo片段抄结果点击没反应、角色原地打转、走到一半卡在石头上白白耗掉半天。下面就把这条链路拆开先讲原理和选型再分别给蓝图和C两套可复现实现最后把高频翻车现场和手感优化一并说清楚。2. 点击移动的完整链路屏幕坐标、射线检测与NavMesh是怎么串起来的2.1 从鼠标点到世界坐标命中检测的三种方式与选型鼠标位置是屏幕上的像素点比如1920x1080的窗口里点在了(320,240)。这个二维坐标没有深度必须靠一条从摄像机近裁剪面穿过该像素的射线去和场景物体求交得到的命中点三维Location才算是角色要去的地方。UE4和UE5里实现“从鼠标到世界坐标”常见有三条路。第一条是GetHitResultUnderCursor它封装了DeprojectMousePositionToWorld和一次LineTraceSingleByChannel通道默认Visibility。蓝图里一个节点就能拿到HitResult胜在代码量小适合快速验证“点击有没有打中地面”。缺点是内部固定了通道和Trace参数不好在命中后过滤特定Actor比如点击自己的角色时希望忽略。第二条是手动Deproject LineTraceSingleByChannel。先在PlayerController里用DeprojectMousePositionToWorld拿到起点和方向再自己组装射线参数TraceStart、TraceEnd、Channel、IgnoreActors执行一次LineTrace。这种方式控制粒度最细生产环境我一般用这个。它能自定义TraceEnd距离——开放世界里鼠标指向远处天空时射线距离如果无限长会有开销通常把TraceDistance限制在20000到30000单位。第三条是用SphereTrace替代LineTrace。鼠标点击追求精确但精确意味着射线很容易碰到地面上的装饰物、碎石、草皮这些物体的碰撞如果处理不好会把目标点吸到奇怪位置。SphereTrace带一个半径去扫对“点一个区域角色走到最近可行走点”的场景更友好容错也高缺点是命中位置不如LineTrace精确。选型建议学习阶段直接用GetHitResultUnderCursor产品功能开发时用手动Deproject加LineTraceSingleByChannel并且在命中后做一次NavMesh可达性检查。做RTS框选移动的话SphereTrace才是主方案。还有一个容易踩的细节鼠标屏幕坐标原点在左上角视口归一化坐标原点在左下角Deproject会自动处理这个差异。若是自己写屏幕转世界坐标千万别手动翻转Y轴否则点击地面上方角色反而往下方跑。2.2 NavMesh在点击移动里扮演什么角色NavMesh是Recast导航系统生成的导航网格。它表达的不是“地面长什么样”而是“多大的角色能站在这块地面上并且能从A格安全走到B格”。点击移动真正依赖的不是地面Mesh而是地面之上看不见的那张导航网。AIController收到MoveToLocation后第一件事是把目标点投影到NavMesh表面。如果你点击的位置射线虽然打中了地面但那一块区域没被导航网格覆盖MoveTo就会失败角色纹丝不动。这也是很多新手“明明打中了为什么不动”的隐藏原因射线没问题NavMesh没覆盖。NavMesh的生成参数里最影响点击移动体验的是Agent Radius和Agent Height。Agent Radius决定角色能通过多窄的缝隙Agent Height决定角色能钻进多矮的通道。有些项目为省事把这两个参数设很大结果角色在走廊被导航判定“过不去”AI只能绕远路。反过来参数设太小导航面生成得过于细碎寻路性能和路径质量都会下降。NavMeshBoundsVolume的放置是最基本的一步。把Volume放大到覆盖整个可玩区域按键盘P键切出导航预览看到地表盖了一层青绿色网格再进游戏测点击移动。注意P键切换的是编辑器里“显示导航体”的开关别把场景里的其他可视化标志跟它混淆。我在项目里一般让关卡策划布好NavMeshBoundsVolume并在生成配置里把Agent Radius调到跟项目里最小活动角色的胶囊体匹配。否则不同体型的角色复用同一张NavMesh小体型角色在这套参数下会吃亏经常出现“肉眼看着能走AI偏偏绕路”的怪事。2.3 为什么生产环境都走AIController而不是直接MoveActor有人问直接用SetActorLocation往目标点挪不是更快吗技术上能产品上不行。SetActorLocation是瞬移逐帧调用时角色确实在动但那是被拉着走不是导航。它不会避障不会选择可达路径不会撞墙绕路也不会在坡上调整姿态。更麻烦的是移动组件内部的速度向量捕捉不到这种变化动画流始终停在原地观感很差。AIController的MoveToLocation是标准答案。它内部会用导航系统做A*寻路、生成一条路径再驱动CharacterMovementComponent按给定速度和加速度前进。路径点之间的转向、避障、停止逻辑都封装好了你只需要提供“目标点”和“容差半径”。UE4与UE5在这里没什么本质区别。UE4时代项目做点击移动常规做法是PlayerController里GetHitResultUnderCursor加MoveToLocationUE5保持了同样的API导航系统也是Recast的演进版。UE5改变的是默认物理和增强输入系统鼠标点击的底层交互依然走PlayerController绑定事件方案无需换思路。需要留意的只是移动结束后的转向。AIController默认不强制角色朝向移动方向如果角色没开bOrientRotationToMovement会看到角色侧着身子跑步。想让角色边走边转向就开bOrientRotationToMovement想让人物先转向再走比如类JRPG那种节奏可以配合bUseControllerRotationYaw做“转向在前、移动在后”的处理这个细节放到避坑章节展开。3. 蓝图实现在UE4/UE5里跑通点击地面移动的最小步骤3.1 场景准备放置NavMeshBoundsVolume并按P验证从Place Actors面板搜索Nav Mesh Bounds Volume拖一个到场景把Scale放大到覆盖玩家可能到达的所有区域。范围可以放到全图面积的120%多出来的部分只是多占一点内存。随后按键盘P键。视口里会出现一块块贴合地面的青绿色导航块。如果按P没看到去World Settings检查Navigation System的Auto Manage Navigation Generation是否开启同时确认地面StaticMesh的碰撞预设是否Block了Visibility。这个步骤最容易出现的问题地面Mesh用了NoCollision。这是美术对纯装饰地面常用的设置但导航生成依赖碰撞几何体NoCollision意味着这块区域完全没有NavMesh。解决方法是给地面加一个简单盒体碰撞或者把碰撞通道里的Visibility和Navigation都设为Block。很多项目还有地编同事问“为什么这块地区域发绿但角色进不去”原因是地面有碰撞、NavMesh也生成了但角色要走的入口宽度小于Agent Radius导航里认为这个洞过不去。这时就要回导航生成参数里调Agent Radius而不是硬塞一个过大的角色模型。3.2 PlayerController里绑定左键点击事件与射线检测新建一个PlayerController子类比如BP_MyPlayerController把它设为GameMode的默认Player Controller Class。然后在这个PlayerController里做三件事在Event BeginPlay里绑定左键按下事件到一个自定义事件OnClickMove在OnClickMove里用Get Hit Result Under Cursor by Channel节点Channel选Visibility把HitResult拆出来取出Hit Location。节点串法Event BeginPlay → 绑定InputAction“ClickMove”在Project Settings的Action Mappings里新增鼠标左键OnClickMove → Get Hit Result Under Cursor by ChannelVisibility → 判断bBlockingHit → Break Hit → 取Hit Location逻辑说明OnClickMove是一个自定义Event左键按下时引擎会调用它。Get Hit Result Under Cursor by Channel内部做了一次从鼠标位置出发的射线检测所以前面提到的手动Deproject在这里被封装掉了。Visibility通道的好处是地面、墙面这类静态几何体默认响应Visibility而角色、怪物等Pawn在很多项目里把Visibility设为Ignore于是玩家点怪不会触发移动点地面才会触发。参数说明Get Hit Result Under Cursor by Channel还有一个bTraceComplex参数设置为true时会检测复杂碰撞对复杂静态网格更准确代价是性能略高建议在PC端开true。移动端为了省性能再改回false。如果这一步你已经发现点击没有任何Hit先把地面的碰撞预设重新确认一遍大概率是Visibility没有被Block。3.3 把目标点交给AI MoveTo并处理移动结束回调拿到Hit Location后还要确保当前Pawn真的带了一个AIController。做法是在Pawn的细节面板里把Auto Possess AI设为Placed in World或Spawned并指定一个AIController类比如BP_MyAIController否则后面Cast到AIController会失败。蓝图节点Get Pawn → Get Controller → Cast to AIControllerAI MoveTo节点Controller连上面Cast的结果Destination连Hit LocationAcceptance Radius填50Stop on Overlap勾上On Success引脚接“到达处理”On Fail引脚接“无法到达”的反馈逻辑逻辑说明AI MoveTo是异步的。它发出请求后立刻返回角色寻路过程中不阻塞游戏线程。On Success在角色到达目标点并停下时触发On Fail在寻路失败或没有可行路径时触发。所以“到达处理”和“无法到达”这两个分支必须都接。移动结束时建议用Stop Movement Immediately节点把速度清零再配合动画蓝图里的Speed变量把跑步动画切回Idle。失败反馈在MOBA里很常见点不可达区域角色脚下闪一个感叹号或者屏幕边缘变红。蓝图这套组合从事件绑定到移动调度总共不到十个节点。第一次接触UE4_UE5的新手照着摆就能跑通难点反而不在节点在前面的NavMesh是否覆盖、碰撞预设是否匹配这些看不见的准备工作上。这也是为什么我建议先做第2章的原理确认再动手连节点。4. C实现生产级点击移动的关键代码与参数4.1 输入绑定与射线通道用Visibility还是GameTrace用C实现时先重写PlayerController子类的SetupInputComponent// 头文件里声明 void OnClickMove(); void OnMoveCompleted(FAIRequestID RequestID); // cpp 里实现 void AMyPlayerController::SetupInputComponent() { Super::SetupInputComponent(); // 前提Project Settings - Input - Action Mappings 里配置好 ClickMove InputComponent-BindAction(ClickMove, IE_Pressed, this, AMyPlayerController::OnClickMove); }逻辑说明BindAction把名为ClickMove的Action映射绑定到OnClickMove。IE_Pressed只在按下瞬间触发一次不会在按住鼠标期间每帧触发避免角色在持续按住时反复接收移动指令。射线通道的选择是关键。GetHitResultUnderCursor内部固定用Visibility通道如果项目把地面放在自定义的GameTrace Channel上这里就会失效。两种解法要么让地面保持在Visibility通道的Block响应上要么改用GetHitResultUnderCursorByChannel传你自己的通道枚举。生产环境里我一般不在OnClickMove里直接调GetHitResultUnderCursor而是拆成两步先DeprojectMousePositionToWorld再LineTraceSingleByChannel。这样能拿到射线起点和终点既方便画调试线也方便动态改TraceDistance。下面是展开写法void AMyPlayerController::OnClickMove() { APawn* ControlledPawn GetPawn(); if (!ControlledPawn) { return; } FVector WorldOrigin; FVector WorldDirection; if (!DeprojectMousePositionToWorld(WorldOrigin, WorldDirection)) { return; } const float TraceDistance 30000.0f; FHitResult Hit; FCollisionQueryParams Params; Params.AddIgnoredActor(ControlledPawn); bool bHit GetWorld()-LineTraceSingleByChannel( Hit, WorldOrigin, WorldOrigin WorldDirection * TraceDistance, ECC_Visibility, Params ); if (!bHit) { return; } // 后面的移动调用见 4.2 }参数说明TraceDistance设成30000单位覆盖大部分关卡尺度同时避免射线无限长造成不必要的开销。AddIgnoredActor把自己忽略掉否则射线可能先打中自己的角色模型导致目标点落在角色身上而不是地面。4.2 AIController::MoveToLocation的完整调用与返回结果拿到命中点后完整调用如下#include AIController.h #include NavigationSystem.h void AMyPlayerController::OnClickMove() { // ... 省略 4.1 的射线部分此处 Hit.Location 即为目标点 AAIController* AIC CastAAIController(ControlledPawn-GetController()); if (!AIC) { UE_LOG(LogTemp, Warning, TEXT(Click move requires AIController)); return; } EPathFollowingRequestResult::Type Result AIC-MoveToLocation(Hit.Location, 50.0f, true, true); if (Result EPathFollowingRequestResult::Failed) { // 没有路径或目标点不可达 UE_LOG(LogTemp, Warning, TEXT(MoveToLocation failed at %s), *Hit.Location.ToString()); } }逻辑说明MoveToLocation是AIController的快捷接口内部做目标点投影、寻路、路径生成再交给移动组件执行。返回值用来区分立即失败和异步进行中。Failed并不是异常它只是告诉你“这次请求没被接收”常见于目标点不在NavMesh上、寻路超时、或者角色本身没有导航权限。参数说明第二个参数50.0f是AcceptanceRadius单位厘米表示角色中心点到目标点多近就算到达。人形角色50到100比较常见太小角色会反复修正太大角色会在离目标半个身位的地方停住。第三个参数true是bStopOnOverlap角色与他人重叠时是否停止纯地面移动保持true。第四个参数true是bCanStrafe允许平行移动动作游戏开true传统RPG开false。返回值还有一个AlreadyAtGoal角色已经站在目标点附近时MoveToLocation会直接返回这个枚举且不会产生任何移动也不会触发OnMoveCompleted回调。这意味着“已经到达”和“正式到达”两条逻辑都要覆盖否则角色会卡在“正在移动”状态里。4.3 移动结束后的状态复位与动画控制MoveToLocation是异步请求移动结束有两种形态真正走到或者中途被打断二次点击、被击退、主动调StopMovementImmediately。监听完成回调的标准做法是绑定AIController的OnMoveCompleted委托// 在 BeginPlay 或初始化点做绑定 AIC-OnMoveCompleted.AddDynamic(this, AMyPlayerController::OnMoveCompleted); void AMyPlayerController::OnMoveCompleted(FAIRequestID RequestID) { UCharacterMovementComponent* MoveComp GetPawn()-FindComponentByClassUCharacterMovementComponent(); if (MoveComp) { MoveComp-StopMovementImmediately(); } if (AMyCharacter* MyChar CastAMyCharacter(GetPawn())) { MyChar-bIsMoving false; // 供动画蓝图切换 Idle } }在移动发起时把bIsMoving置true动画蓝图里用这个布尔做状态切换比直接看Speed更可控。点击移动和手柄摇杆移动分属两套逻辑时bIsMoving还能帮你区分“自动寻路中”和“玩家手动操控中”。这里有一个生产级细节MoveToLocation如果在途中被二次命令打断OnMoveCompleted几乎总会触发但RequestID会变。正确做法是发起新请求时把返回值存下来CurrentMoveRequestID AIC-MoveToLocation(Hit.Location, 50.0f, true, true); void AMyPlayerController::OnMoveCompleted(FAIRequestID RequestID) { if (RequestID ! CurrentMoveRequestID) { return; // 旧请求的回调忽略 } // 才是当前目标真正到达 }不做这个过滤玩家快速连点时角色会在中途莫名其妙地触发“到达”逻辑动画突然切回Idle。这是新手代码最常见也最隐蔽的翻车点。5. 避坑指南点击移动最常见的5个翻车现场5.1 点击毫无反应Trace Channel与碰撞预设不匹配现象鼠标点击地面角色完全没反应HitResult要么没命中要么命中了但角色不动。原因分两类。一类是射线根本没打中地面——地面把Visibility通道设成了Ignore另一类是射线打中了但MoveToLocation返回Failed——目标点那块区域没有NavMesh覆盖。解决先按P看导航是否覆盖目标点再用DrawDebugLine把射线画出来第6章的调试方法就能定位是哪种。射线问题去改地面StaticMesh的Collision Presets把Visibility和Navigation都设为Block。NavMesh缺失则检查NavMeshBoundsVolume的覆盖范围以及地面碰撞是否Block了Navigation通道。我在项目里排查这个问题的顺序永远是先看HitResult对不对再看NavMesh覆盖有没有最后才怀疑代码。前两个是配置问题代码写了没跑的很少见。5.2 点击UI也触发移动忽略Widget的输入穿透现象玩家点技能按钮时角色在画面里也莫名其妙走位。原因鼠标事件没有被Widget消费穿透到了World。UE的Widget如果根节点设置了Self Hit Test Invisible就不拦截鼠标事件继续传到PlayerController。解决两个可靠办法。一是UI根Canvas下放一个全屏透明Image把Hit Test Visibility改为Visible作为兜底挡板让整个UI区域都吃掉输入二是在OnClickMove入口加前置判断鼠标悬停在Widget上时直接return// UWidgetLayoutLibrary::IsMouseOverAnyWidget(this) // 这个接口在多数UE4/UE5版本可用UI结构复杂时建议用全屏挡板方案 if (UWidgetLayoutLibrary::IsMouseOverAnyWidget(this)) { return; }判断放在射线检测之前UI上的点击一律不触发走位。注意透明挡板的层级要放在所有交互控件之下否则它会挡住按钮的点击事件。5.3 角色到达后原地抖动目标点贴NavMesh边界现象在导航边缘点击角色到达目标点后反复前后滑动像钟摆一样停不下来。原因目标点超出了NavMesh可导航范围AIController每个Tick都在重新投影目标点投影结果在边界附近来回跳动路径随之反复微调。解决三步组合。一是对Hit.Location做NavMesh投影把目标点吸附到最近的合法导航位置#include NavigationSystem.h FNavLocation ProjectedLocation; if (UNavigationSystemV1::GetCurrent(GetWorld())-ProjectPointToNavigation( Hit.Location, ProjectedLocation, FVector(100.0f, 100.0f, 100.0f))) { Hit.Location ProjectedLocation.Location; }二是把AcceptanceRadius调大至少比导航投影的误差范围大我一般用80到120。三是在OnMoveCompleted里强制StopMovementImmediately把最后一次微秒级的位移直接掐掉。5.4 移动动画方向不对速度向量与Actor朝向脱节现象角色跑步动画在向前跑但Actor的Yaw没跟随路径角色身体朝侧向或背向移动看起来像漂移。原因CharacterMovement里的bOrientRotationToMovement和Controller的bUseControllerRotationYaw都没有开启。AIController的寻路只改变移动方向不直接更改Actor自身的Yaw。解决在Character蓝图的CharacterMovement组件里勾选Orient Rotation to MovementRotation Rate设为每秒360到720度转向会跟手。同时要确保bUseControllerRotationYaw是关闭的两者不能同时开否则两个旋转源打架角色会抽搐。网络游戏场景里这个选项还需要在服务器端保持一致的配置否则服务器回放的转向跟客户端看到的不同。5.5 动态障碍物绕不过去NavMesh没有运行时更新现象场景里有个能推动的木箱移到过道中间后点击木箱后面的地面角色走到木箱面前停下不会绕。原因木箱没有标记Can Ever Affect Navigation或者项目里导航生成方式配置成了Static Only。移动物体后NavMesh不感知它的存在寻路仍然把它当空区域处理。解决打开可移动阻挡物木箱、门、升降梯的细节面板勾上Can Ever Affect Navigation。同时在项目设置里把Navigation System的Runtime Generation选为Dynamic。注意动态更新有CPU开销场景里有大量动态阻挡时不要所有物体都开动态影响用Navigation Modifier或者触发器在事件发生时局部Rebuild性能会好很多。6. 进阶技巧把点击移动手感调到商业级6.1 用AcceptanceRadius与路径偏移杜绝“最后一步打转”把AcceptanceRadius和角色胶囊体半径关联起来是个好习惯。胶囊体半径40AcceptanceRadius设70半径50设100单位都是厘米。大地图项目可以再乘一个1.2系数。发MoveToLocation前把目标点在NavMesh允许范围内随机偏移1到2米可以有效打破“目标点恰好粘在导航边界”这种抖动的触发条件代价只是角色最终停位偶尔会有小偏差对走位玩法来说完全可以接受。6.2 移动中二次点击取消旧任务与目标点重算移动过程中玩家再点旧请求会被新请求覆盖。MoveToLocation会自动取消旧请求但回调里要按RequestID过滤FAIRequestID CurrentMoveRequestID; // 发起新请求 CurrentMoveRequestID AIC-MoveToLocation(Hit.Location, 80.0f, true, true); // 回调 void AMyPlayerController::OnMoveCompleted(FAIRequestID RequestID) { if (RequestID ! CurrentMoveRequestID) { return; } // 当前目标真正到达 }还有一个细节快速连点时调用AIC-StopMovement()再MoveToLocation可以清掉旧路径避免角色在新旧路径交界处产生一帧倒退。具体原因是旧路径的最后一个路径点还在移动组件的执行队列里新命令到达后会短暂复用旧路径残留方向先停再走可以规避这个罕见问题。6.3 可视化调试用DrawDebugLine验证命中与路径把调试可视化写进OnClickMove是排查画面问题的捷径DrawDebugLine(GetWorld(), Hit.TraceStart, Hit.Location, FColor::Green, false, 2.0f, 0, 2.0f); DrawDebugSphere(GetWorld(), Hit.Location, 12, 12, FColor::Red, false, 2.0f);路径可视化更直观FNavPathSharedPtr Path AIC-GetPath(); if (Path) { const TArrayFNavPathPoint Points Path-GetPathPoints(); for (int32 i 0; i Points.Num() - 1; i) { DrawDebugLine(GetWorld(), Points[i].Location, Points[i 1].Location, FColor::Blue, false, 2.0f, 0, 3.0f); } }这能让你直接看到射线是否命中、路径是否绕开障碍、目标点是否落在NavMesh上。我自己的习惯是把这套调试代码做成一个Debug命令绑定到F9开关遇到项目里点击移动的问题就先开一遍可视化绝大多数情况一眼就能定位是射线、导航还是状态复位的问题。这个习惯帮我省下过很多次半夜排查时间希望帮到你。本文还有配套的精品资源点击获取
返回列表