
简介面向UE4/UE5开发者的“鼠标点击地面移动到指定位置”功能示例包适合正在学习游戏交互逻辑或角色移动控制的中初级开发者。资源基于蓝图实现完整覆盖射线碰撞检测、模型边缘高亮、鼠标样式自定义切换以及DoTween插件驱动的移动动画效果。整包共572个文件以413个uasset资源为主体包含14个umap关卡地图、1个uproject工程文件及配套ini/ush配置压缩后约675MB便于直接打开工程对照学习。目前已有4417人浏览学习可作为同类需求的实现参考。通过该示例可直观掌握点击寻路、高亮反馈与动画插值的关键写法即使未购买DoTween插件也可按说明删除移动方法快速迁移到自己的项目中。1. 点击地面移动这套 UE4_UE5 资源把四个技术点串成了一条完整链路鼠标点一下地面角色走过去——这个动作在 UE4_UE5 里看着简单但真把射线碰撞检测、模型边缘高亮、鼠标样式切换和 DoTween 移动动画四个环节串起来的时候我发现每一步都有坑碰撞通道没对齐点击就丢DoTween 重复触发角色会瞬移高亮材质作用域设错了整个画面泛白。这套点击地面移动资源的好处在于它把完整链路搭好了——从鼠标坐标到世界落点、从悬停反馈到平滑动画都是能直接复用的节点组合。我拆完第一反应是值得存进收藏夹适合刚入门、想做 RPG 或 MOBA 原型的人也适合想看看成熟项目里这套交互到底怎么组织的进阶开发。下文按射线 → 高亮/光标 → DoTween 的顺序把选型理由、参数设置和常见问题逐一填上。2. 射线碰撞检测把 2D 鼠标坐标变成 3D 落脚点的完整链路2.1 追踪通道与碰撞预设的选型为什么用 Visibility 而不是 Pawn鼠标点击地面移动的第一道工序是把玩家的屏幕坐标换算成世界坐标并知道它撞在了什么上面。这套资源里采用的是最常见的做法Get Hit Result Under Cursor by Channel它把 Deproject屏幕坐标反投影和 LineTraceSingleByChannel 封装在了一个节点里你在鼠标点击事件里调用它传一个追踪通道进去返回一个 Hit Result 结构体里面装着命中点、命中的 Actor 和法线方向。追踪通道的选型直接决定点击是否可靠。项目里的地面通常是 StaticMeshActor碰撞预设默认走 WorldStatic 对象通道。我一般选 Visibility 追踪通道而不是 Pawn 或 Camera。原因有两条一是 Visibility 通道默认会对场景中绝大多数阻挡物产生命中地面、墙体、障碍物都能被一条射线打中二是它的配置成本低——在 Project Settings → Collision 的 Object Channels 面板里把地面 Actor 的碰撞预设设为 Visibility 通道可命中角色和可交互物设为忽略射线就不会误伤到跟在角色身上的碰撞体。另一个常见做法是自定义对象通道在 Project Settings → Collision → Object Channels 中新增一个 Ground 通道给地面 StaticMeshActor 的碰撞预设专门绑定这个通道追踪时也选 Ground。这样做的好处是隔离强度更高——以后场景里其他无关物体无论怎么调碰撞都不会影响到地面拾取。坏处是项目里多个关卡都要维护这个预设对于原型阶段反而多了一步配置。如果你只做一个演示场景Visibility 通道足够如果你在搭正式项目的框架我建议前期就自定义 Ground 通道后面加水面、桥面这类特殊地形时能省掉不少碰撞调整。这里有个常见的误用把追踪通道设成 WorldDynamic。WorldDynamic 只对动态物体响应而大多数项目里的地板属于静态网格设成 WorldDynamic 后点击地面就完全没有反应这是“点击无效”最常见的原因之一。我在好几个项目里排查过这个问题最后发现都是新建 Actor 时默认碰撞预设没有检查射线打在了物件的碰撞边界外。2.2 命中结果解析从 Hit Result 里取位置、法线和距离Hit Result 结构体包含的信息量很大但点击移动真正用到的通常只有三个字段Location命中点世界坐标、GetActor命中到的 Actor、ImpactNormal命中点法线。Location 直接作为角色移动的目标点GetActor 用于判断命中物是不是地面ImpactNormal 用来排除墙壁、陡坡这种不能作为落点的表面。在蓝图里我会这样组织节点串[Event 鼠标左键] └→ [Get Hit Result Under Cursor by Channel] TraceChannelVisibility └→ [Break Hit Result] ├→ Hit Actor → [Branch: 是否属于地面类型] ├→ Hit Location → [设置目标位置变量 TargetLocation] └→ Impact Normal → [Dot(法线, (0,0,1)) → 判断Z值是否大于0.7]这段逻辑里有两个判断值得展开。第一个是 Hit Actor 的判断不要只判断“不等于空”要判断“是不是地面”。因为场景里可交互物体也会被 Visibility 通道命中如果你不做类型过滤点击可交互物时角色会尝试走过去这与交互逻辑冲突。常见做法是给地面做一个父类蓝图 BP_GroundBase所有地面都继承它判断时用 Cast 或者 Class 比较即可。第二个是 ImpactNormal.Z 的判断。水平地面的法线接近 (0,0,1)所以 Dot 运算后的值接近 1斜墙、陡坡的法线 Z 值会低很多。我一般设定阈值 0.7对应约 45 度坡凡是低于这个值的命中点一律不响应移动——这能防止玩家点到峭壁侧面时角色朝着墙壁方向一路狂奔。如果你希望角色能攀爬较陡的坡阈值可以放宽到 0.5如果做的是纯平面地板游戏直接设 0.95 更稳。2.3 蓝图节点串法与 C 对照可复制的实现片段如果你项目最终要转 C或者正在研读蓝图想换成代码实现可以参考下面的函数。这个函数把鼠标拾取地面点的过程完整封装了一遍bool AMyPlayerController::TraceGround(FVector OutLocation) { // 把鼠标屏幕坐标投影到世界坐标得到射线起点和方向 FVector WorldLocation, WorldDirection; if (!DeprojectMousePositionToWorld(WorldLocation, WorldDirection)) { return false; } // 射线终点从投影点沿方向延长 10000 个单位 FVector Start WorldLocation; FVector End WorldLocation WorldDirection * 10000.f; FHitResult OutHit; FCollisionQueryParams Params; Params.AddIgnoredActor(GetPawn()); // 忽略角色自己避免误命中 // 使用 Visibility 通道做单次射线检测 if (GetWorld()-LineTraceSingleByChannel(OutHit, Start, End, ECC_Visibility, Params)) { // 检查法线的 Z 分量排除陡坡和墙面 if (OutHit.ImpactNormal.Z 0.7f) { return false; } OutLocation OutHit.Location; return true; } return false; }这段代码里有三个参数值得说明。射线长度 10000 个单位在 UE 的默认尺度下能覆盖绝大多数地图尺寸但如果你在做超大地图地形尺寸超过 1km需要按世界边界重新调整否则远距离点击会拾取不到地面。AddIgnoredActor(GetPawn()) 是为了避免射线打到自己控制的角色上——当相机离角色很近、鼠标指向角色脚底时不忽略角色的话命中点会落在角色的胶囊体上而不是地面。ImpactNormal.Z 的阈值 0.7 是按经验取的对应约 45.6 度的坡度你可以按游戏设计需求调整。在蓝图里调试时我建议在命中点位置挂一个 Draw Debug Sphere 节点持续显示半秒。这样每次点击鼠标都能直观看到射线打在了哪个位置——我基本每次调这个系统都会先开调试因为光靠显示屏看不出射线路径命中的位置到底是角色脚底还是地板表面肉眼很难一次分辨。调试球可以暴露这类问题。3. 模型边缘高亮与鼠标样式切换交互反馈的双层设计3.1 从开启 Custom Depth 渲染通道到后期描边材质鼠标点击移动不只是移动本身——玩家悬停在可交互模型上时没有视觉反馈整个场景会显得很“死”。这套资源里的对应方案是模型边缘高亮技术路线是 Custom Depth-Stencil 通道加后期处理材质描边这也是 UE 里做轮廓光效的通用方案。先做两步基础设置。第一步Project Settings → Rendering → Post Processing把 Custom Depth-Stencil Pass 设为 Enabled with Stencil。如果你的项目只需要区分“可交互/不可交互”两类物体开 Enabled 就够了如果后续要按不同 Stencil 值区分多种交互类型再开 Stencil 通道。第二步在目标模型所属的网格组件StaticMeshComponent里找到 Rendering 属性把 Render Custom Depth Pass 设为 true。这一步非常关键也最容易漏——组件默认不写入 Custom Depth 通道材质端拿不到深度数据高亮就不会有任何效果。后期处理材质这边核心思路是用材质节点对比 Custom Depth 和 Scene Depth 的差异。物体边缘处的深度值会有突跳这个突跳就是描边 mask 的来源。基本材质逻辑如下[SceneTexture: CustomDepth 采样场景自定义深度] → [减去 SceneTexture: SceneDepth 得到深度差] → [Abs 绝对值 → 阈值判断0.001~0.1 区间] → [Saturate → Pow 控制边缘宽度] → [Multiply 高亮颜色] → [Add 混合到 SceneColor 输出]实际的材质节点连接我会把材质域设为 Post ProcessBlendable Location 设为 After Tonemapping。这一步很多新手会忽略默认的 Post Process 材质在 Before Tonemapping 阶段执行色调映射会把描边颜色压暗导致高亮看起来脏脏的。放到 After Tonemapping 之后描边颜色能保持原有饱和度。边缘宽度这个参数我习惯暴露成材质实例参数默认值设在 0.5~1.0 之间单位是像素级差异阈值——数值越小边缘越细。如果高亮是给“可拾取物品”用0.5 的细描边更好看如果是给“敌人目标”用建议开到 1.0 以上战斗场景里远距离就能看清目标轮廓。需要注意这套材质方案的开销集中在后处理阶段但由于实际上只对走进阈值范围的像素做计算性能开销在主流硬件上基本可以忽略。真正影响性能的是反复开关组件的 Render Custom Depth Pass——这个问题在第五章避坑部分会详细展开。3.2 鼠标样式自定义切换从默认箭头到小手的配置细节模型高亮是视觉的第一层反馈鼠标样式切换是第二层。这套资源里用的是 UMG Widget 作为自定义光标创建 UserWidget 蓝图里面放一个 Image 显示手型图标再把 Widget 通过 PlayerController 挂载为游戏光标。关键配置在 PlayerController 里。我通常在 BeginPlay 里做两件事——挂载默认光标和挂载手型光标然后在悬停检测时切换[Event BeginPlay] → [Create Widget] Cursor_Default箭头 → [Create Widget] Cursor_Hand手型 → [SetMouseCursorWidget] CursorDefault, WidgetCursor_Default → [SetMouseCursorWidget] CursorCustom, WidgetCursor_Hand → [SetMouseCursor] CursorDefaultSetMouseCursorWidget 有两个参数Cursor 类型和 Widget 实例。这里我把手型光标挂到 Custom 类型上并且调用 SetMouseCursor 把当前光标切到 Custom——这样在悬停时只需要切一次状态不用反复创建 Widget。使用 Custom 类型而不是 Hand 类型的好处是Hand 类型在某些平台输入框架下会被系统默认光标覆盖而 Custom 类型的显示路径完全由引擎控制跨平台行为更一致。有一个配置项会直接让这套方案失效Project Settings → Engine → Input → Default Classes 里的 Use Default Hardware Cursor 选项默认是 false。如果项目里被改成了 true引擎会强制使用硬件光标SetMouseCursorWidget 的软件光标不会显示——这是鼠标样式切换“看似没生效”的头号原因。排查时先看这个开关十有八九是它的问题。3.3 悬停反馈联动高亮和光标只在同一个入口触发悬停判定和点击移动的射线其实是同一个入口。我在 PlayerController 的 Event Tick 里调用一次 Get Hit Result Under Cursor by Channel同一个命中结果既给点击移动用也驱动悬停反馈。拿到 Hit Actor 后判断是否为可交互物然后分两条路处理悬停在可交互物上且刚进入状态切换手型光标 开启该 Actor 组件的 Render Custom Depth Pass高亮材质自然生效悬停离开可交互物切回默认光标 关闭 Render Custom Depth Pass高亮消失[Event Tick] → [Get Hit Result Under Cursor] (Visibility通道) → [Hit Actor 是否属于可交互目标] ├→ True → [标记当前高亮Actor] → [开启CustomDepth渲染] → [切换手型光标] └→ False → [存在当前高亮Actor?] → [关闭CustomDepth渲染] → [切换默认光标] → [状态变化时才执行不做每帧重复设置]这里的性能优化点值得单独说不要把 SetMouseCursorWidget 和 Render Custom Depth Pass 的设置在每一帧都执行。用一个 bool 变量记录当前悬停状态只在状态翻转时执行切换。否则每帧都在改渲染状态不仅拖慢性能还可能造成描边闪烁——后期材质读到 CustomDepth 时组件开关瞬间产生的深度突变会被当作边缘识别出来画面会闪一下才恢复。另外一个小技巧如果场景里存在多个可交互物体切换高亮前记得先把上一个高亮目标关掉否则会出现两个物体同时亮着的状态。这也是很多点击移动 Demo 里没处理干净的问题——高亮入口分散在多个事件里导致高亮对象的管理混乱。统一收口到某一个入口点只维护一个“当前高亮Actor”变量问题就消失了。4. DoTween 移动动画让角色从 A 点到 B 点平滑过渡4.1 DoTween 插件接入导入、授权和基本配置点击位置拿到手的瞬间移动的核心问题出现角色不能瞬移过去必须平滑地走过去。这套资源采用 DoTween 插件它是付费插件但如果不购买把蓝图里的 DoTween 节点删掉换成 Timeline 也能运行。接入过程不复杂启用插件后在蓝图里搜 DO 前缀的节点即可。它把缓动曲线、暂停恢复、完成回调都封装成了现成节点省去了自己维护时间轴的麻烦。先看角色蓝图里的基本用法[Event 鼠标左键点击命中地面] → [设置目标位置变量 TargetLocation] → [DO Move] Target角色的Root, EndValue(TargetX, TargetY, 当前Z), Duration0.35, EasingOutQuad, Snapfalse → [DO LookAt] Target角色Mesh, Toward移动方向, Duration0.2这里的 Target 参数决定了移动生效的对象。我建议让 DoTween 移动角色的 Root Component而不是直接驱动 CharacterMovement。CharacterMovement 是物理驱动的移动组件如果直接调用它的 Move 接口速度和物理碰撞的计算会叠加上去角色移动时肯能抖动。正确做法是把移动的“所有权”在动画期间交给 DoTween移动开始前把 CharacterMovement 的 MovementMode 设为 None或者用一个 IsMoving 布尔变量控制输入转发——Tick 里只有 IsMoving 为 false 时才把输入发给 CharacterMovement。移动完成回调里恢复 MovementMode 为 Walking这样物理和补间动画不会同时抢角色位置。4.2 DOMove 参数拆解时长、缓动曲线和转身DOMove 的参数有四组值得研究Duration、Easing、Delay 和 Snap。Delay 在点击移动场景里几乎用不到——玩家点下去就希望角色马上动。Snap 参数是控制是否对齐整数坐标游戏里一般不勾选。真正要调的是 Duration 和 Easing 这两个。Duration 我强烈建议做成距离相关而不是固定值。固定 0.35 秒会导致远距离移动速度忽快忽慢——近距离像是在瞬移远距离又显得磨蹭。常用改法是Duration 基础时长 * (目标距离 / 参考距离)基础时长设 0.35 秒对应参考距离 2000 个单位距离翻倍时长跟着翻倍角色移动的线速度就恒定了。这个公式在蓝图里就是两个节点的事先用 Distance 节点算出起点到目标点的距离再除以参考距离乘上基础时长输出接到 DOMove 的 Duration。Easing 曲线方面移动推荐 OutQuad 或 OutCubic。这两个曲线都是开头快、结尾慢角色起步时加速感明显临近目标又平稳停下视觉上最符合“走路”的体感。不要用 Linear除非你想做机器人那种匀速平移的效果。转身方向的反控则用 DOLookAt关键参数是朝向向量——Toward 传入的必须是目标位置减去当前位置得到的向量方向错了角色会背对移动方向行走这个问题在第五章有详细展开。4.3 没有 DoTween 时的降级方案删掉节点换回 Timeline如果你没买 DoTween 插件资源包注释也写了——把蓝图中 DoTween 的移动方法删除掉即可。降级方案最常用的是 UE 自带的 Timeline 节点[Event 鼠标左键点击命中地面] → [K2_ClearTimeline] 清除旧Timeline → [Timeline] 时长0.35秒曲线选OutQuad ├→ [Lerp] 起点位置 → 目标位置每帧更新 → [SetActorLocation] → [Timeline Finished] → [清理Velocity恢复输入]用 Timeline 的麻烦在于自己管理时间轴角色移动到一半被再次点击需要先停止旧的 Timeline 再启动新的。DoTween 插件自动做的就是这个管理。降级方案要实现同样的效果必须在移动开始时记录起点位置并且每次新点击时把起点重置为当前位置否则会从旧起点重新 Lerp角色视觉上会往后退一下。在实际项目中还有另一种替代方案如果你不想用 Timeline——直接在 Tick 里用 FInterp To 插值位置。它比 Timeline 更轻量缺点是缺乏曲线控制。Te 接 Distance 和时间比例可以实现线性插值但想要 OutQuad 这类缓动曲线还是要自己算式。我在这类项目里的习惯是如果只是演示功能用 Timeline 或者 FInterp To 就够如果要在正式游戏里做大量移动叙事或连续点击直接买 DoTween。从效率看插件在蓝图上省下的节点数量不是一点半点。5. 避坑指南点击移动项目里五个最容易翻车的细节5.1 鼠标点击完全没反应碰撞预设与追踪通道不匹配现象鼠标点击地面角色毫无反应但射线追踪节点确认有输出。原因最常见的是碰撞通道错配。如果你把追踪通道设成了 WorldDynamic而地面是静态网格射线根本不会命中静态物体另一种情况是地面 Actor 的碰撞预设里Response to Visibility 被设成了 Ignore 或者 Block 没勾选——注意 Block 和 Overlap 的区别射线追踪要的是 Block 响应。解决在 Project Settings → Collision 里检查地面 Actor 的碰撞预设确保 Visibility 通道的响应是 Block同时把追踪通道统一改为 Visibility 或自定义 Ground 通道。改完最好在命中点加 Draw Debug Sphere 显示一次确认射线实际打在什么位置。5.2 角色瞬移而不是平滑移动DoTween 重复触发导致动画覆盖现象连续快速点击两次地面角色不是走过去而是直接闪现在第二个目标点。原因DoTween 的 DOMove 每次被调用都会创建一个新的补间如果没有先 Kill 掉上一次的补间两个补间同时作用于同一个位置数据后一个会把前一个覆盖。Timeline 方案也有同样问题——旧的 Timeline 还在跑新的又重新 Lerp最终位置变成了第二次的终点。解决在任何移动触发之前先调用 DoTween 的 Kill 节点或者用 SetActive(false) 停掉旧的 Timeline。推荐做法是维护一个“当前移动Tween”的引用每次新移动时先 Kill 旧的。代码层面C 里的写法是保存当前 Tween 句柄创建新 Tween 前调用它的 Kill 方法。5.3 高亮描边闪烁或整个画面泛白Custom Depth 作用域和材质混合出问题现象角色悬停在模型上描边效果闪烁不定更严重的时候整个画面像是蒙了一层白雾。原因两个问题叠加。第一个是 Render Custom Depth Pass 在每帧频繁开关后期材质把开关瞬间的深度突变识别成了边缘第二个是后期处理材质的 Blendable Location 设置错误——如果放在 Before Tonemapping高亮颜色经过色调映射后会泛白。解决高亮状态切换只做一次不做每帧重复操作用布尔变量做状态锁材质 Blendable Location 改为 After Tonemapping。如果画面仍然泛白检查材质输出节点是否直接乘了 SceneColor——描边材质应该用 Add 节点做增量混合而不是替换整个画面。5.4 鼠标光标切换失灵Use Default Hardware Cursor 这个开关现象SetMouseCursorWidget 已经调用了手型光标就是显示不出来关闭后又能显示。原因Project Settings → Engine → Input → Default Classes 里的 Use Default Hardware Cursor 被设成了 true。这个选项一旦开启引擎会绕过软件光标渲染直接使用操作系统硬件光标UMG Widget 光标就无法生效。解决把 Use Default Hardware Cursor 设为 false。同时注意HoloLens 等 MR 项目里硬件光标路径不同如果你在做多平台项目需要针对 Windows 和移动端分别验证光标表现。5.5 角色移动到了点但朝向不对DOLookAt 的方向向量算错现象角色最终站在目标点上但面朝方向完全不对有时候背对目标有时候垂直于移动路径。原因DOLookAt 的 Toward 参数需要的是方向向量不是目标点坐标。如果直接把 TargetLocation 传进去相当于把目标点当作了方向距离越远偏差越大。还有一种情况是 Transform 空间没选对——Local Space 和 World Space 混用会导致朝向计算错乱。解决在调用 DOLookAt 前用 GetActorLocation 和 TargetLocation 先做一次减法再 Normalize得到移动方向向量传进去。Actor 蓝图里还需要确认 LookAt 的目标是 Mesh 而不是 Root——如果对 Root 做旋转胶囊体转向的同时 Mesh 可能因为动画系统有自己的朝向而再次回正。6. 进阶验证点击移动系统的四步清单与参数调整习惯6.1 修改后必查的四步清单点击移动系统跨了碰撞、渲染、输入、动画四个模块任何一个参数动了都可能互相影响。我给自己定了一个四步验证流程每次改完必跑一遍验证步骤检查项预期结果第一步鼠标坐标屏幕四个角落和边缘点击命中点不偏移Debug 球贴合地面第二步射线命中点击平地、斜坡、墙根平地可移动斜坡和墙根被拒绝第三步动画触发连续点击两个远处目标角色平滑走向第二目标无瞬移第四步状态反馈悬停可交互物和离开光标切换一次高亮无闪烁这套流程看起来简单但每次调试都能抓到问题。尤其是第四步——高亮闪烁这类渲染问题在静态截图里看不出来必须运行状态下观察。另外调试时建议把 TraceGround 里的射线长度临时改成 1000用短射线快速验证近距离命中确认无误再改回 10000。6.2 参数调优的经验值和告别翻车的肌肉记忆关于这套系统的参数我把常用的稳定值整理如下射线长度 10000、ImpactNormal.Z 阈值 0.7、移动基础时长 0.35 秒、参考距离 2000、DOLookAt 时长 0.2 秒、描边宽度 0.8。这些值在大多数场景里都能出不错的效果。调参数时最需要注意的是 Duration 和 Easing 的联动——如果你把 Easing 从 OutQuad 换成 Linear0.35 秒的体感会明显变快因为 OutQuad 的减速段占了整个时长的一半以上。我一般先定 Easing再根据实际走路体感调 Duration两个参数别同时乱改。最后说一个我踩过的坑有一次在项目里做点击移动调好了鼠标点击、调好了高亮、调好了动画却忘了验证玩家按住鼠标左键拖动的情况。结果玩家按住左键拖动鼠标角色跟着光标满屏乱跑。从那以后我每次做点击移动系统都强制在验证清单里加上一条“按住拖拽时角色不应该跟随移动”——这个习惯救了我好几次。希望这份拆解能帮你把这套资源用得更顺少走我走过的弯路。本文还有配套的精品资源点击获取