
1. 项目概述为什么3D拾取是UE5开发者的必修课在虚幻引擎5UE5的世界里无论是制作一款第一人称射击游戏让玩家能精准地捡起地上的武器还是开发一个建筑可视化应用让用户能点击并旋转查看模型其背后都离不开一个核心交互机制3D拾取。简单来说3D拾取就是回答“玩家在屏幕上点击的这个像素点到底对应着3D世界里的哪个物体”这个问题。听起来似乎很基础但它的实现质量直接决定了交互的流畅度和沉浸感。一个响应迟钝、判断不准的拾取系统足以毁掉一个精心打磨的体验。很多刚接触UE5的开发者尤其是从蓝图可视化编程入门的可能会直接使用现成的节点比如LineTraceByChannel感觉“一键搞定”。但当你需要定制拾取范围、处理复杂的UI遮挡、或者优化大规模场景下的拾取性能时就会一头雾水。问题的根源往往在于对底层坐标转换流程的“黑盒”操作。你不清楚屏幕上的一个点是如何一步步“穿越”到3D世界并与物体发生碰撞检测的。这就是我们这次动手实践的核心价值所在。我们不满足于“知其然”更要“知其所以然”。我们将从UE5提供的几个核心坐标转换函数如DeprojectScreenToWorld出发彻底拆解从2D屏幕空间到3D世界空间的完整转换链条。然后我们将用两种最主流的方式——纯C和蓝图C混合——来亲手实现一个功能完整、逻辑清晰的简易拾取Demo。无论你是偏爱代码控制的程序还是习惯可视化逻辑的设计师都能从中获得扎实的、可复用的知识。这个Demo将涵盖鼠标点击拾取、高亮反馈、信息显示等完整环节让你不仅理解原理更能立刻应用到自己的项目中。2. 核心原理拆解屏幕上的一个点如何找到3D世界中的它在开始写代码之前我们必须像解一道数学题一样把“屏幕坐标到3D拾取”这个问题的解题步骤理清楚。这个过程可以形象地理解为一次“空间穿越”总共分三步走。2.1 第一步从屏幕像素到标准化设备坐标NDC当我们点击屏幕时操作系统或游戏引擎会给我们一个坐标比如(MouseX, MouseY)。这个坐标的单位是像素其原点(0, 0)通常在屏幕的左上角。但是不同的玩家可能有不同的屏幕分辨率1920x1080, 2560x1440等直接用像素坐标进行计算是不通用的。因此第一步是将其标准化。我们需要将这个像素坐标转换到标准化设备坐标空间。在这个空间里无论屏幕分辨率如何其坐标范围在X和Y轴上都是[-1, 1]。左下角是(-1, -1)右上角是(1, 1)。转换公式很简单NDC_X (MouseX / ViewportWidth) * 2 - 1NDC_Y 1 - (MouseY / ViewportHeight) * 2// 注意Y轴方向反转因为屏幕坐标原点在左上而NDC原点在左下。注意在UE5的视口Viewport和投影矩阵体系中Y轴默认是向上的。但屏幕像素坐标的Y轴是向下的所以这里需要进行1 - ...的操作来进行反转这是初学者最容易忽略导致拾取射线方向错误的一个点。2.2 第二步从NDC到视图空间View Space的射线有了NDC坐标(NDC_X, NDC_Y)我们就可以在近裁剪平面Near Clip Plane和远裁剪平面Far Clip Plane上各确定一个点。在NDC空间中近裁剪平面的Z值通常是-1在DirectX风格的API中或-1在OpenGL风格中UE5内部使用前者远裁剪平面的Z值是1。通过摄像机的投影矩阵的逆矩阵我们可以将这两个NDC点转换到视图空间也叫相机空间。在这个空间里摄像机位于原点看向Z轴正方向或负方向取决于坐标系定义UE5中摄像机看向X轴正方向但概念相通。这一步计算后我们得到的是从摄像机原点出发指向屏幕上点击点的方向向量在视图空间中的表示。这里有一个关键理解我们并不直接得到一个“世界空间”的点而是先得到在摄像机坐标系下的射线。这条射线的起点是摄像机原点视图空间原点方向是通过远、近裁剪平面上的点计算出来的向量。2.3 第三步从视图空间到世界空间并执行碰撞检测最后一步使用摄像机的视图矩阵的逆矩阵在UE5中通常就是摄像机的GetActorTransform或其旋转信息将视图空间中的射线起点和方向转换到世界空间。至此我们得到了一条在世界坐标系下的射线Ray_Start和Ray_Direction。这条射线就代表了从摄像机镜头中心出发穿过屏幕上你点击的那个像素射向3D世界无穷远处的一条线。接下来的事情就交给了碰撞系统。我们使用这条射线对场景进行线性检测。在UE5中最常用的就是UWorld::LineTraceSingleByChannel函数。它会沿着射线方向检测返回第一个被击中的物体如果存在并告诉我们击中的位置、法线、击中的组件等详细信息。这个被击中的物体就是我们要“拾取”的对象。整个流程的核心函数UE5非常贴心地为我们封装了上述大部分计算过程。APlayerController类中的DeprojectScreenPositionToWorld函数或类似功能的工具函数其内部就是完成了从屏幕像素到世界空间射线起点和方向的转换。我们本次Demo会深入使用并解析它。3. 开发环境准备与项目创建工欲善其事必先利其器。在开始编码前确保你的环境是就绪的。3.1 软件与环境配置虚幻引擎5建议使用5.2或以上版本。可以从Epic Games Launcher下载安装。Visual Studio 2022这是UE5 C开发的首选IDE。安装时务必勾选“使用C的游戏开发”工作负载以确保包含必要的Windows SDK和编译工具。项目模板选择打开UE5选择“游戏”类别然后选择“空白”模板。这样可以从零开始不受其他模板预设逻辑的干扰。给项目起一个清晰的名字例如PickupDemo。启用开发者模式在编辑器的“编辑”-“编辑器偏好设置”-“通用”-“源代码”中确保“源代码编辑器”已设置为Visual Studio。3.2 创建第一个C类我们将创建一个用于处理拾取逻辑的Actor组件。使用组件模式而非直接继承Actor是为了让拾取功能可以灵活地附加到任何角色如玩家控制的Pawn或摄像机身上复用性更高。在内容浏览器中右键点击空白处选择“新建C类”。在类类型中选择“Actor组件”命名为ObjectPickerComponent。点击创建UE5会自动生成.h和.cpp文件并打开Visual Studio。实操心得对于工具类、管理器或像拾取器这样的功能模块优先考虑继承自UActorComponent或UObject而不是AActor。这符合“组合优于继承”的设计原则能让你的游戏架构更清晰、更易维护。一个AActor应该代表一个具体的游戏实体如敌人、宝箱而功能则通过组件挂载上去。4. C核心实现构建ObjectPickerComponent我们将完全在C中实现拾取逻辑这能让我们对底层有最强的控制力并便于后续做性能优化或复杂功能扩展。4.1 头文件 (ObjectPickerComponent.h) 设计与声明首先我们定义组件的属性和方法。// ObjectPickerComponent.h #pragma once #include CoreMinimal.h #include Components/ActorComponent.h #include ObjectPickerComponent.generated.h // 声明一个动态多播委托用于当拾取到物体时广播事件。 // 参数被击中的Actor击中点信息。 DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnObjectPicked, AActor*, PickedActor, const FHitResult, HitResult); UCLASS(ClassGroup(Custom), meta(BlueprintSpawnableComponent)) class PICKUPDEMO_API UObjectPickerComponent : public UActorComponent { GENERATED_BODY() public: // 构造函数 UObjectPickerComponent(); protected: // 游戏开始时调用 virtual void BeginPlay() override; public: // 每帧调用 virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override; // 执行一次拾取检测 UFUNCTION(BlueprintCallable, Category ObjectPicker) void PerformPick(); // 拾取距离射线长度 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category ObjectPicker) float PickDistance 10000.0f; // 默认100米 // 用于检测的碰撞通道 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category ObjectPicker) TEnumAsByteECollisionChannel TraceChannel ECC_Visibility; // 拾取成功事件委托 UPROPERTY(BlueprintAssignable, Category ObjectPicker) FOnObjectPicked OnObjectPicked; private: // 内部方法获取玩家控制器和视口信息 APlayerController* GetPlayerController() const; };关键点解析FOnObjectPicked委托这是UE5中一种强大的事件通信机制。我们将拾取逻辑“怎么做”和拾取后的反应“做什么”解耦。组件只负责检测并触发事件至于是高亮模型、显示UI还是播放声音由绑定到这个委托的其他蓝图或C逻辑来决定。这大大提升了组件的通用性。PickDistance这是一个公开变量并标记为EditAnywhere和BlueprintReadWrite意味着你既可以在C代码里修改它也可以在编辑器细节面板中直接调整或者在蓝图中读写。将它暴露出来方便设计者根据游戏场景大小进行调整。TraceChannel碰撞通道决定了射线与哪些物体发生交互。默认使用ECC_Visibility可见性通道它通常与场景中阻挡视线的物体如墙壁、复杂模型相关联。你可以根据需求改为ECC_WorldStatic静态物体或ECC_WorldDynamic动态物体甚至自定义通道。4.2 源文件 (ObjectPickerComponent.cpp) 实现逻辑现在我们来填充核心逻辑。// ObjectPickerComponent.cpp #include ObjectPickerComponent.h #include Engine/World.h #include GameFramework/PlayerController.h #include Engine/Engine.h // 用于调试绘制 UObjectPickerComponent::UObjectPickerComponent() { PrimaryComponentTick.bCanEverTick true; // 启用每帧Tick } void UObjectPickerComponent::BeginPlay() { Super::BeginPlay(); } void UObjectPickerComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) { Super::TickComponent(DeltaTime, TickType, ThisTickFunction); // 通常拾取不需要每帧执行而是由输入事件触发。这里Tick留空或可用于持续高亮等高级功能。 } APlayerController* UObjectPickerComponent::GetPlayerController() const { // 获取拥有这个组件的Actor AActor* Owner GetOwner(); if (!Owner) { return nullptr; } // 尝试从Owner获取玩家控制器 // 如果组件是附加在Pawn或Character上这通常有效 APlayerController* PC CastAPlayerController(Owner-GetInstigatorController()); if (PC) { return PC; } // 如果上述方法失败尝试获取世界中的第一个本地玩家控制器适用于单玩家情况 UWorld* World GetWorld(); if (World) { return World-GetFirstPlayerController(); } return nullptr; } void UObjectPickerComponent::PerformPick() { APlayerController* PC GetPlayerController(); if (!PC) { UE_LOG(LogTemp, Warning, TEXT(ObjectPickerComponent: Failed to get PlayerController!)); return; } // 1. 获取鼠标光标在屏幕上的位置像素坐标 float MouseX, MouseY; if (!PC-GetMousePosition(MouseX, MouseY)) { // 如果获取失败例如在游戏模式下鼠标被锁定可以尝试使用视口中心 int32 ViewportSizeX, ViewportSizeY; PC-GetViewportSize(ViewportSizeX, ViewportSizeY); MouseX ViewportSizeX / 2.0f; MouseY ViewportSizeY / 2.0f; } // 2. 核心步骤将屏幕坐标反投影到世界空间得到射线起点和方向 FVector WorldLocation; // 射线起点实际上在近裁剪面附近 FVector WorldDirection; // 射线方向单位向量 bool bDeprojectSuccess PC-DeprojectScreenPositionToWorld( MouseX, MouseY, WorldLocation, WorldDirection ); if (!bDeprojectSuccess) { UE_LOG(LogTemp, Error, TEXT(ObjectPickerComponent: DeprojectScreenPositionToWorld failed!)); return; } // 3. 计算射线终点 FVector TraceStart WorldLocation; FVector TraceEnd TraceStart (WorldDirection * PickDistance); // 4. 准备并执行线性检测射线检测 FHitResult HitResult; FCollisionQueryParams CollisionParams; CollisionParams.bTraceComplex true; // 进行复杂碰撞检测精度更高 CollisionParams.bReturnPhysicalMaterial false; // 本例不需要物理材质 // 可以添加忽略自身if(GetOwner()) CollisionParams.AddIgnoredActor(GetOwner()); UWorld* World GetWorld(); bool bHit false; if (World) { bHit World-LineTraceSingleByChannel( HitResult, TraceStart, TraceEnd, TraceChannel, CollisionParams ); } // 5. 处理检测结果 if (bHit HitResult.GetActor()) { AActor* HitActor HitResult.GetActor(); UE_LOG(LogTemp, Log, TEXT(Picked Actor: %s at Location: %s), *HitActor-GetName(), *HitResult.Location.ToString()); // 6. 广播拾取事件 OnObjectPicked.Broadcast(HitActor, HitResult); // 【可选】调试绘制在编辑器和独立游戏中可视化射线和击中点 #if ENABLE_DRAW_DEBUG if (World) { DrawDebugLine(World, TraceStart, TraceEnd, FColor::Green, false, 2.0f, 0, 1.0f); DrawDebugPoint(World, HitResult.Location, 10.0f, FColor::Red, false, 2.0f); } #endif } else { UE_LOG(LogTemp, Log, TEXT(No actor picked.)); // 即使未命中也可以广播一个空事件或不同的事件取决于需求。 } }代码逐段解析与避坑指南获取鼠标位置GetMousePosition在游戏窗口有焦点时通常有效。但在某些全屏或鼠标被锁定的游戏模式下如第一人称射击它可能失败。我们的代码提供了备选方案使用视口中心。这是一种常见的“屏幕中心准星”拾取模式。你可以根据游戏类型通过一个bool变量让设计者选择模式。核心函数DeprojectScreenPositionToWorld这是UE5封装好的坐标转换“黑盒”。它内部完成了我们原理部分讲的所有矩阵运算直接输出了世界空间下的射线起点和方向。这是实现拾取最简洁、最推荐的方式。它的参数顺序是 (屏幕X, 屏幕Y, 输出世界起点, 输出世界方向)。碰撞检测参数FCollisionQueryParamsbTraceComplex设置为true会使用网格体的复杂碰撞即渲染网格本身进行检测精度最高但性能开销稍大。设置为false则使用简单的碰撞体如球体、胶囊体、盒子性能更好。对于需要精确拾取模型表面的场景如射击游戏打弱点必须开启。AddIgnoredActor非常实用例如当拾取组件附属于玩家角色时你肯定不希望射线检测到自己。这时就可以在CollisionParams中忽略GetOwner()。这在实现“拾取自身以外的物体”时是必须的。调试绘制DrawDebugLine/Point用#if ENABLE_DRAW_DEBUG宏包裹这是一个好习惯。它确保调试绘制只在开发版本非Shipping发行版中编译和执行避免影响最终游戏的性能。绿色的线是射线路径红色的点是击中点在测试时一目了然。事件广播OnObjectPicked.Broadcast这是整个组件的“输出接口”。C部分的工作到此为止。它告诉外界“嘿我捡到了一个东西它是HitActor撞在HitResult这个地方了”。至于外界是播放音效、显示名字还是让物体飞起来组件完全不关心。5. 蓝图集成将C功能可视化与调用C组件提供了强大的底层能力而蓝图则提供了快速的逻辑编排和迭代能力。接下来我们创建一个蓝图来使用这个组件。5.1 创建玩家角色蓝图并添加组件在内容浏览器中右键选择“蓝图类”。在父类搜索框中搜索“Character”选择它作为父类命名为BP_PickupCharacter。双击打开BP_PickupCharacter。在“组件”面板中点击“添加组件”搜索我们刚创建的ObjectPickerComponent将其添加到角色中。选中添加的ObjectPickerComponent在细节面板中你可以看到我们暴露的变量Pick Distance和Trace Channel可以在这里直接修改。5.2 设置输入映射与调用拾取打开“项目设置”-“引擎”-“输入”。在“操作映射”中添加一个新的映射命名为“PickObject”并为其分配一个按键例如鼠标左键Left Mouse Button。回到BP_PickupCharacter的事件图表。右键搜索“输入动作 PickObject”取决于你的命名添加“Pressed”和“Released”事件。从“Pressed”事件引脚拖出搜索“Perform Pick”这是我们C中标记为BlueprintCallable的函数调用它。这个节点应该能在ObjectPickerComponent组件下找到或者直接从组件引用拖出。现在运行游戏按下鼠标左键你就能在输出日志中看到拾取到的Actor名称和位置信息了。如果开启了调试绘制还能看到绿色的射线。5.3 绑定拾取事件并实现高亮反馈光有日志还不够我们需要一个视觉反馈。常见的做法是改变被拾取物体的材质使其高亮。首先我们需要一个高亮材质。可以在材质编辑器中创建一个简单的“自发光”材质颜色设为亮黄色或红色命名为M_Highlight。在BP_PickupCharacter的事件图表中我们需要处理ObjectPickerComponent的On Object Picked事件。获取ObjectPickerComponent的引用。右键点击该引用选择“绑定事件到 On Object Picked...”。这会自动创建一个自定义事件节点并带有Picked Actor和Hit Result参数。在这个自定义事件后我们可以编写高亮逻辑目标Picked Actor。思路获取Actor的静态网格组件Get Component by Class-StaticMeshComponent然后动态创建一个材质实例Create Dynamic Material Instance并将其设置为网格体的材质Set Material。更健壮的做法考虑到一个Actor可能有多个网格组件或者我们可能想高亮所有组件。我们可以使用Get Components by Class节点获取所有StaticMeshComponent然后遍历数组For Each Loop对每一个组件应用高亮材质。一个关键问题如何取消高亮我们不能让物体永远高亮。通常有两种策略策略A每次拾取新物体时取消上一个物体的高亮。这需要在蓝图中保存一个对“上一个被拾取Actor”的引用变量LastPickedActor。在触发新的On Object Picked事件时先检查LastPickedActor是否存在且有效如果存在就将其材质恢复为原始材质这要求你事先保存了原始材质引用然后再高亮新的物体并更新LastPickedActor。策略B设置一个定时器在一段时间后自动取消高亮。这可以通过在应用高亮材质后设置一个延迟Delay节点然后在延迟后恢复材质来实现。注意事项动态创建材质实例Create Dynamic Material Instance是为了不影响材质资产本身并且允许我们在运行时修改材质参数如高亮颜色、强度。如果你只是简单替换材质可以直接使用Set Material。但动态实例化是更灵活、更标准的做法。6. 纯蓝图实现方案理解Deproject Screen To World节点为了更透彻地理解坐标转换我们完全在蓝图中复现一遍这个过程。这能让你在没有C组件的情况下也能实现拾取功能并且直观地看到每一步的数据流。6.1 蓝图节点拆解在蓝图中核心的转换节点是Deproject Screen To World属于Player Controller。它的功能与C中的DeprojectScreenPositionToWorld完全一致。获取玩家控制器使用Get Player Controller节点索引0通常指第一个本地玩家。获取鼠标位置使用Get Mouse Position节点输出屏幕X和Y。反投影将鼠标X、Y连接到Deproject Screen To World节点的Screen X/Y输入。该节点输出World Position射线起点和World Direction射线方向。计算射线终点使用一个Vector * Float节点将World Direction乘以一个距离如5000再用一个Vector Vector节点将结果与World Position相加得到Trace End。执行射线检测使用Line Trace By Channel节点。输入Start和End选择碰撞通道如Visibility。输出Out Hit结构体里面包含了是否命中bBlockingHit、命中的ActorHit Actor、命中位置Location等所有信息。6.2 蓝图与C方案的对比特性C (ObjectPickerComponent) 方案纯蓝图方案性能更优。逻辑在C中编译执行无蓝图虚拟机开销尤其适合每帧执行的密集检测。良好。对于由输入事件触发的拾取性能差异通常可忽略不计。灵活性高。易于扩展复杂算法如扇形检测、抛物线检测方便集成到大型C框架中。非常高。可视化连线迭代速度快逻辑一目了然非常适合原型设计和快速验证。可维护性较高。逻辑集中在一个类中但需要编译。适合团队中有程序员的项目。高。对于小型功能或独立设计师蓝图更直观易懂。但复杂后可能产生“面条图”。复用性极高。组件可拖放到任何Actor上参数可调事件驱动是标准的模块化设计。中等。需要复制蓝图节点或创建蓝图函数库/宏来复用。理解深度需要理解C类、委托等概念但能触及引擎底层API。直观展示数据流非常适合理解坐标转换的每一步。个人建议对于核心游戏系统、需要高性能或复杂计算的模块如武器系统、技能系统使用C组件是更专业的选择。对于一次性原型、关卡设计中的特殊交互、或者团队中缺少程序员的情况纯蓝图方案是强大且高效的工具。最佳实践往往是混合使用用C编写基础、高效、可复用的功能组件如我们的ObjectPickerComponent然后用蓝图来配置参数、编排事件响应如播放动画、显示UI。7. 高级话题与性能优化一个基础的拾取Demo完成后我们可以思考如何让它更健壮、更高效。7.1 处理UI遮挡在游戏中UI如血条、背包界面通常会覆盖在3D场景之上。当你点击UI时通常不希望触发背后的3D物体拾取。UE5提供了完善的机制来处理这个问题。检查光标是否在UI上APlayerController有一个函数GetHitResultUnderCursorForObjects但它主要用于UI。更常用的方法是利用Slate应用程序的路径。使用FSlateApplication在C中可以调用FSlateApplication::Get().IsCursorDirectlyOverSlateWidget()来检查光标是否在任何一个Slate控件包括UMG控件上。如果是则跳过3D拾取检测。蓝图中的简单方法在触发拾取前可以先检测是否有可交互的UI控件处于焦点状态。或者在设计UMG时确保交互式UI控件会阻塞鼠标事件向下传递。7.2 优化大规模场景拾取当场景中有成千上万个物体时对每一条射线都进行LineTraceSingleByChannel它默认会检测场景中所有该通道的物体可能会成为性能瓶颈。使用更精确的碰撞通道不要对所有物体都使用ECC_Visibility。为可拾取物体专门设置一个自定义碰撞通道比如ECC_GameTraceChannel1并只在必要的物体上启用该通道的碰撞响应。这样射线检测的范围就大大缩小了。空间划分与查询优化对于极端密集的物体可以考虑使用LineTraceSingleByObjectType可以指定一个对象类型列表进一步过滤。使用Overlap检测代替LineTrace如果你只需要检测鼠标“指向”的物体而不是精确的碰撞点可以先在摄像机前做一个小的球体或胶囊体重叠检测快速筛选出附近的物体再对筛选出的少数物体进行精确的射线检测。集成空间索引对于自己管理的可拾取物体列表可以使用四叉树、八叉树或网格系统进行空间管理只检测鼠标方向一定锥形区域视锥体内的物体。7.3 从屏幕到世界的其他应用理解屏幕到世界的转换其应用远不止于物体拾取拖拽3D物体拾取后在鼠标移动事件中持续将新的屏幕位置反投影到世界空间可以计算出一个基于鼠标移动的偏移量从而实时更新被拖拽物体的位置。地面点击移动RTS游戏玩家点击地面需要将屏幕坐标转换为地面一个特定Z值的平面上的世界坐标。这可以通过计算射线与水平平面的交点来实现。构建/放置系统在建造游戏中随着鼠标移动预览建筑模型需要始终“贴”在地面或其他物体表面。这需要结合射线检测和表面法线来调整预览模型的旋转和位置。UI与3D场景的关联例如在3D角色头上显示一个血条UI。你需要将角色的世界坐标GetActorLocation 一个偏移量通过摄像机的投影矩阵转换到屏幕坐标然后把这个屏幕坐标赋值给UI控件的位置。8. 常见问题排查与调试技巧在实际开发中你肯定会遇到拾取失灵、位置不准等问题。下面是一个快速排查清单问题现象可能原因解决方案点击没有任何反应日志无输出。1. 输入映射未设置或按键错误。2.ObjectPickerComponent没有添加到Pawn上或Pawn不是被玩家控制的。3.GetPlayerController()返回空。1. 检查项目设置的输入映射。2. 确保组件已添加且Pawn的“Auto Possess Player”设置为“Player 0”。3. 在PerformPick函数开始处添加UE_LOG打印PC是否有效。射线似乎从奇怪的地方发出或者方向不对。1.DeprojectScreenPositionToWorld使用的屏幕坐标不正确如鼠标未锁定时的坐标获取问题。2. 没有正确处理视口中心模式。1. 打印出MouseX, MouseY的值看是否在预期范围内0到屏幕宽高。2. 启用调试绘制 (DrawDebugLine)直观查看射线路径。确保WorldDirection是单位向量长度≈1。能检测到物体但高亮材质没有应用。1. 蓝图事件绑定不正确。2. 被拾取的Actor没有StaticMeshComponent。3. 材质索引设置错误或动态材质实例创建失败。1. 检查On Object Picked事件是否被正确绑定和触发。2. 使用Get Components by Class (StaticMeshComponent)并遍历。3. 尝试先应用一个简单的静态材质排除材质本身的问题。射线穿过了物体或者必须在很近的距离才能拾取。1. 物体的碰撞体Collision未正确设置或过于简单。2.LineTraceSingleByChannel的碰撞通道不匹配。3.PickDistance设置得太小。1. 在静态网格体编辑器中检查碰撞复杂度或为其生成/指定一个合适的碰撞体。2. 确保物体的碰撞预设Collision Presets对你使用的TraceChannel有“阻挡”Block响应。3. 适当增大PickDistance。在打包后的游戏中调试绘制不显示。ENABLE_DRAW_DEBUG宏在Shipping构建中默认是关闭的。调试绘制仅用于开发。正式逻辑不应依赖它。如果需要持久可视化应创建实际的调试Actor或粒子效果。一个实用的调试技巧在PerformPick函数中无论是否命中都强制绘制一条从摄像机到远处点的调试线。这能让你立刻看清射线的方向和范围是排查坐标问题最快的方法。