【UEC++】虚幻C++基础入门
温馨提示:懒得分P了,建议配合目录使用(目录下没有小标题的就是还没更完的)
(由于此篇非一次性创造完成,缝缝补补中难免造成案例图片的版本不尽相同,可能上一章节中使用 5.0 配合 VS,下一章节中又变成 4.27 + Rider 的情况,还请谅解~ 软件都是工具,重在体会精神)
① Visual Studio + Visual assists X(小番茄,但耗性能)
② Rider for UE(学生可申请免费使用,请务必白嫖,好用的不得鸟)
③ Visual Studio Code
一. 虚幻工程目录结构与代码命名规则
1.1 工程目录结构
- 打包项目时,主要保住下面3个红框文件夹,其他的会再生成不用担心
- .vs:编译器 VS 的环境配置文件
- Binaries:存放编译生成的结果二进制文件,编译动态库,如果热加载出现问题可以删除后重新生成(SVN 可以 ignore,反正每次都会生成)
- Config:配置文件
- Content:所有的资源和蓝图等都放在该目录里
- DerivedDataCache:“DDC”,存储着引擎针对平台特化后的资源版本,过程文件(SVN可以ignore,比如同一个图片,针对不同的平台有不同的适合格式,这个时候就可以在不动原始的uasset的基础上,比较轻易的再生成不同格式资源版本)
- Intermediate:中间文件,存放着一些临时生成的文件(SVN 可以 ignore)
- Build 的中间文件,.obj 和预编译头等
- UHT 预处理生成的 .generated.h/.cpp 文件
- VS.vcxproj 项目文件,可通过 .uproject 文件生成编译生成的 Shader 文件
- AssetRegistryCache:Asset Registry 系统的缓存文件,Asset Registry 可以简单理解为一个索引了所有 uasset 资源头信息的注册表,CachedAssetRegistry.bin 文件也是如此
- Saved:存储自动保存文件,其他配置文件,日志文件,引擎崩溃日志,硬件信息,烘培信息数据等(Svn 可以 ignore)
- Source:代码文件,存放工程源码。
1.2 项目目录结构
- Engine 引擎源码文件(只读)
- 虚幻是开源项目,我们可以直接在工程中看到引擎源码但无法修改(如需修改请下载 Git 源码工程)
- 引擎源码:Engine - Source - Runtime
- 编辑器源码:Engine - Source - Editor
- Games 项目工程文件(主要编写逻辑文件)
- 我们的代码需要在此工程中编写。虚幻中采用了编译模块方式进行引擎构建,所以对于引擎来说,我们编写的内容只是一个模块,模块会被动态编译为库文件,加载到引擎中使用
- Target.cs 文件就是模块配置文件
-
Visualizers 虚幻 4.21 加入的文件夹,VS 编辑器配置文件
- 当添加、删除、重命名 C++类、改变 C++ 源码文件夹结构后,需要重新生成
1.3 编译选项
- 默认是 DevelopmentEditor,但可以根据项目开发的实际情况自选
- 每种编译配置包含两种关键字:
- 引擎以及游戏项目的状态
- 正在编译的目标
- 为了调试代码方便,一般选择 DebugEditor 来加载游戏项目
- 如需最简化流程时,可用 Debug 来运行独立版本
- 打包时,打 Shipping 包
- Debug 模式下可查看的性能信息更多,但会牺牲一些 CPU 和 GPU 的时间
- Development(默认)性能会稍优一些
- 整合如下:
- 三种编译方式
- 热编译 Hot reload
- VS 和 UE 未连接,最简单,但偶尔会出现问题(编写代码后在 UE 点击“编译”)
- 从 VS 运行
- VS 和 UE 连接,彻底、问题少、可进行代码调试(编译器中 F5 编译,每次启动,编译器中:Debug - Detach 恢复到 Hot reload)
- cmd 命令行
- 最 geek 的一种方式
- 热编译 Hot reload
1.4 编译系统:UBT和UHT
- 我们写的 UE4 代码不是标准的 C++ 代码,是基于 UE4 源代码层层改装了很多层的魔改 C++。
- UBT(Unreal Build Tool,C#):UE4 的自定义工具,来编译 UE4 的逐个模块并处理依赖
- UHT(Unreal Header Tool,C++):UE4 的 C++ 代码解析生成工具
- UHT 将 UE 代码转换成标准的 C++ 代码,而 UBT 负责调用 UHT 来实现这个转化工作,转化完之后 UBT 调用标准的 C++ 代码的编译器来将 UHT 转化后的标准 C++ 代码完成编译成二进制文件
- 整体上看,UHT 是 UBT 的编译流程的一部分
1.5 代码命名规则(遵循帕斯卡命名法)
虚幻引擎|的史诗C++编码标准虚幻引擎5.1文档 (unrealengine.com)
UHT 在工作的时候需要你提供正确的前缀:
- 模版类:T 前缀(如 TArray,TMap,TSet)
- UObject 派生类:U前缀
- AActor 派生类:A 前缀
- SWidget 派生类:S 前缀
- 全局对象:G 开头(如 GEngine)
- 抽象接口:I 前缀
- 枚举:E 开头
- bool 变量:b 前缀
- 其他的大部分以 F 开头(如FString,FName)
- typedef 的以原型名前缀为准(如 typedef TArray FArrayOfMyTypes)
- 在编辑器里和 C# 里,类型名是省略掉前缀的
1.6 资源命名规则
1.7 文件夹命名规则
二. Actor
2.1 创建 Actor
- Actor 是载体,放在Actor 上的东西称之为组件 Component(每一个 Actor 在场景中都不具备存在的能力,但添加了场景组件 SceneComponent,就可以在场景中存在)
- 创建 Actor 的方式:
- ① 静态创建:直接在场景中编辑拖拽
- 优势:创建由引擎构建场景时进行创建,无需编码,更加直观简单
- 缺点:可能会影响游戏启动速度,增加场景构建负担
- ② 动态创建:通过编码动态进行生成
- 优势:可控性更强,动态生成的 Actor 会持有有效的操作指针,可根据实际情况生成,更加灵活
- 缺点:难,复杂
- ① 静态创建:直接在场景中编辑拖拽
- 通过 SpawnActor() 函数动态创建(基础入门版):
- 创建 C++ 文件
- 打开后,可以看到虚幻帮我们写好了的内容结构
- 重写 BeginPlay() 函数和 Tick() 函数:头文件 .h 中写函数声明,.cpp 文件中写函数定义
- 用 SpawnActor 函数动态生成 Actor
- SpawnActor 是工厂函数,需要通过 UWorld 指针 进行创建
- 先去源码 World.h 文件中观察一下 UWorld 类中要调用的 SpawnActor() 函数,果然有很多
- 我们先从提供了默认值的 T* SpawnActor( const FActorSpawnParameters& SpawnParameters = FActorSpawnParameters() ) 基础版开始入门
//函数源码
/** Templated version of SpawnActor that allows you to specify a class type via the template type */ //(泛型编程)由于 C++ 是编译型语言,在编译前必须确定好类型, //定义一个还不确定具体类型的 T类型,调用时需使用尖括号来确定<具体类型> template< class T > T* SpawnActor( const FActorSpawnParameters& SpawnParameters = FActorSpawnParameters() ) { return CastChecked<T>(SpawnActor(T::StaticClass(), NULL, NULL, SpawnParameters),ECastCheckedType::NullAllowed); }//此处插播一下【模板函数 / 泛型函数】的原理 //以 Sum 求和的重载函数为例: int Sum(int a, int b) { return a + b; } int Sum(float a, float b) { return a + b; } int Sum(double a, double b) { return a + b; } //调用 Sum 求和函数时,会根据传入参数的数据类型不同,调用如上不同的重载函数 //而当,不确定传入参数的数据类型时,C++提供了 “泛型编程” 的概念,申请任意一个符号类型(如 T)来代替未来将要传入的参数数据类型(效果如上) template<class T> //特殊情况下,有多个未知类型,则: template<class T,class T1,class T2> T SumFun(T a, T b) { return a + b; } //调用模板函数 / 泛型函数时,需要使用尖括号写出明确的类型 SumFun<int>(1,2); SumFun<float>(1,2); //参数1,2会被强转为 float 类型
- 在 BeginPlay() 函数定义中调用 SpawnActor() 函数
- 由于 UWorld 本身的头文件已经在父类中被包含,所以无需额外引入头文件 #include "Engine/UWorld.h"
-
假如需要生成自定义的 AActor,则需要引入头文件:
//GetWorld() 函数源码 UWorld* AActor::GetWorld() const { // CDO objects do not belong to a world // If the actors outer is destroyed or unreachable we are shutting down and the world should be nullptr if (!HasAnyFlags(RF_ClassDefaultObject) && ensureMsgf(GetOuter(), TEXT("Actor: %s has a null OuterPrivate in AActor::GetWorld()"), *GetFullName()) && !GetOuter()->HasAnyFlags(RF_BeginDestroyed) && !GetOuter()->IsUnreachable()) { if (ULevel* Level = GetLevel()) { return Level->OwningWorld; } } return nullptr; }
- 生成效果如下:会在原点生成 2个空 Actor
- 由于 UWorld 本身的头文件已经在父类中被包含,所以无需额外引入头文件 #include "Engine/UWorld.h"
- 创建 C++ 文件
- 通过 SpawnActor() 函数动态创建(进阶入门版):
- 同理,再去源码 World.h 文件中观察一下 UWorld 类中要调用的 SpawnActor() 函数,共 5 个:
- 现在请尝试:依次调用上面 5 个 SpawnActor() 函数
- 其中,需要特别说明的是:
- FVector
- 是一个结构体,由 X,Y,Z 三个浮点变量组成
- 是一个结构体,由 X,Y,Z 三个浮点变量组成
- FRotator
- 也是一个结构体,由 Pitch,Yaw,Roll 三个浮点变量组成
- 也是一个结构体,由 Pitch,Yaw,Roll 三个浮点变量组成
- FTransform
- 虽是一个结构体,但比上面的复杂亿点,由 FQuat Rotation(四元数) 、FVector Scale3D(矢量)、FVector Translation(矢量) 组成
- 虽是一个结构体,但比上面的复杂亿点,由 FQuat Rotation(四元数) 、FVector Scale3D(矢量)、FVector Translation(矢量) 组成
- UClass*
- 每个继承 Actor 里面都有 StaticClass() ,StaticClass() 是一个合成函数,获取一个UClass指针,旨在将操作类作为一个参数进行传递(传递模版)
- FVector
- 运行后,可以明显看到生成了 5 个Actor
- 同理,再去源码 World.h 文件中观察一下 UWorld 类中要调用的 SpawnActor() 函数,共 5 个:
- 通过 SpawnActorDeferred() 函数动态滞后创建(进阶升级版):
- 滞后生成,适用于占资源时的优化
-
其中,StaticClass() 是合成函数,获取一个UClass指针,旨在将操作类作为一个参数进行传递(传递模版)
-
//SpawnActorDeferred 函数源码: /** * Spawns given class and returns class T pointer, forcibly sets world transform (note this allows scale as well). WILL NOT run Construction Script of Blueprints * to give caller an opportunity to set parameters beforehand. Caller is responsible for invoking construction * manually by calling UGameplayStatics::FinishSpawningActor (see AActor::OnConstruction). */ template< class T > T* SpawnActorDeferred( UClass* Class, FTransform const& Transform, AActor* Owner = nullptr, APawn* Instigator = nullptr, ESpawnActorCollisionHandlingMethod CollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::Undefined ) { if( Owner ) { check(this==Owner->GetWorld()); } FActorSpawnParameters SpawnInfo; SpawnInfo.SpawnCollisionHandlingOverride = CollisionHandlingOverride; SpawnInfo.Owner = Owner; SpawnInfo.Instigator = Instigator; SpawnInfo.bDeferConstruction = true; return (Class != nullptr) ? Cast<T>(SpawnActor(Class, &Transform, SpawnInfo)) : nullptr; }
//FinishSpawning 函数源码: void AActor::FinishSpawning(const FTransform& UserTransform, bool bIsDefaultTransform, const FComponentInstanceDataCache* InstanceDataCache) { #if ENABLE_SPAWNACTORTIMER FScopedSpawnActorTimer SpawnTimer(GetClass()->GetFName(), ESpawnActorTimingType::FinishSpawning); SpawnTimer.SetActorName(GetFName()); #endif if (ensure(!bHasFinishedSpawning)) { bHasFinishedSpawning = true; FTransform FinalRootComponentTransform = (RootComponent ? RootComponent->GetComponentTransform() : UserTransform); // see if we need to adjust the transform (i.e. in deferred cases where the caller passes in a different transform here // than was passed in during the original SpawnActor call) if (RootComponent && !bIsDefaultTransform) { FTransform const* const OriginalSpawnTransform = GSpawnActorDeferredTransformCache.Find(this); if (OriginalSpawnTransform) { GSpawnActorDeferredTransformCache.Remove(this); if (OriginalSpawnTransform->Equals(UserTransform) == false) { UserTransform.GetLocation().DiagnosticCheckNaN(TEXT("AActor::FinishSpawning: UserTransform.GetLocation()")); UserTransform.GetRotation().DiagnosticCheckNaN(TEXT("AActor::FinishSpawning: UserTransform.GetRotation()")); // caller passed a different transform! // undo the original spawn transform to get back to the template transform, so we can recompute a good // final transform that takes into account the template's transform FTransform const TemplateTransform = RootComponent->GetComponentTransform() * OriginalSpawnTransform->Inverse(); FinalRootComponentTransform = TemplateTransform * UserTransform; } } // should be fast and relatively rare ValidateDeferredTransformCache(); } FinalRootComponentTransform.GetLocation().DiagnosticCheckNaN(TEXT("AActor::FinishSpawning: FinalRootComponentTransform.GetLocation()")); FinalRootComponentTransform.GetRotation().DiagnosticCheckNaN(TEXT("AActor::FinishSpawning: FinalRootComponentTransform.GetRotation()")); { FEditorScriptExecutionGuard ScriptGuard; ExecuteConstruction(FinalRootComponentTransform, nullptr, InstanceDataCache, bIsDefaultTransform); } { SCOPE_CYCLE_COUNTER(STAT_PostActorConstruction); PostActorConstruction(); } } }
- 运行后效果
2.2 销毁 Actor
- Actor 的消亡
- 通过 Destroy() 函数动态销毁
//Destroy() 函数源码: //bNetForce:是否强制网络同步删除(默认 true) //bShouldModifyLevel :控制删除 actor / 修改关卡的先后顺序(默认 true:先修改关卡(把 actor 先移除出场景),再从内存中彻底删除 actor)
bool AActor::Destroy( bool bNetForce, bool bShouldModifyLevel ) { // It's already pending kill or in DestroyActor(), no need to beat the corpse if (!IsPendingKillPending()) { UWorld* World = GetWorld(); if (World) { World->DestroyActor( this, bNetForce, bShouldModifyLevel ); } else { UE_LOG(LogSpawn, Warning, TEXT("Destroying %s, which doesn't have a valid world pointer"), *GetPathName()); } } return IsPendingKillPending(); }效果如下:
- 通过 SetLifeSpan() 函数延迟销毁(Actor 被标记为等待销毁并从关卡的Actor阵列中移除)
//SetLifeSpan()函数源码: void AActor::SetLifeSpan( float InLifespan ) { // Store the new value InitialLifeSpan = InLifespan; // Initialize a timer for the actors lifespan if there is one. Otherwise clear any existing timer if ((GetLocalRole() == ROLE_Authority || GetTearOff()) && !IsPendingKill()) { if( InLifespan > 0.0f) { GetWorldTimerManager().SetTimer( TimerHandle_LifeSpanExpired, this, &AActor::LifeSpanExpired, InLifespan ); } else { GetWorldTimerManager().ClearTimer( TimerHandle_LifeSpanExpired ); } } }
- 当对象被删除时(非内存删除)需要通过 EndPlay() 函数进行回调操作
- 对象被彻底清除时回调 EndPlay() 函数,回调会进行删除类型通知
//EndPlay()函数源码: // void AActor::EndPlay(const EEndPlayReason::Type EndPlayReason) { if (ActorHasBegunPlay == EActorBeginPlayState::HasBegunPlay) { TRACE_OBJECT_EVENT(this, EndPlay); ActorHasBegunPlay = EActorBeginPlayState::HasNotBegunPlay; // Dispatch the blueprint events ReceiveEndPlay(EndPlayReason); OnEndPlay.Broadcast(this, EndPlayReason); TInlineComponentArray<UActorComponent*> Components; GetComponents(Components); for (UActorComponent* Component : Components) { if (Component->HasBegunPlay()) { Component->EndPlay(EndPlayReason); } } } }
- 对象被彻底清除时回调 EndPlay() 函数,回调会进行删除类型通知
- 通过 Destroy() 函数动态销毁
2.3 Actor 生命周期
- Actor 的 2 种创建方式,虽有细微差异,但是总体不大,基本分为以下调用流程:
- 调用构造函数
- 初始化成员变量
- 如有蓝图,则初始化蓝图数据
- 构建组件
- BeginPlay(标志着Actor被创建到世界当中)
- Tick
//MyActor.cpp //借助打印验证如上调用流程 // Fill out your copyright notice in the Description page of Project Settings. #include "MyActor.h" // Sets default values(构造函数内) AMyActor::AMyActor() { // Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it. PrimaryActorTick.bCanEverTick = true; UE_LOG(LogTemp,Log,TEXT("1.最先调用构造函数")); UE_LOG(LogTemp,Log,TEXT("2.然后初始化成员变量")); } // Called when the game starts or when spawned void AMyActor::BeginPlay() { Super::BeginPlay(); UE_LOG(LogTemp,Log,TEXT("BeginPlay()")); } // Called every frame void AMyActor::Tick(float DeltaTime) { Super::Tick(DeltaTime); UE_LOG(LogTemp,Log,TEXT("Tick(DeltaTime)")); }
- Actor 生命周期详解图:Actor 被实例的三种主要路径(无论 Actor 的创建方式如何,销毁路径均相同)
- 静态创建
- Load Actors from Disk 从磁盘加载
- Play in Editor 从编辑器复制
- 动态创建
- SpawnActor 生成
- SpawnActorDeferred 延迟生成
- 销毁
- 静态创建
三. 日志输出
3.1 屏幕日志输出
- 借助全局变量GEngine指针调用函数 AddOnScreenDebugMessage,完成屏幕输出
-
- Key = -1时,则添加新的消息,不会覆盖旧有消息(当key为-1时,bNewerOnTop有效,直接添加到队列最上层)
-
Key不是-1时,则更新现有消息,效率更高
- 打印过程:
- 在 MyGameModeBase C++文件中重写 BeginPlay() 函数,借助全局变量 GEngine 指针调用函数 AddOnScreenDebugMessage
//在MyGameModeBase.h 文件 UCLASS() class UE_C_API AMyGameModeBase : public AGameModeBase { GENERATED_BODY() public: //重写 BeginPlay()函数 virtual void BeginPlay() };
//在 MyGameModeBase.cpp 文件 void AMyGameModeBase::BeginPlay() { //void UEngine::AddOnScreenDebugMessage(int32 Key, float TimeToDisplay, FColor DisplayColor, const FString & DebugMessage, bool bNewerOnTop, const FVector2D & TextScale) GEngine->AddOnScreenDebugMessage(-1, 10, FColor::Blue, TEXT("HI")); }
- 在 MyGameModeBase C++文件中重写 BeginPlay() 函数,借助全局变量 GEngine 指针调用函数 AddOnScreenDebugMessage
-
-
把 MyGameModeBase 挂载到游戏模式重载里面,再运行就可以看到打印在屏幕上的内容了:
-
-
- 或者显示文字
3.2 控制台日志输出
- 使用宏UE_LOG进行控制台日志输出(日志会写入本地缓存)
- 日志分类
- 决定了内容输入到控制台时的分类项
-
- 日志类型冗余度,分为:
-
Fatal(会终止进程)致命问题
-
Error(会终止进程)错误问题
-
Warning、Display、Log(较常用的日志分类项)
-
Verbose (将日志详细信息记录到日志文档,但不向控制台输出)
-
VeryVerbose(将日志详细信息记录到日志文档,但不向控制台输出)
-
- 日志类型冗余度,分为:
- 过程演示:
- 首先打开输出日志
-
- 在 MyGameModeBase.cpp 文件中重写 BeginPlay()函数(和上面一样),使用宏UE_LOG进行控制台日志输出
void AMyGameModeBase::BeginPlay() { Super::BeginPlay(); //UE_LOG(LogAutomationController, Display, TEXT("Report can be opened in the editor at '%s'"), ReportExportPath.IsEmpty() ? *FPaths::ConvertRelativePathToFull(FPaths::AutomationReportsDir()) : *ReportExportPath); UE_LOG(LogTemp, Log, TEXT("嘿嘿嘿hhhh")); }
- 这样就可以在控制台打印了
- 在 MyGameModeBase.cpp 文件中重写 BeginPlay()函数(和上面一样),使用宏UE_LOG进行控制台日志输出
3.3 自定义日志分类
- 上面的日志LogTemp都是系统给的,开发大型项目的时候可以自己定义日志分类,方便更快捷的查询自己想要的消息
- 使用宏进行自定义日志输出
//-------------在MyGameModeBase.h文件:
//1.声明日志分类(宏) //DECLARE_LOG_CATEGORY_EXTERN(自定义日志分类名称(Log开头), 日志默认级别(一般使用Log), 日志编译级别(高于此级别的不会被编译,一般用All)); DECLARE_LOG_CATEGORY_EXTERN(LogProjectName, Log, All);
UCLASS() class UE_C_API AMyGameModeBase : public AGameModeBase { GENERATED_BODY() virtual void BeginPlay(); };
//------------在MyGameModeBase.cpp文件:
//2.定义日志分类 //DEFINE_LOG_CATEGORY(CategoryName),放外面的 LogProjectName变成全局类型名 DEFINE_LOG_CATEGORY(LogProjectName);
void AMyGameModeBase::BeginPlay() {
//3.打印到控制台和日志文件 UE_LOG(LogProjectName, Log, TEXT("自定义日志分类"));
}
- 运行结果如下:
3.4 格式化日志
- 通过占位符,传参输出
-
%d 整数输出
-
%f 浮点数输出
-
%s 输出UE类型字符串(非对象型字符串FString)
//在MyGameModeBase.cpp文件: void AMyGameModeBase::BeginPlay() { int32 A = 10; //整数参数 float B = 3.14; //浮点参数 FString C = TEXT("abc"); //非对象型字符串FString //传参 UE_LOG(LogTemp, Log, TEXT("格式化日志输出:%d,%f,%s"), A, B, *C); }
- 运行结果如下:
-
四. 数据类型
4.1 基本数据类型
- UE 对于 C++ 基本数据类型进行深度重定义,禁止在 UE 中使用 C++ 的基本数据类型,这样会影响引擎的跨平台特性
- UE 常用数据类型
- 无符号基本数据类型
//UE数据类型 bool bTest01 = true; //true,false TCHAR ch = 'a'; //字符型,不要用char c1 uint8 a = 10; //无符号字节 typedef unsigned char uint16 a1 = 20; //无符号短整型 typedef unsigned short uint32 a2 = 30; //无符号整型 typedef unsigned int uint64 a3 = 40; //无符号"四字" typedef unsigned _int64 int8 b1 = -10; //有符号字节 int16 b2 = -20; //有符号短整型 int32 b3 = -30; //有符号整型 int64 b4 = -40; //有符号"四字" float c = 3.14; //单精度浮点型 double d = 3.14; //双精度浮点型
- 无符号基本数据类型
- 类型转换:
static_cast<int>() //静态转换 reinterpret_cast<int>() //强制转换 std::string str; int a = atoi(str.c_str()); //将string转成char,再最终转成int
4.2 字符编码
- 虚幻引擎4中的所有字符串都作为FStrings或TCHAR数组以UTF-16 格式存储在内存中
- UE4也提供了如下的字符串和编码转换的宏定义(StringConv.h):
4.3 字符类型
-
① FString:可以被操作的字符串。开销大于其他类字符串类型
//FString 字符串类型的三种写法:(必须用 TEXT宏 包裹字符串) FString F1 = TEXT("我好想出去玩啊啊啊啊"); FString F2(TEXT("我好想睡觉啊啊啊啊")); FString F3 = FString (TEXT("我不想上班啊啊啊啊")); //在屏幕上输出F1"我好想出去玩啊啊啊啊" GEngine->AddOnScreenDebugMessage(-1, 10, FColor::Red, F1); //在日志输出 F1 + F2 + F3(要加*) UE_LOG(LogTemp, Log, TEXT("%s\n%s\n%s\n"), *F1, *F2, *F3);
//① FString 的比较操作(Equals 默认开启大小写) if (F1.Equals(F2, ESearchCase::IgnoreCase)) { UE_LOG(LogTemp, Log, TEXT("两个字符串相等")); } else { UE_LOG(LogTemp, Log, TEXT("两个字符串不相等咯")); } if (F1 == F2) //操作符重载==(忽略大小写)
//② FString 的查询操作: //1.Contains(要查的连续子字符串, 默认忽略大小写, 默认从前往后查),返回bool if (F1.Contains(TEXT("我"),ESearchCase::IgnoreCase,ESearchDir::FromStart)) { UE_LOG(LogTemp, Log, TEXT("字符串包含“我”")); } else { UE_LOG(LogTemp, Log, TEXT("字符串不包含“我”")); } //2.Find(要查的连续子字符串, 默认忽略大小写, 默认从前往后查, 默认查询的起始位置从第一个),返回int32的字符串 int32 SPlace = F3.Find(TEXT("我"),ESearchCase::IgnoreCase,ESearchDir::FromStart,-1);
//③ FString 的检查操作: //IsEmpty() if (F2.IsEmpty()) { UE_LOG(LogTemp, Log, TEXT("空字符串")); }
//④ FString 的路径分隔符:/ F1 = F2 / F3; UE_LOG(LogTemp, Log, TEXT("%s"), *F1); //运行结果如下: //我好想睡觉啊啊啊啊/我不想上班啊啊啊啊
//⑤ FString 的格式化文本操作: FString F4 = FString::Printf(TEXT("%d--%f--%s--"), 232, 3.15f, *F2); UE_LOG(LogTemp, Log, TEXT("%s"), *F4); //运行结果如下: //232--3.150000--我好想睡觉啊啊啊啊--
//⑥ FString 的裁切操作: //Split() F1.Split(TEXT("玩"), &F2, &F3); //以"玩"为中心截断,前面字符串赋给F2,后面赋给F3 UE_LOG(LogTemp, Log, TEXT("%s\n%s\n%s\n"), *F2, *F3, *F1); //打印结果如下: //我好想出去 //啊啊啊啊 //我好想出去玩啊啊啊啊
- ② FName:资源命名字符串,用来存储名称(散列存储),速度快
//FName 字符串类型的四种写法:(必须用 TEXT宏 包裹字符串) FName N1 = TEXT("张三"); FName N2(TEXT("张小三")); FName N3 = FName(TEXT("张大三")); //用 FString 初始化 FName FString Str = TEXT("张三三"); FName N4 = *Str; //在屏幕上输出N1"张三" GEngine->AddOnScreenDebugMessage(-1, 10, FColor::Red, N1.ToString()); //在日志输出 N1 + N2 + N3 + N4 UE_LOG(LogTemp, Log, TEXT("%s\n%s\n%s\n%s"), *N1.ToString(), *N2.ToString(), *N3.ToString(), *N4.ToString());
//① FName 的比较操作(Equals 默认开启大小写)(== 忽略大小写) if (N1 == N2) { UE_LOG(LogTemp, Log, TEXT("两个字符串相等")); } if (N1.IsEqual(N2)) { UE_LOG(LogTemp, Log, TEXT("两个字符串不相等咯")); }
//② FString 的检查操作: //IsNone() 是否为空 if (N1.IsNone()) { UE_LOG(LogTemp, Log, TEXT("空字符串")); } //IsValid() 是否有效(空字符串也是有效的) if (N1.IsValid()) { UE_LOG(LogTemp, Log, TEXT("有效字符串")); }
- ③ FText:显示字符串,处理用户的显式文本。支持格式化文本,方便构建国际化操作,不提供修改函数,无法进行内容修改
//六种 FText字符串 的创建方式: #define LOCTEXT_NAMESPACE "Space" //声明LOCTEXT宏的本地文本的命名空间 void AMyGameModeBase::BeginPlay() { Super::BeginPlay(); //1.构建一个空白文本 FText F1 = FText::GetEmpty(); //2.使用宏构建 NSLOCTEXT(空间名称,键值名称,内容) //(只有使用宏构建的FText 才能被文本器收集,罗列到本地的操作面板中) FText F2 = NSLOCTEXT("UE5", "k1", "嘿哈嚯"); //3.使用宏构建 LOCTEXT(键值名称,内容) //步骤:先声明空间名称 -> 创建对象 -> 卸载宏 FText F3 = LOCTEXT("k2", "嘿哈嚯嘿嘿"); //4.将 FString 转为 FText //(缺点:不能被文本收集器收集) FString str(TEXT("嘿哈嚯嘿嘿哈哈哈哈")); FText F4 = FText::FromString(str); //5.将 FName 转为 FText //(缺点:也不能被文本收集器收集) FName name(TEXT("张三三三")); FText F5 = FText::FromName(name); //在屏幕上输出F2"嘿哈嚯" GEngine->AddOnScreenDebugMessage(-1, 10, FColor::Red, F2.ToString()); //在日志输出F2 + F3 + F4 + F5 UE_LOG(LogTemp, Log, TEXT("%s\n%s\n%s\n%s\n%s"), *F2.ToString(), *F3.ToString(), *F4.ToString(), *F5.ToString());
//6.FText 转换 //① 数字转换:数字int32 转换为 FText FText F6_1 = FText::AsNumber(10); //② 百分比转换:浮点0.3 转换为 FText(30%) FText F6_2 = FText::AsPercent(0.3); //③ 金钱转换: FText F6_3 = FText::AsCurrency(666, TEXT("$")); //④ 日期转换:将秒转换为年月日(单位:秒) FText F6_4 = FText::AsDate(66666666666);
//7.FRText 的比较操作:比较两个FText是否相同 //① EqualTo():返回布尔值,不忽略大小写 if (F2.EqualTo(F3)) {} //② EqualToCaseIgnored():忽略大小写 if (F2.EqualToCaseIgnored(F3)) {}
}
#undef LOCTEXT_NAMESPACE //卸载LOCTEXT宏
五. 容器
- 容器是方便存储数据的载体,虚幻提供了三种同质容器(只能用来存储相同类型的数据):
- TArray(只有TArray可以使用UPROPERTY宏说明)
- TMap
- TSet
5.1 TArray
- UE中的数组
- 速度快,内存消耗小,安全性高
- TArray 的操作:
-
① 初始化构建与元素遍历
void AMyGameModeBase::BeginPlay() { //1.容器在初始化构建时,直接构建为栈对象,不要new为堆对象! TArray<FString> Array; //2.使用初始化函数添加元素(10个“牛逼”) Array.Init(TEXT("牛逼"), 10); //3.遍历元素: Num()获取TArray当前元素个数 for (int32 i = 0; i < Array.Num(); i++) { UE_LOG(LogTemp, Log, TEXT("%s"), *Array[i]); } //3.新语法进行遍历 for (auto& i : Array) { UE_LOG(LogTemp, Log, TEXT("NEW: %s"), *i); } }
-
-
- ② 添加元素
void AMyGameModeBase::BeginPlay() { //从之前初始化创建的10个元素的数组,添加3条,又接一个长度3的数组,变成16个元素 //4.Add()添加元素到数组末尾,返回当前添加元素在数组中的下标 int32 Array_A = Array.Add(TEXT("你爹")); //5.Emplace()添加元素到数组末尾,返回下标 Array.Emplace(TEXT("你爹1")); //6.AddUnique()只添加数组中没有的元素,如果数组中已存在该元素,则返回该元素在数组中的下标。 Array.AddUnique(TEXT("你爹2")); //7.Append()复制普通数组到TArray容器中 //ARRAY_COUNT(NewArray)获取新数组长度 FString NewArray[3]{ TEXT("我是新数组") }; Array.Append(NewArray, ARRAY_COUNT(NewArray)); }
- ② 添加元素
-
- ③ 插入元素
void AMyGameModeBase::BeginPlay() { //8.Insert(要插入的元素内容, 插入的数组下标位置) //如果插入位置超过容器的大小,将会报错 //插入后元素依次向后排列,数组长度+1 Array.Insert(TEXT("插入元素"), 5); }
- ③ 插入元素
-
- ④ 设置容器的大小
void AMyGameModeBase::BeginPlay() { //9.SetNum函数,用来设置容器的大小 TArray<int32>NewArray; NewArray.SetNum(20); }
- ⑤ 迭代器
void AMyGameModeBase::BeginPlay() { //10.迭代器(设计模式):不允许修改元素个数、添加、移除 //① 创建只读迭代器,只能读取 for (auto i = Array.CreateConstIterator(); i; ++i) { UE_LOG(LogTemp, Log, TEXT("%s"), **i); } //② CreateIterator()创建迭代器函数,可以通过迭代器修改元素内容 for (auto i = Array.CreateIterator(); i; ++i) { //修改Array元素 *i = FString(TEXT("改个元素内容")); UE_LOG(LogTemp, Log, TEXT("%s"), **i); } }
- ④ 设置容器的大小
-
- ⑥ 转成普通数组
void AMyGameModeBase::BeginPlay() { //11.转成普通数组 //返回类型指针,指针地址是数组中第一个元素的地址 FString* NewArray = Array.GetData(); }
- ⑦ 常规查询函数
void AMyGameModeBase::BeginPlay() {//11.常规查询函数 //① IsValidIndex():数组在该下标中的元素是否有效,返回布尔值 Array.IsValidIndex(5); //② Last():返回容器中最后一个元素 // Last(3):返回容器中倒数第三个元素 // Top():返回容器中第一个元素 Array.Last(); Array.Last(3); Array.Top(); //③ Contains():包含,返回布尔值 Array.Contains(TEXT("牛")); //④ Find():查询,返回该元素在数组中的下标(没有则返回-1) Array.Find(TEXT("牛逼")); //查询给定元素所在位置,将位置索引设置到Index,返回布尔值 //(即能返回布尔,也会返回元素所在位置) int Index = 0; Array.Find(TEXT("牛逼"), Index); }
- ⑧ 常规移除函数
void AMyGameModeBase::BeginPlay() { //12.常规移除函数 //① 在容器中移除给定元素,返回移除的元素个数 int32 RemoveNum = Array.Remove(TEXT("牛逼")); //② 移除容器中找到的第一个给定元素 //(返回1表示移除成功,返回0则说明容器中没有这个元素) int32 RemoveIndex = Array.RemoveSingle(TEXT("牛逼")); //③ 移除给定下标位置的元素 Array.RemoveAt(2); //④ 清空容器 Array.Empty(); }
- ⑥ 转成普通数组
5.2 TMap
- 关联型容器,存储对象均有一个关联值,通过键值可以高效的进行对象访问
- Map的结构例如钥匙和锁,一把钥匙找一把锁,他们之间是对应的关系
- 映射被销毁时,其元素也将被销毁
- 键类型必须为值类型,不能使用指针
- TArray 的操作:
-
① 初始化构建、添加元素与元素遍历
void AMyGameModeBase::BeginPlay() { //1.初始化构建容器,键值类型为FString,数据类型为int32 TMap<FString, int32>Map; //2.添加元素 //注意:添加Map容器中已有的键值,会覆盖之前键值对应的元素 Map.Add(TEXT("K1"), 111); //添加元素时,只添加键值会使用默认元素值 Map.Add(TEXT("K2")); //可以用这种方法修改数据内容,但不能添加 Map[TEXT("K2")] = 222; //3.Append()合并元素 //将NewMap中的键值和对应元素,复制到Map TMap<FString, int32>NewMap; Map.Append(NewMap); //4. 元素遍历 for(auto& Item : Map) { Item.Key; //获取键值 Item.Value; //获取元素 UE_LOG(LogTemp, Log, TEXT("% s,%d"), *Item.Key, Item.Value); } }
-
-
- ② 迭代器
void AMyGameModeBase::BeginPlay() {//5.迭代器 for (auto i = Map.CreateConstIterator(); i; i++) { //禁止修改 i.Key(); i.Value(); } for (auto i = Map.CreateIterator(); i; i++) { //获得键值 i.Key(); i.Value() = 100; i->Value; } }
- ③ 查询函数
void AMyGameModeBase::BeginPlay() {//6.查询函数 //① 查询容器中元素个数 Map.Num(); //② 是否包含 Map.Contains(TEXT("111")); //③ 返回键值对应的元素指针,如果没找到内容则返回空指针 Map.Find(TEXT("222")); }
- ④ 移除函数
void AMyGameModeBase::BeginPlay() {//7.移除函数 //① 使用给定键值移除元素 Map.Remove(TEXT("K1")); //② 清空容器 Map.Empty(); }
- ② 迭代器
5.3 TSet
- 也是键值容器(和TMap类似),但速度快,无需提供单独的键进行关联元素,不允许有重复的键
-
TSet是KV容器,不保证数据填充顺序。
-
TSet数据存储时无法重复存储,TArray可以
- TSet 的操作:
-
① 初始化构建、添加元素与元素遍历
void AMyGameModeBase::BeginPlay() { //1.初始化构建 TSet<FString>Set1; //2.添加元素 Set1.Add(TEXT("哟西")); //3.允许合并操作 TSet<FString>Set2; Set1.Add(TEXT("斯国一")); Set2.Append(Set1); //将Set1中元素复制一份添加到Set2中 //4.元素遍历 for (auto& Item : Set2) { UE_LOG(LogTemp, Log, TEXT("%s"), *Item); } }
-
-
- ② 迭代器
void AMyGameModeBase::BeginPlay() {//5.普通迭代器 for (auto Item = Set1.CreateIterator(); Item; Item++) { } //6.只读迭代器 for (auto Item = Set1.CreateConstIterator(); Item; Item++) { *Item; //获取内容,禁止修改 } }
- ③ 查询函数
void AMyGameModeBase::BeginPlay() {//7.获取容器中元素类型 Set1.Num(); //8.检查元素中是否包含给定的键值 Set1.Contains(TEXT("西")); //9.如果查找某一元素是否存在,可直接使用Find进行单一查找,返回找到指向元素的指针,没找到则返空 Set1.Find(TEXT("西")); //10.将TSet容器转为TArray容器 Set1.Array(); }
- ④ 移除函数
void AMyGameModeBase::BeginPlay() {//11.移除给定键值内容,成功返回1,失败返回0 Set1.Remove(TEXT("西")); //12.移除所有元素,释放空间 Set1.Empty(); //13.移除元素,但不释放空间 Set1.Reset(); }
- ② 迭代器
六. 宏标记
- 虚幻引擎是模块化编程,先来了解一下继承关系中头文件描述
6.1 UObject
- UE中使用的对象大部分继承自UObject(顶级父类)
- UObject具备如下优点:
-
- 垃圾回收
- 引用更新(更新受限于GC回收机制,需使用UPROPERTY宏标记)
- 序列化(场景中的Actor被保存时发生)
- 默认属性变化自动更新
- 源码调整了对象属性时,编译后会自动更新到资源实例上,前提场景实例资源没有修改过属性值,修改过后将使用场景中的修改内容做填充参考
- 自动属性初始化
- 自动编辑器整合
- 运行时类型信息可用
- 运行时使用Cast可以进行类型信息投射检查(Cast To)
- 网络复制
3. 创建UObject类对象:
-
- NewObject()是最为简单的UObject工厂模式
- 它需要可选的外部对象和类,并会创建拥有自动生成的名称的新实例
6.2 UClass 宏
- 用于标记从UObject派生的类,使得UObject处理系统识别到它们
- 语法结构:
UCLASS(描述指令,描述指令,…)
-
六个常用宏标记:
//① 可以作为蓝图中的一种变量类型使用,类默认均可被蓝图访问 UCLASS(BlueprintType) //② 默认可被继承,标记关系向子类传递,子类可覆盖描述关系 UCLASS(Blueprintable) UCLASS(BlueprintType,NotBlueprintable) // 可访问,但不可继承 //③ 将类声明为“抽象基类”,这样会阻止用户在虚幻编辑器中向这个世界中添加这个类的Actor,或者在游戏过程中创建这个类的实例 UCLASS(Abstract) //④ 所有属性及函数均为常量,并应作为常量导出。该标识由子类继承 UCLASS(Const) //⑤ 类内的成员变量数据信息保存到本地配置文件中,需要显式调用函数SaveConfig使用,并配合UPROPERTY宏操作 UCLASS(Config=Game) //⑥ 用来配置组件在添加时分组情况 UCLASS(ClassGroup = (TestClass01))
6.3 UFUNCTION 宏
- 语法结构:
UFUNCTION(指令,指令..,meta(key=value)) //注意:在UFUNCTION修饰的函数中,如果参数类型是引用型参数,则在蓝图中将当做返回参数使用,无法查到输入针脚。
// 如果参数类型是const修饰的引用型参数,则参数被当做输入针脚使用 - 五个常用宏标记:
//1. BlueprintCallable:表明此函数可在蓝图中被调用(当类被蓝图继承后才有效果) UFUNCTION(BlueprintCallable) void Func(int32& A, const int32& B); //void Func(输出针脚, 输入针脚) //如果函数参数是引用类型,则在蓝图中调用被当做输出针脚 //如果传入参数是const修饰的引用类型,则在蓝图中被当做输入针脚 //2. Category:标明此函数在蓝图中的分类 Category=”UE4Test|MyActor” // |符号用来划分分类级别 UFUNCTION(BlueprintCallable,Category=”UE4Test|MyActor”); //3. BlueprintImplementableEvent:用此标记可以在C++中构建函数声明,但是定义由蓝图完成,从而达到C++向蓝图进行调用的目的,在CPP无需定义 public: UFUNCTION(BlueprintImplementableEvent, Category = "UECPP|ACPPActory") void Func(); //必须放在公有或受保护的访问域中,不能在C++中定义函数(本质上相当于纯虚函数virtual) //注意: //① 无参无返,则在蓝图中当作事件Event使用; //② 无参有返,则在蓝图中当作函数使用(需要在函数的overlap中寻找) //③ 有参无返(基本数据类型),当作普通事件输入参数使用 //④ 有参无返(自定义数据类型,如FString),编译不过 //⑤ 有参无返(基本数据类型/自定义数据类型引用),当作函数看待,在函数表中寻找 //4. BlueprintNativeEvent:标记的函数只能在c++中调用,在蓝图中无法调用,此标记可以标注函数可在蓝图中被重写,并且具备在C++中有另一个标记函数 UFUNCTION(BlueprintNativeEvent) void Func01(int32 Num); //在蓝图中调用 void Func01_Implementation(int32 Num); //如果蓝图重写此函数,则函数实现在蓝图,如果蓝图没有重写此函数,则函数实现在(函数名_ Implementation)上 //蓝图中实现后,可以右键函数节点,选择add call to parent function可以调用父类的函数逻辑(类似类中的虚函数,在继承关系中子类可以调用父类的虚函数) //注意: //① 无参无返,则在蓝图中当作事件Event使用; //② 无参有返,则在蓝图中当作函数使用(需要在函数的overlap中寻找) //5.BlueprintPure:特殊标记,构建一个蓝图中的纯函数,用来获取对象数据 //此标记的函数必须有有效返回值(无返回值编译报错),且在蓝图中无法加入到执行队列,只能以输出值的操作方式被使用,定义实现均放在cpp中 UFUNCTION(BlueprintCallable, BlueprintPure)
6.4 UPROPERTY 宏
- 语法结构:
UPROPERTY(标记,标记,...,meta(key=value,key=value,...) //用于将对象属性暴露到蓝图中操作
- 常用宏标记:
- ① Category:分组(定义属性的分类)
UPROPERTY(Category=CategoryName) Type VariableName; UPROPERTY(Category="CategoryName|SubCategoryName") Type VariableName;
UPROPERTY(EditAnywhere, Category = "MyCategory") int CategoryNum; UPROPERTY(EditAnywhere, Category = "MyCategory|SubCategory") int SubCategoryNum;
- ② Blueprint
//----------以下2个不兼容,只能取其一: //① BlueprintReadOnly:只读 UPROPERTY(BlueprintReadOnly) //② BlueprintReadWrite:可读可写 UPROPERTY(BlueprintReadWrite) //----------以下2个仅能用于Multicast代理: //③ BlueprintAssignable:应显示该属性,以供在蓝图中分配 UPROPERTY(BlueprintAssignable) //④ BlueprintCallable:应显示该属性,以在蓝图代码中调用 UPROPERTY(BlueprintCallable)
- ③ Edit
//① EditAnywhere:可从编辑器内的属性窗口编辑,在原型和实例中 //(可以在编辑器窗口中进行编辑,也可在场景细节面板中编辑) UPROPERTY(EditAnywhere) //② EditDefaultsOnly:可通过属性窗口来编辑,仅能对原型编辑 //(可以在蓝图编辑器中编辑原型数据,但无法在场景细节面板中编辑场景中的具体对象) UPROPERTY(EditDefaultsOnly) //③ EditInstanceOnly:可通过属性窗口来编辑,仅能对实例而非原型进行编辑 //(属性的修改权限在实例,不能在蓝图编辑器原型中修改) UPROPERTY(EditInstanceOnly) //④ EditFixedSize:限定动态数组长度,禁止用户在蓝图属性面板中修改(单一添加无法显式,需要配合使用上面两个标记) //只限制修改数组大小,不限制普通变量 UPROPERTY(BlueprintReadWrite, EditAnyWhere, EditFixedSize) //⑤ EditInline:使得用户可编辑UnrealEd的属性查看器中的变量所引用的对象属性 //(仅对对象引用可用,包括对象引用数组) UPROPERTY(EditInline)
- ④ Visible
//① VisibleAnywhere:在属性窗口可见但无法编辑(原型实例中均可看到) //如果标记组件指针,则表示组件内容在场景和编辑器的细节面板中显示所有编辑项 //如果标记的是普通属性(指针、基本数据类型、复合数据类型),则在场景和编辑器中只显示,无法编辑 UPROPERTY(VisibleAnywhere) //② VisibleDefaultsOnly:仅在原型蓝图编辑器属性窗口中可见,无法编辑 //如果标记组件指针,则表示组件内容在场景和编辑器的细节面板中显示所有编辑项 //如果标记的是普通属性(指针、基本数据类型、复合数据类型),则只在编辑器中会显示(不在场景细节面板中显示),无法编辑 UPROPERTY(VisibleDefaultsOnly) //⑩ VisibleInstanceOnly:仅在实例属性窗口中可见,无法编辑 //如果标记组件指针,则表示组件内容在场景和编辑器的细节面板中显示所有编辑项 ///如果标记的是普通属性(指针、基本数据类型、复合数据类型),则只在场景细节面板中会显示(不在编辑器中显示),无法编辑 UPROPERTY(VisibleInstanceOnly)
- ⑤ 其他
- AdvancedDisplay:将属性信息在细节面板中隐藏到高级显式项内容中
UPROPERTY(EditAnywhere, Category = "Adv") int AdvNum1;
UPROPERTY(EditAnywhere, AdvancedDisplay, Category = "Adv") int AdvNum2; - Config:标记此属性可被存储到指定的配置文件中,启动时属性内容将从配置文件中获取
UPROPERTY(Config)
- meta=(DisplayName=”别名”):别名标记指令
//给函数或属性取一个别名,用于蓝图显示和搜索 //可用在修饰属性和函数上,要和暴露到蓝图中的宏连用 UPROPERTY(BlueprintReadWrite, EditAnyWhere, meta=(DisplayName=”取一个别名”))
-
成员属性值域约束
//只在UI上约束,填入数据不约束 UPROPERTY(EditAnyWhere, meta = (UIMin="10", UIMax = "20")) //UI上约束,填入数据也被约束 UPROPERTY(EditAnyWhere, meta = (ClampMin = "0", ClampMax = "10"))
-
成员属性修改约束
//EditCondition="bShow",借助一个布尔变量用来控制另一个变量是否可以在面板中被修改 UPROPERTY(EditAnywhere, EditFixedSize) bool bShow; UPROPERTY(EditAnywhere, AdvancedDisplay, meta = (ClampMin = "0", ClampMax = "10", EditCondition="bShow")) int32 wock; //蓝图可以访问C++中private中的属性,作用于在private中不能使用BlueprintReadWrite,加了(AllowPrivateAccess = “true”)之后就会把C++中私有的属性继承到蓝图并变成公有属性 UPROPERTY(EditAnywhere, EditFixedSize, meta = (AllowPrivateAccess = “true”))
- AdvancedDisplay:将属性信息在细节面板中隐藏到高级显式项内容中
- ① Category:分组(定义属性的分类)
七. 结构体与枚举
7.1 结构体
- 由于蓝图特殊,普通的结构体定义无法被蓝图访问,我们需要借助 USTRUCT 宏进行构建UE中的结构体
USTRUCT(BlueprintType) //借助 USTRUCT宏 构建结构体,(BlueprintType:在蓝图中使用) struct FStructName // 结构体名称必须使用F开头 { GENERATED_USTRUCT_BODY(); //再带一个操作宏 int32 X; int32 Y; }
7.2 枚举
- 构建枚举的两种方法
//(在GameMode.h文件中)
//第一种方式:空间构建枚举 UENUM(BlueprintType) //宏标记(BlueprintType将类型暴露到蓝图中使用) namespace sColor { enum Type { Red, Blue, Yellow, }; }
//第二种方式:构建枚举 UENUM(BlueprintType) enum class EColor :uint8 { Red, Blue, };
UCLASS(minimalapi) class AUECGameMode : public AGameModeBase { GENERATED_BODY() public:
//第一种方式构建枚举的两种定义方式:
//① 定义枚举(只能在C++中使用,不能在蓝图中使用) //使用空间名称作为访问依据,可以更清晰的标明意图,方便使用 sColor::Type Etype; //② 定义枚举(既可以在C++中使用,也可以在蓝图) //将变量暴露到蓝图中使用,必须要加 UPROPERTY 相应的宏标记 UPROPERTY(EditAnyWhere,BlueprintReadWrite) TEnumAsByte<sColor::Type> Color;//第二种方式定义枚举:
//可以暴露到蓝图中使用,但需要对应的宏标记 UPROPERTY(EditAnywhere, BlueprintReadWrite); EColor ColorType;
7.3 UMETA 宏
- 可以帮助枚举名进行蓝图别名创建,方便在蓝图中寻找操作(空间声明枚举的方式不适用)
UENUM(BlueprintType) //必须加上 enum ZQState { Game UMETA(DisplayName = 'ZWJ') };
八. 资源加载
- 在虚幻中我们需要处理的资源分为:类资源和非类资源
- 资源/资产加载到内存中,我们操作需要通过引用来完成,引用分两类:
- 直接属性引用(硬性引用):直接引用
- 间接属性引用(软性引用):通过间接机制(如字符串形式的对象路径)来引用
- 加载方式分为:
- 同步加载(资源过大会导致游戏程序卡顿)
- 在加载运行线程中,阻塞线程的流程执行,将线程停止在当前加载逻辑中,加载完成后继续线程的执行逻辑操作
- 对于加载小资源可以保证资源的不为空,但是加载大资源将导致调用线程卡顿
- 异步加载
- 在加载线程中,不阻塞当前线程逻辑加载资源,加载器本身具备线程进行资源加载
- 比同步加载更加灵活,但维护成本较高,资源加载成功后需要进行回调通知,以完成整个加载流程
- 同步加载(资源过大会导致游戏程序卡顿)
8.1 直接属性引用
1. 编辑器直接加载
- 通过使用属性 UPROPERTY 宏标记,将资产对象指针暴露到编辑器面板,从而直接从编辑器面板拾取资产
2. TSubClassOf
- 是提供UClass的安全模板类,可以快速构建某种类型对象数据
- 但只能选取模版类型或继承自模版类型的类 / 蓝图
//(在.h文件中)
//1.引用类资源//① UClass类:暴露到蓝图中,直接拾取任意类资源(用指针) UPROPERTY(EditAnyWhere, BlueprintReadWrite) UClass* CharacterClass;
//② TSubclassOf:只能用来拾取 AMyActor 类或其子类(用<>包裹)(记得加头文件) UPROPERTY(EditAnyWhere, BlueprintReadWrite) TSubclassOf<AMyActor> MyActor; MyActor.GetClass(); //获取UClass指针
//2.引用非类资源UPROPERTY(EditAnyWhere, BlueprintReadWrite) USoundBase* Sound; //拾取音频资源
//拾取音频还可以用:USoundCue(加头文件#include<Sound/USoundCue.h>) UPROPERTY(EditAnyWhere, BlueprintReadWrite) UStaticMesh* Mesh; //拾取静态网格体 UPROPERTY(EditAnyWhere, BlueprintReadWrite) UStaticMeshComponent* Mesh; //静态网格体组件(装载资产的组件) UPROPERTY(EditAnyWhere, BlueprintReadWrite) USkeletalMesh* SMesh; //拾取骨骼网格体//3.加载类资源和非类资源
//UObject类 UPROPERTY(EditAnyWhere, BlueprintReadWrite) UObject* Object;
3. 构造函数加载
- 借助构造函数资产加载类进行资源引用
- ConstructorHelpers可以进行类引用,源资源引用
- ConstructorHelpers 只能在构造函数中使用,GameInstance中是 Init 函数(需要重载)
- 常用的资源加载分类:
-
FClassFinder:加载创建后的蓝图对象
ConstructorHelpers::FClassFinder<APawn>(TEXT("/Game/Flappybird/Blueprints/BP_Bird")).Class;//返回数据类型是TSubClassof //使用路径拾取蓝图对象(要加_C) ConstructorHelpers::FClassFinder<AActor> UnitSelector(TEXT(“Blueprint‘/Game/Blueprints/MyBlueprint.MyBlueprint_C’”));//下划线C必须要加
-
FObjectFinder:加载各种资源(如音频,图片,材质,静态网格)
ConstructorHelpers::FObjectFinder<UTexture2D> BarFillObj(TEXT("/Game/UI/HUD/BarFill")); BarFillTexture = BarFillObj.Object; //将获取的数据内容指针保存
- 注意:
- 操作路径前加入 /Game/ 前缀
- 加载蓝图类模版对象时,需要加注“_C”
- 加载失败或是未找到资源,对象内的资产属性为null
-
4. 查找加载
- 在只知道目标资源路径的基础上,进行运行时态的资源加载
- 资源加载可能会失败或是无效,所以需要对操作的结果进行判定
- LoadClass:加载类资产,可放在任何地方加装 , 返回UClass指针,可用TSubclassOf来接
TSubclassOf<AMyActor> Player1 = LoadClass<AMyActor>(NULL, TEXT("Blueprint'/Game/BP/BP_Actor.BP_Actor_C'"));
- LoadObject:加载类资源 + 非类资产,返回该类的指针
AMyActor* Player2 = LoadObject<AMyActor>(NULL, TEXT("Blueprint'/Game/BP/BP_Actor.BP_Actor_C'"));
- LoadClass:加载类资产,可放在任何地方加装 , 返回UClass指针,可用TSubclassOf来接
5. 路径引用
- 此处的 “文件路径” 是复制绝对路径
8.2 间接属性引用
1. FSoftObjectPath
- 是一个简单的结构体,使用一个字符串包含资源的完整名称
- 可以在编辑器中拾取资源(与直接属性引用相同),但没有把资源加载到内存(资源的加载需要通过额外的代码编写完成)
- 对于资源的拾取并没有特定的要求,所有能够被序列化的资源均能被拾取(类资源,非类资源)
2. FSoftClassPath
- 仅拾取类资源链接
//(在.h文件中) //间接属性引用
//1.FSoftClassPath,用于拾取类资源的链接 UPROPERTY(EditAnywhere) FSoftClassPath CharacterPath;
//2.FSoftObjectPath:(类资源 + 非类资源)暴露到蓝图中拾取的是一个链接,不会进行资源加载 UPROPERTY(EditAnyWhere) FSoftObjectPath SoundPath;
8.3 同步加载和异步加载
- 同步加载(加载大资源将导致调用线程卡顿)
- 异步加载(资源加载成功后需要进行回调通知)
- FSreamableManager 构建异步处理逻辑,创建在全局游戏的单例对象中,结合FSoftObjectPath进行加载
//(在UECCharacter.h文件中) #include <Engine/StreamableManager.h> //资源加载器头文件引入
UCLASS(config=Game) class AUECCharacter : public ACharacter { public://1.同步加载 //加载播放音效资源 void LoadPlaySound();
//2.异步加载 //① 声明资源加载器(构建为栈对象,需引入头文件,不要构建为堆对象) FStreamableManager LoadStream; //② 回调通知函数 void LoadCallBack();
};
//(在UECCharacter.cpp文件中) #include <Kismet/GameplayStatics.h> //播放音频头文件引入 void AUECCharacter::LoadPlaySound() { if (!SoundPath.IsValid()) //判断资产链接是否有效 { //如果资产链接无效,则直接设置资产路径(可以用来替代细节面板中的引用) SoundPath.SetPath(TEXT("SoundCue'/Game/StarterContent/Audio/Collapse_Cue.Collapse_Cue'")); }
//尝试加载资源 UObject*Source = SoundPath.TryLoad();//1.同步加载(会阻塞线程) UObject* Source = LoadStream.LoadSynchronous(SoundPath);
//2.异步加载 //① 请求加载,并绑定回调函数(资源,单播) LoadStream.RequestAsyncLoad(SoundPath,FStreamableDelegate::CreateUObject(this,&AUECCharacter::LoadCallBack));
}
//② 回调函数调用(表明异步加载完成) void AUECCharacter::LoadCallBack() { //类型转换Cast //SoundPath.ResolveObject():获取异步加载好的资源引用 USoundBase* Sound = Cast<USoundBase>(SoundPath.ResolveObject()); if (Sound) { //播放音效 UGameplayStatics::PlaySound2D(GetWorld(), Sound); } }
- FSreamableManager 构建异步处理逻辑,创建在全局游戏的单例对象中,结合FSoftObjectPath进行加载
8.4 模板资源拾取类
1. TSoftObjectPtr和TSoftClassPtr
- TSoftObjectPtr和TSoftClassPtr也分为同步加载与异步加载
UPROPERTY(EditAnywhere) TSoftObjectPtr<class UStaticMesh> SoftMesh;
UPROPERTY(EditAnywhere) TSoftClassPtr<class ATestActor> SoftTestActor;
- TSoftObjectPtr
- 同步加载
//(.h 文件) UPROPERTY(EditAnywhere) TSoftObjectPtr<class UStaticMesh> SoftMesh; // 构建为栈对象,需要引入头文件,不要构建为堆对象 // #include "Engine/StreamableManager.h" FStreamableManager m_Streamable;
//(.cpp文件) // 可以转换为FSoftObjectPath对象 SoftMesh.ToSoftObjectPath(); // 同步加载 UObject* Source = m_Streamable.LoadSynchronous(SoftMesh); UStaticMesh* Mesh = Cast<UStaticMesh>(Source); if (Mesh) { UKismetSystemLibrary::PrintString(this, TEXT("加载成功!")); }
- 异步加载
//(.h文件) // 异步加载回调函数 void LoadSourceCallback();
//(.cpp文件) // 异步加载 //需要在初始化函数中绑定回调函数,然后实现回调函数 m_Streamable.RequestAsyncLoad(SoftMesh.ToSoftObjectPath(), FStreamableDelegate::CreateUObject(this, &UTestGameInstance::LoadSourceCallback));
- 同步加载
- TSoftClassPtr
- 同步加载
//(.h文件) UPROPERTY(EditAnywhere) TSoftObjectPtr<class UStaticMesh> SoftMesh; // 构建为栈对象,需要引入头文件,不要构建为堆对象 // #include "Engine/StreamableManager.h" FStreamableManager m_Streamable;
//(.cpp文件) // 同步加载 TSubclassOf<ATestActor> TestActorClass = m_Streamable.LoadSynchronous(SoftTestActor); if (TestActorClass) { GetWorld()->SpawnActor<ATestActor>(TestActorClass); }
- 异步加载
//(.cpp文件) // 异步加载 m_Streamable.RequestAsyncLoad(SoftTestActor.ToSoftObjectPath(), FStreamableDelegate::CreateUObject(this, &UTestGameInstance::LoadSourceCallback)); //回调函数实现 void UTestGameInstance::LoadSourceCallback() { // 此函数调用,则表明异步加载完成 UClass* TestActorClass = SoftTestActor.Get(); if (TestActorClass)
{ GetWorld()->SpawnActor<ATestActor>(TestActorClass); } }
- 同步加载
九. 代理
- 代理可以解决一对一或一对多的任务分配工作,还可以解决通知问题
- 通过代理完成调用某一个对象的一个函数,而不直接持有该对象的任何指针
- (要调用某个函数,但并不直接去调用,而是通过另一个入口去调用,这就是代理)
- 代理分为:
- 单播代理(只能进行通知一个人)
- 多播代理(可以进行多人通知)
- 动态代理(可以被序列化,体现在可与蓝图进行交互,C++中可以将通知事件进行蓝图广播)
9.1 单播代理
- 单播代理只能绑定一个通知对象,无法进行多个对象通知
- 通过宏进行构建,分为两种:
- 有返回类型
- 没有返回类型
- 常用绑定函数:
- BindUObject:绑定UObject类型对象成员函数的代理
- BindSP:绑定基于共享引用的成员函数代理
- BindRaw:绑定原始自定义对象成员函数的代理,操作调用需要注意执行需要检查IsBound
- BindStatic:绑定全局函数成为代理
- UnBind:解除绑定代理关系
- (绑定中传递的对象类型必须和函数指针所属类的类型相同,否则绑定会报错)
- 调用执行:
- 执行Execute函数之前,需要检查是否存在有效绑定使用函数 IsBound
- Execute 调用代理通知,不安全
- ExecuteIfBound 调用代理通知,安全,但是有返回类型的回调函数无法使用此函数执行回调
- IsBound 检查当前是否存在有效代理绑定
- 构建步骤:
-
通过宏进行声明代理对象类型(根据回调函数选择不同的宏)
-
使用代理类型进行构建代理对象
-
绑定回调对象、操作函数
-
执行代理对象回调
//(在.h文件中)
//1.构建单播代理(此例以无参无返单播代理为例) //① 单播代理:无参数无返回值单播代理 DECLARE_DELEGATE(CallTest)
//② 1个参数的单播代理 DECLARE_DELEGATE_OneParam(Delegate_OneP, int32)
//③ 2个参数的单播代理 DECLARE_DELEGATE_TwoParams(Delegate_TwoP, int32, int32)
//④ 有返回值,和2个参数的单播代理 DECLARE_DELEGATE_RetVal_TwoParams(float, Delegate_RTwoP, float, float)UCLASS(config=Game) class AUECCharacter : public ACharacter { GENERATED_BODY() public:
//2. 声明无参无返的单播代理 CallTest Dnop; //3.创建要绑定到无参无返的单播代理上的函数 void Func(); virtual void BeginPlay();
};
//(在.cpp文件中)
void AUECCharacter::Func() { UE_LOG(LogTemp, Log, TEXT("---------我是无参无返的单播代理!---------")); }
void AUECCharacter::BeginPlay() { Super::BeginPlay(); //4.将Func函数绑定到单播代理 Dnop.BindUObject(this, &AUECCharacter::Func); //5.判断单播代理是否绑定 if (Dnop.IsBound()) { //6.调用代理,执行绑定的函数 Dnop.Execute(); } }
-
9.2 多播代理
- 无法构建具有返回值的多播代理,只能构建参数
-
广播
- 调用函数Broadcast,但是调用不保证执行顺序的正确性
- 构建步骤:
-
使用宏构建代理类型
-
使用代理类型构建多播代理对象
-
添加绑定代理
-
执行调用
//(在.h文件中)
//1.构建多播代理(不可以构建有返回值的) //① 无参的多播代理 DECLARE_MULTICAST_DELEGATE(Delegate_MultiNoP) //② 单播代理:有2个参数的单播代理 DECLARE_MULTICAST_DELEGATE_TwoParams(Delegate_MultiTwoP, int32, int32)
UCLASS(config=Game) class AUECCharacter : public ACharacter { GENERATED_BODY() public:
//2. 声明多播代理 Delegate_MultiNoP MultiNoP; Delegate_MultiTwoP MultiTwoP; //3.创建要绑定到无参无返的单播代理上的函数 void Func1_MultiNoP(); void Func2_MultiNoP(); void Func1_MultiTwoP(int32 A, int32 B); void Func2_MultiTwoP(int32 C, int32 D); virtual void BeginPlay();
};
//(在.cpp文件中)
void AUECCharacter::Func1_MultiNoP() { UE_LOG(LogTemp, Log, TEXT("---------我是无参的多播代理Func1_MultiNoP()!---------")); }
void AUECCharacter::Func2_MultiNoP() { UE_LOG(LogTemp, Log, TEXT("---------我是无参的多播代理Func2_MultiNoP()!---------")); }
void AUECCharacter::Func1_MultiTwoP(int32 A, int32 B) { UE_LOG(LogTemp, Log, TEXT("---------我是有2个int参AB的多播代理Func1_MultiTwoP!---------")); }
void AUECCharacter::Func2_MultiTwoP(int32 C, int32 D) { UE_LOG(LogTemp, Log, TEXT("---------我是有2个int参CD的多播代理Func1_MultiTwoP!---------")); }
void AUECCharacter::BeginPlay() { Super::BeginPlay(); //4.将多个函数绑定到多播代理 MultiNoP.AddUObject(this, &AUECCharacter::Func1_MultiNoP); MultiNoP.AddUObject(this, &AUECCharacter::Func2_MultiNoP); MultiTwoP.AddUObject(this, &AUECCharacter::Func1_MultiTwoP); MultiTwoP.AddUObject(this, &AUECCharacter::Func2_MultiTwoP); //5.判断多播代理是否绑定 if (MultiNoP.IsBound()) { //6.调用多播代理,执行绑定的函数 MultiNoP.Broadcast(); } //5.判断多播代理是否绑定 if (MultiTwoP.IsBound()) { //6.调用多播代理,执行绑定的函数 MultiTwoP.Broadcast(2,3); }
//清空所有绑定函数 MultiNoP.Clear(); //移除单绑定,需要传递 FDelegateHandle(代理句柄) FDelegateHandle DH_Fun = MultiTwoP.AddUObject(this, &AUECCharacter::Func1_MultiTwoP); MultiTwoP.Remove(DH_Fun); //移除某个对象中的所有函数绑定 MultiTwoP.RemoveAll(this);
}
-
9.3 动态代理
- 是允许被序列化的数据结构(代理可以被数据化提供给蓝图进行使用),使得可以在.cpp中调用代理广播,并将事件通知到蓝图
- UE中的大部分通知事件均使用动态代理,方便蓝图操作(如碰撞通知)
- 动态代理无法使用带有返回值的函数进行构建
- 动态代理分为
- 动态单播(但是创建动态单播还不如创建普通的多播,单播无法在蓝图中绑定,无法使用宏BlueprintAssignable修饰)
- 动态多播
- 动态代理与上面单播多播的区别:
- 动态代理的类型名称需要用 F 开头(动态代理实现了机制构建类)
- 动态代理对象类型可以使用UPROPERTY标记;其他代理均无法使用(不加编译可过,调用就会出错)
- 动态代理绑定对象的函数需要使用UFUNCTION进行描述(因为需要跟随代理被序列化)
- 构建步骤
-
通过宏进行声明代理对象类型(根据回调函数选择不同的宏)
-
使用代理类型进行构建代理对象
-
绑定回调对象、操作函数
-
执行代理对象回调
-
十. 事件与接口
十一. 智能指针
十二. 协同开发
- 不可避免地,在学习工作中会遇到多人开发的场景
- 什么是SVN?
- 免费的代码版本管理工具,版本控制(版本回退),功能一般
- 操作简单(但容易产生奇奇怪怪的问题),适合新手入门,易于搭建局域网服务器
- 其他的版本控制工具
- Git
- 免费且功能强大,可以和 github 无缝衔接
- 概念和操作繁多,学习有一定难度
- Perforce
- 中心化管理(客户端从服务器下载、更新、修改、上传),易学易用
- 收费,适合中大型团队(小团队免费)
- Git
12.1 SVN服务器搭建和使用
1. 下载地址: http://subversion.apache.org/packages.html
2. 建议:VisualSVN server 服务端和 TortoiseSVN客户端搭配使用
3. 下载VisualSVN server,下载完成后双击安装:
4. 创建版本库:启动VisualSVN Server Manager,可以在窗口的右边看到版本库的一些信息(状态,日志,用户认证,版本库之类的)
5. 建立用户:需要建立用户和组,并且需要分配权限
6. 设置权限:
12.2 SVN客户端安装
1. 下载 ”svn小乌龟”后,进行安装:
2. 无脑安装:
3. checkout项目文件:
4. 获取URL:URL可以在SVN服务器获取到
5. Checkout 检出:
12.3 协同开发的常遇问题
附:一些杂七杂八的东西
UE 引擎相关操作
彻底删除无用的UEC文件
- 在编译器源码中删除相关文件,再删除工程文件夹对应浏览器目录下的相关文件,最后返回编译器中编译项目
- 反之,如果没删干净,下次再创建同名 C++ 类时就会出现奇奇怪怪的报错
修改引擎编辑器
VS 编辑器相关操作
VS 中的一些快捷键
- F12:跳转查看源码中函数定义
- Ctrl + F :查找
- Ctrl + 点击函数:跳转查看源码中函数定义
使用插件 Visual Assist X 小番茄的常用快捷键
- Alt + O:在源文件 / 头文件间切换
- Alt + G:在声明 / 定义间切换
- Alt + Shift + O:在整个解决方案中查找文件
- Alt + Shift + S:在整个解决方案中查找 Symbol(类、结构体、枚举、方法、变量)
修改 VS 编译选项框
- 当编译选项框过小
- 可以在工具栏任意处右键,自定义 - 命令:工具栏:标准(选择“解决方案配置”)- 修改所选内容
- 效果如下:
解决编译器中文乱码
- 如果出现中文乱码的情况:高级保存选项 -> 将编码设置为UTF-8格式
-
但是可能有些朋友会发现在 “文件” 的地方找不到 “高级保存选项” 这玩意,可能是因为你没有打开:
- 创建静态网格体组件,并设置为根组件
// 在.cpp文件: #include "Components/StaticMeshComponent.h" //静态网格体组件头文件 // 构造函数中初始化 AMyActor::AMyActor() { // 创建一个静态网格体组件,并设置为根组件 RootComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("组件别名")); }
未完待续...