ARTICLE DETAIL

资讯详情

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

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

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

1. 项目概述:从蓝图到C++,掌控你的第一个游戏对象

如果你刚接触Unreal Engine,可能已经用蓝图(Blueprint)拖拽节点实现过一些功能,感觉既直观又方便。但当你想要实现更复杂的逻辑、追求更高的运行时性能,或者想深入理解引擎底层运作机制时,C++就成了绕不开的一环。很多朋友卡在第一步:如何在UE的C++项目里,真正创建一个能被我们代码完全控制的游戏对象(Actor)?这不仅仅是写一个UCLASS()宏那么简单,它涉及到从项目配置、类创建、组件挂载到逻辑控制的完整链条。今天,我们就来彻底拆解这个过程,我会结合自己踩过的坑,手把手带你完成从零到一的跨越,让你不仅知道怎么做,更明白为什么这么做。

所谓“游戏对象”,在Unreal Engine里最典型的代表就是AActor及其派生类。它可以是一把武器、一个敌人、一个可收集的道具,甚至是整个关卡中的一盏灯。通过C++创建并控制它,意味着我们将拥有对其实例化、属性初始化、组件管理、每帧更新(Tick)以及响应游戏事件(如碰撞)的完全代码级控制权。这为我们实现精准、高效且可复用的游戏逻辑打下了坚实基础。整个过程会涉及Visual Studio(或你偏好的IDE)的项目文件管理、Unreal Header Tool(UHT)的代码生成机制、以及Unreal特有的反射和垃圾回收系统。别担心,我们会一步步来。

2. 环境准备与项目创建:打好地基

在动手写代码之前,确保你的开发环境是正确搭建的,这能避免后续一大堆令人头疼的编译错误和链接问题。

2.1 引擎与IDE的安装与关联

首先,你需要安装Unreal Engine。建议通过Epic Games Launcher安装最新的稳定版本(如5.3或5.4)。同时,你需要一个C++ IDE,Windows平台首推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/:由引擎生成,包含编译结果、中间文件和配置,通常不需要手动修改。
  • .vsconfig:Visual Studio的配置文件,确保打开项目时加载正确的组件。

在VS中编译(通常选“Development Editor”配置)并运行项目,如果成功弹出一个空白的UE编辑器窗口,恭喜你,环境搭建成功。如果编译失败,请首先检查输出窗口的错误信息,常见问题包括路径包含中文、权限不足或者SDK版本不匹配。

3. 核心类创建:定义你的游戏对象蓝图

现在,我们来创建第一个属于自己的C++游戏对象类。我们将创建一个简单的“旋转立方体”作为示例。

3.1 使用编辑器添加C++类

最稳妥的方式是通过UE编辑器来添加类,这能确保所有必要的文件生成和项目配置更新自动完成。在UE编辑器中,点击菜单栏的“工具(Tools)” -> “新建C++类(New C++ Class...)”。

在弹出的类选择窗口中,我们会看到很多基类选项。对于大多数可放置在关卡中的物体,我们选择“Actor”作为父类。点击“下一步”,将类命名为ARotatingCube(Unreal的命名约定中,A开头的类代表继承自AActor)。保持“公共(Public)”访问权限,路径使用默认的Source/MyFirstCPPObject/即可。点击“创建类”。

编辑器会提示你正在添加代码,然后自动打开Visual Studio,并定位到你新创建的ARotatingCube类的头文件(.h)和源文件(.cpp)。这个自动生成的过程背后是Unreal Header Tool(UHT)在工作,它解析了你的类声明,并生成了必要的反射代码(*.generated.h文件)。

3.2 剖析生成的代码结构

让我们看看生成的RotatingCube.hRotatingCube.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,在privatepublic区域添加组件指针和可编辑属性。

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 = CreateDefaultSubobject<USceneComponent>(TEXT("RootComponent")); // 创建并设置静态网格体组件 CubeMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("CubeMesh")); // 如果创建了根组件,则附加到根组件,否则设置为根组件 // if (RootComponent) CubeMesh->SetupAttachment(RootComponent); SetRootComponent(CubeMesh); // 直接将网格体设置为根组件 // 设置默认旋转速度 RotationSpeed = 100.0f; // 度/秒 // 在构造函数中加载一个默认的立方体网格(方法一:硬编码) // 注意:这要求引擎内容中存在该路径的资源 // static ConstructorHelpers::FObjectFinder<UStaticMesh> MeshAsset(TEXT("/Engine/BasicShapes/Cube.Cube")); // if (MeshAsset.Succeeded()) // { // CubeMesh->SetStaticMesh(MeshAsset.Object); // } }
  • CreateDefaultSubobject<UStaticMeshComponent>(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::FObjectFinder<UStaticMesh> 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)得到本帧应旋转的度数。
  • FRotator:Unreal中用于表示旋转的结构体,三个分量分别代表绕Pitch(X轴)、Yaw(Y轴)、Roll(Z轴)的旋转角度(度)。
  • AddActorLocalRotation:这是AActor提供的辅助函数,它相对于Actor自身的局部坐标系(Local Space)施加一个旋转增量。这比手动获取、计算再设置旋转更简洁,并且能正确处理旋转插值等。

5.2 编译与测试

