资讯动态

Unreal Engine C++入门:从零创建并控制你的第一个游戏对象Actor

发布时间:2026/8/3 19:49:51 来源:尧图企业网站定制
1. 项目概述从蓝图到C掌控你的第一个游戏对象如果你刚接触Unreal Engine可能已经用蓝图Blueprint拖拽节点实现过一些功能感觉既直观又方便。但当你想要实现更复杂的逻辑、追求更高的运行时性能或者想深入理解引擎底层运作机制时C就成了绕不开的一环。很多朋友卡在第一步如何在UE的C项目里真正创建一个能被我们代码完全控制的游戏对象Actor这不仅仅是写一个UCLASS()宏那么简单它涉及到从项目配置、类创建、组件挂载到逻辑控制的完整链条。今天我们就来彻底拆解这个过程我会结合自己踩过的坑手把手带你完成从零到一的跨越让你不仅知道怎么做更明白为什么这么做。所谓“游戏对象”在Unreal Engine里最典型的代表就是AActor及其派生类。它可以是一把武器、一个敌人、一个可收集的道具甚至是整个关卡中的一盏灯。通过C创建并控制它意味着我们将拥有对其实例化、属性初始化、组件管理、每帧更新Tick以及响应游戏事件如碰撞的完全代码级控制权。这为我们实现精准、高效且可复用的游戏逻辑打下了坚实基础。整个过程会涉及Visual Studio或你偏好的IDE的项目文件管理、Unreal Header ToolUHT的代码生成机制、以及Unreal特有的反射和垃圾回收系统。别担心我们会一步步来。2. 环境准备与项目创建打好地基在动手写代码之前确保你的开发环境是正确搭建的这能避免后续一大堆令人头疼的编译错误和链接问题。2.1 引擎与IDE的安装与关联首先你需要安装Unreal Engine。建议通过Epic Games Launcher安装最新的稳定版本如5.3或5.4。同时你需要一个C IDEWindows平台首推Visual Studio 2022。安装时务必在“工作负载”中勾选“使用C的游戏开发”这会自动安装必要的Windows SDK、C工具集以及Unreal Engine的调试器扩展。安装完成后关键一步是确保引擎能找到你的编译器。打开Epic Games Launcher在Unreal Engine标签页的“库”中找到你安装的引擎版本点击右侧的“选项”下拉菜单选择“验证”。这个操作会检查引擎文件的完整性并确保其与Visual Studio的关联是正确的。有时候安装顺序不对会导致关联失败验证过程可以修复大部分此类问题。注意强烈不建议使用Visual Studio Code作为主要的UE C开发IDE尽管它可以作为编辑器。UE项目庞大的代码量和复杂的编译、生成过程对VS的深度集成如热重载、蓝图调试、性能分析器依赖很强。VSCode更适合作为辅助的代码浏览和编辑工具。2.2 创建第一个C项目启动Unreal Engine选择“游戏”类别然后点击“下一步”。在项目模板页面选择“空白”项目。这是最关键的一步在下方“项目默认设置”中将“项目类型”从“蓝图”切换为“C”。给你的项目起个名字比如MyFirstCPPObject选择好项目存放路径。点击“创建”后引擎会自动为你生成一个解决方案文件.sln和一个基本的C项目结构。它会自动打开Visual Studio。这个初始项目已经包含了一个基本的游戏模式类AMyFirstCPPObjectGameModeBase和编译所需的所有配置文件。在VS中你可以看到“解决方案资源管理器”里包含了你的项目模块通常以项目名命名以及一系列引擎模块的引用。2.3 理解项目文件结构在项目根目录下你会看到几个关键文件夹和文件Source/所有C源代码的存放地。里面会有一个以你项目名命名的模块文件夹如MyFirstCPPObject/。MyFirstCPPObject.uproject项目描述文件定义了项目名称、模块、引擎版本等元信息。右键它选择“Generate Visual Studio project files”可以重新生成解决方案当你手动添加了新的.h或.cpp文件后有时需要这一步。Binaries/、Intermediate/、Saved/由引擎生成包含编译结果、中间文件和配置通常不需要手动修改。.vsconfigVisual Studio的配置文件确保打开项目时加载正确的组件。在VS中编译通常选“Development Editor”配置并运行项目如果成功弹出一个空白的UE编辑器窗口恭喜你环境搭建成功。如果编译失败请首先检查输出窗口的错误信息常见问题包括路径包含中文、权限不足或者SDK版本不匹配。3. 核心类创建定义你的游戏对象蓝图现在我们来创建第一个属于自己的C游戏对象类。我们将创建一个简单的“旋转立方体”作为示例。3.1 使用编辑器添加C类最稳妥的方式是通过UE编辑器来添加类这能确保所有必要的文件生成和项目配置更新自动完成。在UE编辑器中点击菜单栏的“工具(Tools)” - “新建C类(New C Class...)”。在弹出的类选择窗口中我们会看到很多基类选项。对于大多数可放置在关卡中的物体我们选择“Actor”作为父类。点击“下一步”将类命名为ARotatingCubeUnreal的命名约定中A开头的类代表继承自AActor。保持“公共(Public)”访问权限路径使用默认的Source/MyFirstCPPObject/即可。点击“创建类”。编辑器会提示你正在添加代码然后自动打开Visual Studio并定位到你新创建的ARotatingCube类的头文件.h和源文件.cpp。这个自动生成的过程背后是Unreal Header ToolUHT在工作它解析了你的类声明并生成了必要的反射代码*.generated.h文件。3.2 剖析生成的代码结构让我们看看生成的RotatingCube.h和RotatingCube.cpp。RotatingCube.h:#pragma once #include CoreMinimal.h #include GameFramework/Actor.h #include RotatingCube.generated.h // 必须包含的生成头文件 UCLASS() class MYFIRSTCPPOBJECT_API ARotatingCube : public AActor { GENERATED_BODY() public: // 设置此Actor属性的默认值 ARotatingCube(); protected: // 游戏开始或生成时调用 virtual void BeginPlay() override; public: // 每一帧调用 virtual void Tick(float DeltaTime) override; };UCLASS()这是一个宏告诉UHT这个类需要被纳入Unreal的反射系统。反射使得这个类能在编辑器中显示、能被蓝图继承、其属性可被序列化等。MYFIRSTCPPOBJECT_API这是模块的导出宏确保这个类能被其他模块正确链接。GENERATED_BODY()这是另一个关键宏必须放在类体的最开头。它会被UHT替换为生成的一大段反射代码包括元数据、属性描述符等。没有它你的类在引擎中将无法正常工作。构造函数ARotatingCube()用于初始化对象的默认属性。BeginPlay()相当于Start当Actor被放入关卡且游戏开始时调用用于初始化逻辑。Tick(float DeltaTime)每帧调用DeltaTime是上一帧到这一帧的时间间隔秒用于实现持续变化的逻辑如旋转、移动。RotatingCube.cpp:#include RotatingCube.h // 构造函数实现 ARotatingCube::ARotatingCube() { // 将此Actor设置为每帧调用Tick() PrimaryActorTick.bCanEverTick true; } // BeginPlay实现 void ARotatingCube::BeginPlay() { Super::BeginPlay(); // 务必调用父类实现 } // Tick实现 void ARotatingCube::Tick(float DeltaTime) { Super::Tick(DeltaTime); // 务必调用父类实现 }初始的构造函数里设置了PrimaryActorTick.bCanEverTick true;这意味着这个Actor默认会启用每帧更新。如果你不需要Tick可以在构造函数里将其设为false以提升性能。4. 为对象添加可视组件与属性一个空的Actor在游戏里是看不见的。我们需要给它添加一个静态网格体组件Static Mesh Component来赋予它形状。4.1 在头文件中声明组件与属性修改RotatingCube.h在private或public区域添加组件指针和可编辑属性。UCLASS() class MYFIRSTCPPOBJECT_API ARotatingCube : public AActor { GENERATED_BODY() public: ARotatingCube(); protected: virtual void BeginPlay() override; public: virtual void Tick(float DeltaTime) override; // 声明一个静态网格体组件指针 UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category Components) class UStaticMeshComponent* CubeMesh; // 声明一个可编辑的旋转速度属性 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category Rotation) float RotationSpeed; };UPROPERTY()属性宏。它定义了属性在编辑器和运行时环境中的行为。VisibleAnywhere该属性在“属性详情”面板中可见但不能被编辑。EditAnywhere该属性在“属性详情”面板中可见且可编辑。BlueprintReadOnly蓝图可以读取该属性。BlueprintReadWrite蓝图可以读取和写入该属性。Category ...在“属性详情”面板中该属性会被归到指定的分类下使面板更整洁。class UStaticMeshComponent* CubeMesh;前向声明了UStaticMeshComponent类并在后面声明了指针。在.cpp文件中需要包含对应的头文件。4.2 在构造函数中创建并初始化组件修改RotatingCube.cpp的构造函数#include RotatingCube.h #include Components/StaticMeshComponent.h // 包含组件头文件 ARotatingCube::ARotatingCube() { PrimaryActorTick.bCanEverTick true; // 创建根组件可选但推荐 // RootComponent CreateDefaultSubobjectUSceneComponent(TEXT(RootComponent)); // 创建并设置静态网格体组件 CubeMesh CreateDefaultSubobjectUStaticMeshComponent(TEXT(CubeMesh)); // 如果创建了根组件则附加到根组件否则设置为根组件 // if (RootComponent) CubeMesh-SetupAttachment(RootComponent); SetRootComponent(CubeMesh); // 直接将网格体设置为根组件 // 设置默认旋转速度 RotationSpeed 100.0f; // 度/秒 // 在构造函数中加载一个默认的立方体网格方法一硬编码 // 注意这要求引擎内容中存在该路径的资源 // static ConstructorHelpers::FObjectFinderUStaticMesh MeshAsset(TEXT(/Engine/BasicShapes/Cube.Cube)); // if (MeshAsset.Succeeded()) // { // CubeMesh-SetStaticMesh(MeshAsset.Object); // } }CreateDefaultSubobjectUStaticMeshComponent(TEXT(CubeMesh))这是在Actor构造函数中创建组件对象的标准方式。TEXT宏用于定义组件的名字在编辑器中显示。绝对不能使用C的new关键字来创建UObject派生类对象。SetRootComponent(CubeMesh)将这个网格体组件设置为Actor的根组件。根组件是变换层级结构的原点。一个Actor必须有且只有一个根组件。关于加载默认网格我注释掉的那段代码展示了如何在构造函数中通过硬编码路径加载一个引擎内置的网格。但在实际项目中更灵活的做法是将网格选择权交给设计师即通过UPROPERTY(EditAnywhere)让其在编辑器中指定或者在BeginPlay中动态加载。4.3 更优实践在编辑器中指定网格让我们采用更佳实践允许在编辑器中指定网格和材质。修改头文件UCLASS() class MYFIRSTCPPOBJECT_API ARotatingCube : public AActor { // ... GENERATED_BODY() 等 ... public: // ... 原有CubeMesh和RotationSpeed声明 ... // 可编辑的网格资产引用 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category Appearance) class UStaticMesh* CubeStaticMesh; // 可编辑的材质资产引用 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category Appearance) class UMaterialInterface* CubeMaterial; };然后在BeginPlay()中应用这些资产void ARotatingCube::BeginPlay() { Super::BeginPlay(); // 如果指定了网格则应用 if (CubeStaticMesh) { CubeMesh-SetStaticMesh(CubeStaticMesh); } else { // 备用方案尝试加载引擎默认立方体 static ConstructorHelpers::FObjectFinderUStaticMesh DefaultMeshFinder(TEXT(/Engine/BasicShapes/Cube.Cube)); if (DefaultMeshFinder.Succeeded()) { CubeMesh-SetStaticMesh(DefaultMeshFinder.Object); } } // 如果指定了材质则应用 if (CubeMaterial) { CubeMesh-SetMaterial(0, CubeMaterial); // 0表示第一个材质槽 } }这样设计师无需修改代码就可以在UE编辑器的“属性详情”面板中自由地为ARotatingCube实例选择任意的静态网格和材质。5. 实现核心逻辑让对象动起来逻辑的核心在Tick函数中实现。我们将实现让立方体绕一个轴持续旋转。5.1 编写Tick函数逻辑修改RotatingCube.cpp中的Tick函数void ARotatingCube::Tick(float DeltaTime) { Super::Tick(DeltaTime); if (CubeMesh) // 安全判断 { // 计算本帧应旋转的角度 float RotationThisFrame RotationSpeed * DeltaTime; // 创建一个绕Z轴旋转的增量旋转量 FRotator DeltaRotation(0.0f, 0.0f, RotationThisFrame); // 应用旋转到根组件即CubeMesh // AddActorLocalRotation 是 AActor 的便捷函数相对于自身坐标系旋转 AddActorLocalRotation(DeltaRotation); // 另一种等价方式直接操作组件的相对旋转 // FRotator NewRotation CubeMesh-GetRelativeRotation() DeltaRotation; // CubeMesh-SetRelativeRotation(NewRotation); } }DeltaTime这是帧时间间隔。所有基于时间的运动都必须乘以DeltaTime以确保在不同帧率下运动速度一致。RotationSpeed的单位是“度/秒”乘以秒DeltaTime得到本帧应旋转的度数。FRotatorUnreal中用于表示旋转的结构体三个分量分别代表绕PitchX轴、YawY轴、RollZ轴的旋转角度度。AddActorLocalRotation这是AActor提供的辅助函数它相对于Actor自身的局部坐标系Local Space施加一个旋转增量。这比手动获取、计算再设置旋转更简洁并且能正确处理旋转插值等。5.2 编译与测试在Visual Studio中确保解决方案配置是“Development Editor”然后按F5编译并运行或者点击“本地Windows调试器”。编译成功后UE编辑器会自动重新加载模块并打开。在UE编辑器中在“内容浏览器”的“C类”文件夹下你应该能找到你的ARotatingCube类。将其拖拽到视口Viewport中创建一个实例。选中这个实例在“细节Details”面板中你应该能看到“Rotation”分类下的Rotation Speed属性以及“Appearance”分类下的Cube Static Mesh和Cube Material属性。尝试修改Rotation Speed为一个更大的值比如200。点击编辑器左上角的“运行Play”按钮。你应该能看到场景中的立方体开始绕Z轴持续旋转。6. 暴露更多控制蓝图可调用函数与事件为了让我们的C类能与蓝图进行更灵活的交互我们可以暴露一些函数和事件。6.1 添加蓝图可调用函数假设我们想添加一个函数让蓝图可以随时反转旋转方向。在RotatingCube.h的public部分添加// 蓝图可调用函数反转旋转方向 UFUNCTION(BlueprintCallable, Category Rotation) void ReverseRotationDirection();在RotatingCube.cpp中实现void ARotatingCube::ReverseRotationDirection() { RotationSpeed -RotationSpeed; // 可以在这里添加一些视觉或声音反馈 // 例如播放一个反转音效 // if (ReverseSound) UGameplayStatics::PlaySoundAtLocation(this, ReverseSound, GetActorLocation()); }UFUNCTION(BlueprintCallable, Category Rotation)宏使得这个C函数可以在蓝图中被调用。现在在蓝图中你可以找到一个名为“Reverse Rotation Direction”的节点连接到任何事件如按键事件上即可触发。6.2 添加蓝图可分配事件委托我们还可以定义一个事件当旋转速度超过某个阈值时触发允许蓝图做出响应。这需要用到委托Delegate。首先在头文件中声明一个多播委托和其关联的事件// 在类声明上方ARotatingCube类之外声明一个委托类型 DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnSpeedExceededThreshold, float, ExceededSpeed); UCLASS() class MYFIRSTCPPOBJECT_API ARotatingCube : public AActor { // ... GENERATED_BODY()等 ... public: // ... 其他成员 ... // 声明一个蓝图可分配的事件 UPROPERTY(BlueprintAssignable, Category Rotation|Events) FOnSpeedExceededThreshold OnSpeedExceededThreshold; // 新增一个阈值属性 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category Rotation) float SpeedThreshold; };然后在Tick函数中检查并触发事件void ARotatingCube::Tick(float DeltaTime) { Super::Tick(DeltaTime); if (CubeMesh) { float RotationThisFrame RotationSpeed * DeltaTime; FRotator DeltaRotation(0.0f, 0.0f, RotationThisFrame); AddActorLocalRotation(DeltaRotation); // 检查速度是否超过阈值取绝对值 if (FMath::Abs(RotationSpeed) SpeedThreshold SpeedThreshold 0.0f) { // 触发事件通知所有绑定者 OnSpeedExceededThreshold.Broadcast(RotationSpeed); // 注意为了避免每帧都广播可以设置一个标志位只在第一次超过时广播 } } }在蓝图中你现在可以在ARotatingCube实例的“事件图表”中找到“On Speed Exceeded Threshold”事件节点并为其添加自定义逻辑比如播放特效、改变材质等。7. 常见问题与调试技巧实录在实际操作中你几乎一定会遇到各种编译错误、运行时崩溃或逻辑不符预期的情况。这里记录一些典型问题和排查思路。7.1 编译与链接错误问题1LNK2019: unresolved external symbol链接错误。原因最常见的原因是.cpp文件中没有实现头文件中声明的函数或者函数签名参数、常量性不匹配。排查仔细核对头文件中的函数声明与.cpp文件中的实现是否完全一致。检查是否漏写了类作用域如ARotatingCube::。问题2C2143: syntax error: missing ; before *等语法错误。原因通常是头文件包含顺序问题或前向声明使用不当。排查确保在.cpp文件中包含了所有用到的类的具体头文件如#include Components/StaticMeshComponent.h。在头文件中对于仅用指针或引用引用的类使用前向声明class UStaticMeshComponent;是良好实践可以加快编译速度。问题3修改了.h文件中的UCLASS/USTRUCT/UENUM但编辑器不更新。原因Unreal Header ToolUHT没有重新运行。解决在Visual Studio中执行“重新构建Rebuild”或者关闭编辑器手动删除项目目录下的Intermediate/和Saved/文件夹较激进然后重新生成项目文件并编译。7.2 运行时问题与调试问题4Actor放入关卡后看不到任何网格。排查步骤在编辑器视口中选中该Actor查看“细节”面板中的CubeMesh组件是否存在其Static Mesh属性是否被正确赋值。检查BeginPlay()中加载默认网格的代码路径是否被执行。可以在BeginPlay()开头加一个UE_LOG(LogTemp, Warning, TEXT(BeginPlay called));来验证。检查网格资源路径是否正确。使用控制台命令Editor.Export导出资产列表或直接在内容浏览器中搜索确认路径。问题5旋转速度不稳定或时快时慢。原因没有正确使用DeltaTime或者Tick的调用间隔不稳定这是正常的。验证在Tick函数中打印DeltaTimeUE_LOG(LogTemp, Log, TEXT(DeltaTime: %f), DeltaTime);。确保你的运动计算是速度 * DeltaTime。问题6使用CreateDefaultSubobject时程序崩溃。原因CreateDefaultSubobject只能在构造函数中调用。在BeginPlay或Tick等其他函数中调用会导致崩溃。牢记组件的创建和初始附加SetupAttachment必须在构造函数中完成。动态添加组件可以使用NewObject但管理起来更复杂。7.3 性能与最佳实践注意事项1. Tick的滥用不是每个Actor都需要Tick。对于大量静止或低频更新的物体启用Tick是巨大的性能浪费。在构造函数中设置PrimaryActorTick.bCanEverTick false;。如果后续需要开启可以调用SetActorTickEnabled(true)。2. 组件查找开销避免在Tick中频繁使用FindComponentByClass或GetComponentByClass来查找组件。应在BeginPlay中查找一次并将指针缓存起来。3. 合理使用UPROPERTY说明符 - 对于不需要蓝图读写、仅C使用的属性可以不加BlueprintReadWrite或使用BlueprintReadOnly。 - 对于配置数据使用EditDefaultsOnly仅在类默认值中可编辑而非EditAnywhere可以防止关卡中的实例被意外修改。 - 使用meta (AllowPrivateAccess true)可以让private属性在蓝图中通过Getter/Setter访问。4. 日志与屏幕输出善用UE_LOG进行调试。对于临时观察的变量可以使用DrawDebugString或GEngine-AddOnScreenDebugMessage输出到屏幕但发布前记得移除或使用条件编译。5. 热重载Live Coding对于简单的C代码修改如修改函数内部逻辑可以使用Visual Studio的“调试 - 全部中断 - 应用代码更改”进行热重载无需重启编辑器能极大提升迭代效率。但对于修改类结构如增删UPROPERTY、头文件包含关系等通常需要完全重新编译。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价