多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

UE5 C++项目创建与蓝图协作实战指南

UE5 C++项目创建与蓝图协作实战指南 1. 项目概述从蓝图到C的跨越如果你已经用UE5的蓝图系统捣鼓过一阵子做出了几个会动的小人或者能开关的门心里可能会开始痒痒那些更复杂的功能、更高的性能、更优雅的代码结构是不是得靠C来实现答案是肯定的。今天我们就来亲手创建你的第一个UE5 C项目这不仅是多了一个文件那么简单而是意味着你正式踏入了Unreal Engine核心开发的大门。C在UE5中扮演着“骨骼”和“肌肉”的角色它定义了游戏对象最根本的行为逻辑和数据结构而蓝图则是附着其上的“皮肤”和“神经”负责快速迭代和可视化交互。对于有志于开发高性能游戏、复杂系统或者希望深入引擎底层机制的开发者来说掌握C是必经之路。这个教程适合所有已经熟悉UE5编辑器基本操作并对C语法有初步了解至少知道类、函数、变量是什么的开发者。我们将从零开始确保你能在30分钟内看到一个由C代码驱动的角色在场景中跑起来。2. 环境准备与项目创建在动键盘写第一行代码之前确保你的“工坊”工具齐全且摆放到位这是避免后续一连串编译错误的关键。2.1 必备软件清单与安装要点首先你需要三个核心软件Unreal Engine 5建议使用5.3或5.4等较新稳定版本、Visual Studio 2022社区版即可以及对应的“Windows桌面C开发”工作负载。很多人安装VS时只勾选默认项这是第一个坑。你必须手动在安装器的工作负载页面找到“使用C的桌面开发”并确保其被选中。安装完成后打开Visual Studio Installer点击“修改”你的2022版本在“单个组件”标签页中额外搜索并勾选“Windows 11 SDK (10.0.22621.0)”或更高版本以及“C ATL for latest v143 build tools (x86 x64)”。这些组件是Unreal Engine源码编译和IntelliSense智能提示所必需的。接下来是Unreal Engine 5的安装。如果你通过Epic Games启动器安装请确保安装时勾选了“Editor symbols for debugging”选项。这会在安装目录下保留调试符号文件当你的C代码崩溃时VS可以定位到具体的引擎源码行而不是一堆十六进制地址这对排查复杂Bug至关重要。安装路径建议全英文且不要有空格或特殊字符例如D:\UE_5.4就比C:\Program Files\Unreal Engine 5.4要稳妥。2.2 创建第一个C项目模板选择与命名玄学打开Epic Games启动器或虚幻引擎点击“游戏”类别这里你会看到一系列模板。对于纯新手我强烈建议选择“第三人称游戏(3rd Person)”并务必在下方“项目默认设置”中将“起始内容”从“蓝图(Blueprint)”切换到“C”。这个切换是本质区别蓝图模板生成的是.uproject文件加一堆蓝图资产而C模板会额外生成一个.sln解决方案文件和一个Source文件夹里面包含了最基础的C类。项目名称和路径的命名有个不成文的规矩项目名使用帕斯卡命名法每个单词首字母大写无空格和下划线例如MyFirstCPPProject。路径同样使用全英文避免中文。我见过太多因为路径含中文导致IDE找不到头文件或者编译工具链崩溃的例子。创建过程会比纯蓝图项目慢一些因为引擎需要为你生成初始的C代码并编译一遍。耐心等待直到看到“正在打开Visual Studio”的提示。注意首次打开由UE生成的项目Visual Studio会花较长时间来构建IntelliSense数据库右下角有提示。在此期间代码的语法高亮和自动补全可能不完整请等待其完成不要急于开始编码否则体验会很差。3. 项目结构深度解析当Visual Studio打开你的项目后别急着关掉虚幻编辑器。两者将是你未来开发中并行的左右手。我们先来彻底弄懂这个刚诞生的项目骨架。3.1 目录结构Source文件夹里的世界在资源管理器里打开你的项目根目录你会发现一个名为Source的文件夹这是你所有C代码的居所。其典型结构如下MyFirstCPPProject/ ├── MyFirstCPPProject.uproject ├── Content/ (蓝图、材质、模型等资源) └── Source/ ├── MyFirstCPPProject/ (项目模块目录) │ ├── MyFirstCPPProject.Build.cs (模块构建规则文件) │ ├── MyFirstCPPProject.cpp (模块实现文件) │ ├── MyFirstCPPProject.h (模块头文件) │ ├── MyFirstCPPProjectCharacter.h (角色类头文件) │ ├── MyFirstCPPProjectCharacter.cpp // (角色类实现文件) │ ├── MyFirstCPPProjectGameMode.h // (游戏模式头文件) │ └── ... (其他自动生成的类) ├── MyFirstCPPProjectEditor.Target.cs // (编辑器构建目标) └── MyFirstCPPProject.Target.cs // (游戏构建目标)最关键的是MyFirstCPPProject这个子文件夹它代表一个“模块”。在UE中大型项目通常被拆分为多个模块如Gameplay、AI、UI每个模块独立编译便于管理和复用。你的主游戏逻辑就放在这个与项目同名的模块里。.Build.cs文件是用C#写的构建脚本它告诉UnrealBuildToolUBT这个模块依赖哪些其他模块。例如如果你的游戏需要用到UMGUI系统你就要在这里的PublicDependencyModuleNames数组里添加UMG。现在先不要动它但要知道它是添加第三方库或引擎模块依赖的入口。.Target.cs文件定义了构建目标。“Editor”目标用于生成带编辑器的开发版本“Game”目标用于生成独立的可执行游戏包。3.2 核心类初窥GameMode与Character让我们用VS打开两个最重要的初始类文件MyFirstCPPProjectGameMode.h/.cpp和MyFirstCPPProjectCharacter.h/.cpp。GameMode游戏模式是游戏规则的“导演”。它决定了默认使用哪个Pawn玩家控制的实体、哪个PlayerController玩家输入处理器、以及游戏状态如何管理。在生成的MyFirstCPPProjectGameMode.h中你会看到类似这样的代码UCLASS(minimalapi) class AMyFirstCPPProjectGameMode : public AGameModeBase { GENERATED_BODY() public: AMyFirstCPPProjectGameMode(); };UCLASS()宏是UE反射系统的核心它将这个C类暴露给虚幻编辑器和蓝图系统。minimalapi是一个优化标记意味着这个类只在必要的模块中被导出可以加快编译速度。GENERATED_BODY()宏必须放在类体的最开头它由Unreal Header ToolUHT在编译前生成包含了所有反射所需的样板代码。Character角色是一种特殊的Pawn它内置了移动组件CharacterMovementComponent和胶囊体碰撞组件非常适合用于需要复杂移动如行走、跳跃、飞行的玩家或AI角色。打开MyFirstCPPProjectCharacter.cpp在构造函数里你能看到引擎自动为其添加了弹簧臂组件和摄像机组件构建了一个第三人称的相机框架。这就是模板的便利之处它为你搭建好了最基础的“可移动的视角”。理解这两个类的关系当你运行游戏时GameMode会生成一个默认的Character实例作为玩家控制的Pawn。你的大部分游戏逻辑比如移动、跳跃、攻击都会先从扩展这个Character类开始。4. 编写你的第一个C类一个简单的交互组件现在我们不满足于模板生成的代码要亲手创建一个全新的、具有实际功能的C类。我们的目标是创建一个“交互组件”让角色可以面对场景中的物体按下按键时在屏幕上显示该物体的名称。4.1 使用编辑器添加C类正确姿势与陷阱很多新手会直接在VS里新建.h和.cpp文件然后手动写UCLASS()宏。这极易出错因为你还需手动修改.Build.cs文件并确保UHT能正确扫描到你的新类。正确做法是永远通过虚幻编辑器来添加C类。回到虚幻编辑器在内容浏览器中右键点击任意位置或直接在“工具”菜单栏选择“新建C类…”。在弹出的对话框中你会看到一个类列表。这里有个关键选择如果你要创建的是一个可以附加到Actor上的功能单元应该选择“Actor组件”ActorComponent。我们选择“Actor组件”并将其命名为InteractComponent。点击创建后编辑器会触发编译并在你的Source/项目模块/目录下生成InteractComponent.h和InteractComponent.cpp。实操心得通过编辑器添加类UE会自动完成四件事1) 生成符合UHT规范的头文件和源文件骨架2) 在模块的.Build.cs中注册对于ActorComponent这类引擎内置类其实依赖已存在但如果是自定义模块则需要3) 在解决方案中正确添加文件引用4) 触发一次增量编译确保新类立即可用。手动创建几乎百分百会漏掉其中一两步导致编译失败或编辑器无法识别。4.2 组件功能设计与UProperty暴露打开生成的InteractComponent.h。我们要为其添加功能。首先定义一个射线检测的交互距离并让它能在编辑器中方便地调整。在类定义的public部分添加UCLASS(ClassGroup(Custom), meta(BlueprintSpawnableComponent)) class MYFIRSTCPPPROJECT_API UInteractComponent : public UActorComponent { GENERATED_BODY() public: // 设置此组件属性的默认值 UInteractComponent(); // 每帧调用的函数 virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override; // 执行交互的函数 UFUNCTION(BlueprintCallable, Category Interaction) void PerformInteract(); protected: // 交互距离厘米 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category Interaction, meta (ClampMin 0.0)) float InteractDistance 200.0f; // 调试时是否显示射线 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category Interaction) bool bDebugDrawRay false; };这里引入了几个关键的UE宏UPROPERTY()用于将C变量暴露给虚幻的属性系统使其可在编辑器细节面板中编辑或被蓝图读写。参数是关键EditAnywhere在属性面板和原型实例上均可编辑。BlueprintReadWrite蓝图既可以读取也可以修改这个变量。Category “Interaction”在细节面板中这个属性会被归到“Interaction”分类下便于管理。meta (ClampMin “0.0”)元数据为属性添加额外约束这里确保距离不小于0。UFUNCTION()用于将C函数暴露给蓝图或其它系统调用。BlueprintCallable表示这个函数可以在蓝图中被调用。Category “Interaction”同样用于在蓝图的节点菜单中分类。在InteractComponent.cpp中我们需要实现射线检测逻辑。在TickComponent函数中如果不需要每帧检测可以改为由输入事件触发这里为了演示简单放在Tick里void UInteractComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) { Super::TickComponent(DeltaTime, TickType, ThisTickFunction); // 获取拥有此组件的Actor通常是我们的Character AActor* Owner GetOwner(); if (Owner nullptr) return; // 获取玩家控制器以计算相机视角 APlayerController* PC CastAPlayerController(Owner-GetInstigatorController()); if (PC nullptr) return; FVector ViewLocation; FRotator ViewRotation; PC-GetPlayerViewPoint(ViewLocation, ViewRotation); FVector EndLocation ViewLocation (ViewRotation.Vector() * InteractDistance); // 进行射线检测 FHitResult HitResult; FCollisionQueryParams QueryParams; QueryParams.AddIgnoredActor(Owner); // 忽略自身 bool bHit GetWorld()-LineTraceSingleByChannel(HitResult, ViewLocation, EndLocation, ECC_Visibility, QueryParams); if (bDebugDrawRay) { DrawDebugLine(GetWorld(), ViewLocation, EndLocation, FColor::Green, false, -1.0f, 0, 1.0f); } // 如果检测到物体可以在这里存储它供PerformInteract使用 FocusedActor HitResult.GetActor(); }然后在PerformInteract函数中对FocusedActor进行处理例如调用其上的某个接口函数或者简单地打印日志void UInteractComponent::PerformInteract() { if (FocusedActor ! nullptr) { FString ActorName FocusedActor-GetName(); UE_LOG(LogTemp, Warning, TEXT(Interacted with: %s), *ActorName); // 这里可以进一步调用FocusedActor上的交互接口 } else { UE_LOG(LogTemp, Log, TEXT(No actor to interact with.)); } }4.3 编译、挂载与测试代码写完后在Visual Studio中按CtrlShiftB编译解决方案或点击“生成 - 生成解决方案”。编译成功后切换回虚幻编辑器它会自动检测到更改并重新加载模块。接下来将组件挂载到我们的角色上。在内容浏览器中找到并打开蓝图BP_MyFirstCPPProjectCharacter这是模板为我们生成的基于C Character类的蓝图。在蓝图编辑器的组件面板点击“添加组件”搜索“Interact”你应该能看到我们刚创建的InteractComponent。将其添加进来。现在我们需要绑定一个输入事件来触发交互。打开项目设置Edit - Project Settings找到“Input”选项添加一个新的“Action Mapping”命名为“Interact”并绑定一个按键例如“E”。然后在角色的蓝图事件图表中右键搜索“InputAction Interact”在按下Pressed事件后调用InteractComponent上的PerformInteract函数。最后在场景中放置几个静态网格体Actor运行游戏。控制角色看向这些物体按下E键查看“输出日志”窗口Window - Developer Tools - Output Log你应该能看到打印出的交互信息。如果勾选了组件细节面板中的bDebugDrawRay还能看到一条绿色的射线。5. 蓝图与C的协作模式至此你已经体验了一个典型的UE5 C工作流用C实现核心、可复用的游戏逻辑InteractComponent然后用蓝图进行配置设置交互距离、调试开关、组装将组件添加到角色蓝图和触发绑定输入事件。这种模式结合了C的性能、类型安全与蓝图的快速迭代、可视化优势。5.1 在C中定义蓝图可继承的类C类可以成为蓝图的“父类”。例如我们的MyFirstCPPProjectCharacter就是一个C类而BP_MyFirstCPPProjectCharacter是基于它创建的蓝图。在C头文件中使用UCLASS(Blueprintable)宏实际上大部分UCLASS默认就是Blueprintable就允许编辑器基于它创建蓝图。在C中声明一个UFUNCTION(BlueprintNativeEvent)或UFUNCTION(BlueprintImplementableEvent)函数就可以在蓝图中进行重写或实现这为设计师提供了巨大的灵活性。5.2 在蓝图中调用C函数与访问变量正如我们之前所做的将UFUNCTION(BlueprintCallable)和UPROPERTY(BlueprintReadWrite)暴露给蓝图后在蓝图中就可以像使用原生蓝图节点一样使用它们。这是双向的C也可以调用在蓝图中实现的函数通过UFUNCTION(BlueprintCallable)在C中声明在蓝图中实现逻辑。6. 调试技巧与常见问题实录当你开始编写更多C代码时不可避免地会遇到崩溃、逻辑错误和诡异的Bug。掌握调试技巧至关重要。6.1 使用Visual Studio进行调试最强大的工具是Visual Studio的调试器。确保你的启动项目设置为“YourProjectNameEditor”DebugGame Editor配置。在VS中设置断点然后按F5启动调试虚幻编辑器会启动。当代码执行到断点处编辑器会暂停VS会获得焦点你可以查看所有变量的值、调用堆栈进行单步调试。这对于理解引擎运行流程和排查复杂逻辑错误无比重要。6.2 利用UE_LOG进行日志输出UE_LOG是你的好朋友。在代码中插入日志输出可以快速追踪函数是否被调用、变量的值是什么。使用不同的日志级别Log, Warning, Error可以帮助过滤信息。记得在发布版本中过多的日志会影响性能可以使用UE_LOG的Verbosity级别控制或者用#if WITH_EDITOR宏将调试日志包裹起来。6.3 常见编译与运行时问题排查下面是一个快速排查表列出了新手最常遇到的几个问题问题现象可能原因解决方案编译失败报错“无法打开源文件…”或“未定义的标识符”1. 新增的.h文件未被包含。2. 模块依赖未添加。3. IntelliSense数据库未更新仅是提示错误实际编译可能成功。1. 检查#include路径是否正确。2. 在.Build.cs文件的PublicDependencyModuleNames中添加所需模块名。3. 在VS中尝试“编辑 - IntelliSense - 重新扫描解决方案”。编辑器能启动但一运行游戏或点击播放就崩溃1. 访问了空指针Nullptr。2. 数组越界。3. 在非游戏线程中调用了渲染或蓝图相关函数。1. 对所有指针进行有效性判断后再使用。2. 检查数组索引使用IsValidIndex。3. 确保代码在正确的线程中执行使用AsyncTask或FFunctionGraphTask将任务派发到游戏线程。在编辑器中修改了C代码但重新编译后变化未生效1. 热重载失败。2. 修改了头文件中UCLASS/UFUNCTION/UPROPERTY的宏参数但未重新生成项目文件。1. 尝试手动关闭编辑器在VS中重新编译再启动编辑器。2. 右键点击.uproject文件选择“Generate Visual Studio project files”然后重新编译。蓝图中找不到刚创建的C函数或变量1. 未正确使用UFUNCTION或UPROPERTY宏或宏参数错误。2. 编译未成功。3. 蓝图未刷新。1. 检查宏的拼写和参数特别是BlueprintCallable/BlueprintReadWrite。2. 确认VS编译无错误。3. 在蓝图中右键选择“刷新所有节点”。打包Build失败1. 缺少某些非引擎自带库的依赖。2. 代码中存在平台特定的API调用。3. 磁盘空间不足。1. 在.Build.cs中正确添加第三方库的路径和链接库。2. 使用#if PLATFORM_WINDOWS等宏包裹平台相关代码。3. 检查打包输出路径的磁盘空间。6.4 性能分析与内存管理对于C开发者需要更关注内存。UE使用智能指针如TSharedPtr,TUniquePtr和其自有的垃圾回收GC系统来管理UObject派生类的对象。牢记继承自UObject的类使用NewObject或SpawnActor创建由GC管理非UObject的纯C类需自行管理生命周期推荐使用TUniquePtr。不当的强引用循环会导致UObject无法被GC回收造成内存泄漏。使用UPROPERTY()持有的对象引用GC会将其视为根引用确保对象不被错误回收但也需注意解除不再需要的引用。创建第一个C项目并成功运行只是万里长征的第一步。接下来你可以尝试为你的交互组件添加更复杂的功能比如显示交互物体的UI提示学习使用UE的接口Interface系统来设计更松耦合的交互或者深入研究Gameplay Ability System (GAS) 来实现一套技能框架。关键在于保持“用C构建系统用蓝图配置内容”的思维让两者各司其职发挥最大效力。当你遇到问题时官方文档、源码注释以及社区论坛都是极好的资源。多读引擎源码是理解其设计哲学和提升开发能力的最快途径。
返回列表