在Visual Studio中,确保解决方案配置是“Development Editor”,然后按F5编译并运行,或者点击“本地Windows调试器”。编译成功后,UE编辑器会自动重新加载模块并打开。

在UE编辑器中:

  1. 在“内容浏览器”的“C++类”文件夹下,你应该能找到你的ARotatingCube类。
  2. 将其拖拽到视口(Viewport)中,创建一个实例。
  3. 选中这个实例,在“细节(Details)”面板中,你应该能看到“Rotation”分类下的Rotation Speed属性,以及“Appearance”分类下的Cube Static MeshCube Material属性。
  4. 尝试修改Rotation Speed为一个更大的值,比如200。
  5. 点击编辑器左上角的“运行(Play)”按钮。你应该能看到场景中的立方体开始绕Z轴持续旋转。

6. 暴露更多控制:蓝图可调用函数与事件

为了让我们的C++类能与蓝图进行更灵活的交互,我们可以暴露一些函数和事件。

6.1 添加蓝图可调用函数

假设我们想添加一个函数,让蓝图可以随时反转旋转方向。在RotatingCube.hpublic部分添加:

// 蓝图可调用函数:反转旋转方向 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 编译与链接错误

问题1:LNK2019: unresolved external symbol链接错误。

  • 原因:最常见的原因是.cpp文件中没有实现头文件中声明的函数,或者函数签名(参数、常量性)不匹配。
  • 排查:仔细核对头文件中的函数声明与.cpp文件中的实现是否完全一致。检查是否漏写了类作用域(如ARotatingCube::)。

问题2:C2143: syntax error: missing ';' before '*'等语法错误。

  • 原因:通常是头文件包含顺序问题或前向声明使用不当。
  • 排查:确保在.cpp文件中包含了所有用到的类的具体头文件(如#include "Components/StaticMeshComponent.h")。在头文件中,对于仅用指针或引用引用的类,使用前向声明(class UStaticMeshComponent;)是良好实践,可以加快编译速度。

问题3:修改了.h文件中的UCLASS/USTRUCT/UENUM,但编辑器不更新。

  • 原因:Unreal Header Tool(UHT)没有重新运行。
  • 解决:在Visual Studio中执行“重新构建(Rebuild)”,或者关闭编辑器,手动删除项目目录下的Intermediate/Saved/文件夹(较激进),然后重新生成项目文件并编译。

7.2 运行时问题与调试

问题4:Actor放入关卡后看不到任何网格。

  • 排查步骤
    1. 在编辑器视口中选中该Actor,查看“细节”面板中的CubeMesh组件是否存在,其Static Mesh属性是否被正确赋值。
    2. 检查BeginPlay()中加载默认网格的代码路径是否被执行。可以在BeginPlay()开头加一个UE_LOG(LogTemp, Warning, TEXT("BeginPlay called"));来验证。
    3. 检查网格资源路径是否正确。使用控制台命令Editor.Export导出资产列表,或直接在内容浏览器中搜索确认路径。

问题5:旋转速度不稳定或时快时慢。

  • 原因:没有正确使用DeltaTime,或者Tick的调用间隔不稳定(这是正常的)。
  • 验证:在Tick函数中打印DeltaTimeUE_LOG(LogTemp, Log, TEXT("DeltaTime: %f"), DeltaTime);。确保你的运动计算是速度 * DeltaTime

问题6:使用CreateDefaultSubobject时程序崩溃。

  • 原因CreateDefaultSubobject只能在构造函数中调用。在BeginPlayTick等其他函数中调用会导致崩溃。
  • 牢记:组件的创建和初始附加(SetupAttachment)必须在构造函数中完成。动态添加组件可以使用NewObject,但管理起来更复杂。

7.3 性能与最佳实践注意事项

1. Tick的滥用:不是每个Actor都需要Tick。对于大量静止或低频更新的物体,启用Tick是巨大的性能浪费。在构造函数中设置PrimaryActorTick.bCanEverTick = false;。如果后续需要开启,可以调用SetActorTickEnabled(true)

2. 组件查找开销:避免在Tick中频繁使用FindComponentByClassGetComponentByClass来查找组件。应在BeginPlay中查找一次并将指针缓存起来。

3. 合理使用UPROPERTY说明符: - 对于不需要蓝图读写、仅C++使用的属性,可以不加BlueprintReadWrite或使用BlueprintReadOnly。 - 对于配置数据,使用EditDefaultsOnly(仅在类默认值中可编辑)而非EditAnywhere,可以防止关卡中的实例被意外修改。 - 使用meta = (AllowPrivateAccess = "true")可以让private属性在蓝图中通过Getter/Setter访问。

4. 日志与屏幕输出:善用UE_LOG进行调试。对于临时观察的变量,可以使用DrawDebugStringGEngine->AddOnScreenDebugMessage输出到屏幕,但发布前记得移除或使用条件编译。

5. 热重载(Live Coding):对于简单的C++代码修改(如修改函数内部逻辑),可以使用Visual Studio的“调试 -> 全部中断 -> 应用代码更改”进行热重载,无需重启编辑器,能极大提升迭代效率。但对于修改类结构(如增删UPROPERTY)、头文件包含关系等,通常需要完全重新编译。

返回列表