1.C++基础

面向对象

../attachment/Pasted image 20250713123841.png
UE中的所有类的基类是Object(实际上是UObject),不过一般常用的是Actor。

U++反射系统

虚幻反射系统:

虚幻中的类

虚幻中对象的基类是UObject,通过UCLASS()宏来标记UObject派生的子类,使得UObject处理系统可以注意到这些类。

//在.h文件中

%% UCLASS()宏,会告诉引擎将下面这个类给管理到虚幻引擎中去待使用。 %%
UCLASS()

%% 定义一个XGBaseObject类,前缀U表示是虚幻的Object对象,后面public UObject表示公开继承自UObject %%
class XGPLANDEMO_API UXGBaseObject: public UObject
{

	%% GENERATED_BODY宏配合CLASS()宏使用 %%
	GENERATED_BODY()
}

虚幻中的模板类

TSubclassOf是模板类<不懂>

虚幻中的结构体:

%% 在UEC++中用USTRUCT()宏来标记 %%
USTRUCT()
struct FXGBaseStruct
{
	GENERATED_USTRUCT_BODY()  %% 类似于类 %%

	UPROERTY()%% 此宏可以将这个结构体反射到蓝图中 %%
	
	FString Name;
}

虚幻智能指针库

比较复杂,方便管理非object对象的行为和数据

接口

%% U类帮助实现反射到虚幻引擎 %%
UINTERFACE(MinimalAPI)
class UXGBaseInterface :public UInterface
{
	GENERATED_BODY()
}

%% I类负责实现方法 %%
class XGPLANDEMO_API IXGBaseInterface
{
	GENERATED_BODY()
	//添加方法
public:
}

Metadata说明符

控制类、接口、结构体、列举、函数或属性于引擎和编辑器各方面的交互方式,每种数据结构或成员都有自己的源数据说明符列表。

UENUM()
namespace EXGHelperEnumOldStyle
{
	enum Type
	{
		XG_TestEnum,
		XG_EnumTest,
	};
}

UENUM(BlueprintType)%% 标记让蓝图去访问 %%
%% 这里的字符串会被UHT识别到,并实现这部分显示的功能 %%
enum class EXGEnumGeneralStyle : uint8
{
	XG_None UMETA(DisplayName = "None"),
	XG_TestEnum UMETA(DisplayName = "TestEnum"),
 	XG_EnumTest UMETA(DisplayName = "XG_EnumTest")
};

函数

public:
	UFUNCTION(BlueprintCallable,meta = (BlueprintInternalUseOnly = "true",WorldContext = "WorldContextObject",Delay = "0.5"))

实战U++类创建

创建C++类,方案一

  1. 点击ue引擎的tools->New C++Class->All Class-> UObject->路径自己创建并选择(规范一些)->命名处选择UppLearn(Runtime)->不选择public或者private../attachment/Pasted image 20250713140817.png此时应该有如下警报../attachment/Pasted image 20250713141609.png此时找到UppLearn.Build.cs文件添加这段配置../attachment/Pasted image 20250713142129.png此时还有红线,但是右键UppLearn点击Build进行编译不会报错了,如果想要去掉红线警告,就重启刷新一下项目
  2. 注意:
    1. None这种类表示的是原生C++的类
    2. UppLearn(Runtime):runtime结尾表示开发的是项目模块而不是插件模块
    3. 开发项目模块是不需要考虑Public还是Private的
    4. .build.cs文件的名字根据项目名不同

U++创建类,方案二

1.直接创建文件../attachment/Pasted image 20250713145002.png之后去项目根路径找到.uproject文件,右键重新生成一下。

U++创建类,方案三

在VS中创建../attachment/Pasted image 20250713150137.png../attachment/Pasted image 20250713150440.png

创建接口

在tools->new c++ class->common class ->Unreal Interface->创建一个XGBaseInterface,在BaseType路径下

创建函数库

tool->new c++ class ->common class ->BluePrintFunctionLibrary->创建一个XGBaseBPLibrary库,在BaseType路径下

创建插件

Edit->Plugin->左上角add->选择BluePrintLibarary->工程取名(XGSampleTool)->输入作者名称->勾选测试创建一个插件
../attachment/Pasted image 20250714000525.png在编译过后可见如下这样一个层级模块。
../attachment/Pasted image 20250714002207.png其中:public中的XGSampleTool.h里面是模块的名字;XGSampleToolBPLibrary.h中使用了GENERATED_UCLASS_BODY()宏,这导致初始化的两个对象时不能传入参数(一般放在BeginPlay里初始化,二阶初始化)。
然后根据注释可知:蓝色执行节点(CallCable),绿色执行节点(Pure)。Pure的绿色节点每次调用必定执行一次。

创建结构体和枚举

直接在BaseType的路径下创建了,并去.uproject哪里右键刷新项目了

之后

再尝试在蓝图系统中使用创建的XGBaseObject类等类的时候,你会发现无法提升为变量。
右键选择从类创建对象,并选择为XGBaseObject类,此时右键return value会发现无法提升成变量
../attachment/Pasted image 20250714004938.png../attachment/Pasted image 20250714005206.png
尝试到XGBaseObject.h,XGBaseStruct.h中的UCLASS()宏,USTRUCT()宏的括号中添加BlueprintType

//XGBaseStruct.h

#pragma once
#include "CoreMinimal.h"
#include "XGBaseStruct.generated.h"

USTRUCT(BlueprintType)
struct FXGBaseStruct
{
	GENERATED_USTRUCT_BODY();
public:
	UPROPERTY()
	FString StructName = TEXT("");
}

此时可以将这些类提升为变量了。
注意:

  1. 在USTRUCT(),UCLASS()这样的反射宏添加BlueprintType就可以让这个类,这个对象被提升为变量来使用了。
  2. GENERATED_BODY()和GENERATED_UCLASS_BODY()这两个宏的区别,使用了后者就必须在cpp文件中声明类的构造函数。否则编译不过(后者已经过时了)。
  3. UE中U类用于实现功能逻辑,F类用于属性组织和操作数据结构。F雷诺不能用UFUNCTION将方法暴露给蓝图,如果要在蓝图调用结构体的方法需要特殊操作。

类默认对象

UObject是所有类的基类,它的父类UObjectBaseUtility它提供了一些工具类的方法(这个类一般不归我们管),UObjectBase是UObject的更底层的实现。我们只关注UObject就行了。

UE的反射系统会记录所有的UObject,在蓝图中右键输入FindComponent byTag,Find by Class,这两个是反射系统常见的方法,可以用于查找被记录的UObject类

部分宏

UCLASS()宏创建类

  1. 情景:创建一个XGClassActor(VS中右键BaseType文件夹add 虚幻引擎项->常见项->actor->命名XGCLASSActor->添加到BaseType路径下)
  2. 这里有一个坑,复现方式:将项目编译,进入UE,再用XGClassActor创建一个蓝图类,再将XGClassActor和衍生出来的蓝图类拖入世界中,你会发现这两个类都没有根节点这回事(或者说没有变换移动等的)。../attachment/Pasted image 20250716082228.png../attachment/Pasted image 20250716083133.png../attachment/Pasted image 20250716083154.png如图C++的类是没有根节点
  3. 解决方案:添加这行代码来指定根组件(创建根节点)../attachment/Pasted image 20250716083243.png
  4. 注意:Uclass标记的类会在游戏中创建一个默认模板对象CDO,这样可以避免游戏中反复创建对象,造成性能和安全问题。游戏世界太复杂,避免运行时创建。

UPROPERTY()宏创建属性

可以看见我门定义信息大多都是借助头文件定义,在头文件中利用宏设置一些信息的权限功能等,再由cpp去调用,这里的UPROPERTY()宏时用于将下面这个数据暴露给蓝图的(内部的参数表示这个变量在蓝图中可以读也可以写,可以在任何位置对属性进行编辑)。
../attachment/Pasted image 20250720223610.png
当然初始化数据还有别的方式。../attachment/Pasted image 20250720223921.png
如图在cpp中去改h中的变量。.h文件中100最先被覆盖,其次时类外部定义的200,最后才是300。
此时再查看那个基于XGClassActor的蓝图类../attachment/Pasted image 20250720224358.png已经存在Money这个变量了,并且还是300。

UFUNCTION宏创建方法

在.h文件声明此方法。../attachment/Pasted image 20250720225700.png然后点击左侧的箭头并选择创建addMoney。这样就能编辑方法的逻辑了。../attachment/Pasted image 20250721013833.png这里是当前函数的实现,但是现阶段不重要。重点在于实现了这个函数后。../attachment/Pasted image 20250721014254.png我们就能在蓝图中像这样调用它了。(一般关卡蓝图不写逻辑,不写业务,只用来写测试)

UObject派生类

构造方法

uobject的派生类往往有UCLASS()宏修饰,在.h头文件添加public和下方代码;这两行代码和类名同名(构造方法),这两行代码有黄色的警报,点击警报,选择在cpp中创建这几个定义的实现。之后就能写cpp文件了。
../attachment/Pasted image 20250723140052.png
这里有两个构造方法,因此需要注意构造方法执行的次序,在大型网游中,这个很重要。
Super负责在调用方法体里自己写的方法之前线调用一次父类的构造方法,初始化一些框架的属性。
UObjects不支持构造参数:唯一支持的参数就是FObjectInitializer,这是反射系统的封装和限制。因此对于Actor和Actor组件,想要初始化一些功能,就应该使用BeginPlay()方法来初始化。

虚构方法

用virtual打头的就是虚构函数。这里由红色警告,是因为只有声明没有实现,实现一下就行了。
../attachment/Pasted image 20250723223916.png
虚构方法和构造函数相反,是在对象释放时调用的函数。

建议

  1. UObjects不支持构造参数:唯一支持的参数就是FObjectInitializer,这是反射系统的封装和限制。因此对于Actor和Actor组件,想要初始化一些功能,就应该使用BeginPlay()方法来初始化。
  2. UObject如果需要运行时创建,采用NewObject构建(没做好标记会被垃圾回收系统回收),或者将CreateDefeaultSubobject用于构造器。(参考下面的两个栏)
    NewObject //使用所有可用的选项的可选参数创建实例,很灵活
    CreateDefaultSubobject<class> //创建一个组件或子对象,可以提供构建子类或返回父类的方法。
    
  3. 具体做区分什么时候用什么复杂,以及是否使用CreateDefaultSubobject太过复杂了。简单的建议就是父类有这个参数,你就带着个参数。
  4. 有修改父类属性的需求就带参,没有就不带参数。
  5. UObject永远不要使用new运算符,所有的UObject由虚幻管理内存和垃圾回收。new和delete被虚幻重载了,使用它们都过不了编译。

NewObject

右键BaseType文件夹添加虚幻常见类,命名XGObjectActor,向其中添加第二个类的声明,并创建对应的定义
../attachment/Pasted image 20250723230723.png接下来修改XGObjectActor.h的ObjectActor声明../attachment/Pasted image 20250723234250.png以及实现../attachment/Pasted image 20250723234406.png这里主要就是实现了一个方法,用于创建一个ObjectObject()实例,一个方法用于获取内部的ObjectObject实例的Health属性。再搭建这么一个蓝图(1负责初始化obob实例,2负责输出obob中的health,3负责清理垃圾)
../attachment/Pasted image 20250724000205.png
运行后1正常,2会输出20,3也正常,按完3后再按下别的东西再按2,2的输出就不可控了。即使你已经用if来确保实例必须存在了。但实际上这个对象依旧被回收了,并且if保护了个寂寞。
想解决此问题:方案一../attachment/Pasted image 20250724001124.png添加UPROPERTY()宏防止此变量被回收。
方案二:../attachment/Pasted image 20250724001525.png在实现的时候将对象添加到根上防止被回收。

CreateDefaultSubobject

../attachment/Pasted image 20250723230350.png

UObject的其他功能

除了

  1. 垃圾回收
  2. 反射(让引擎能看到c++类)外,
    还有功能:
  3. 序列化(保存游戏数据到硬盘或者从硬盘读数据)
  4. 默认属性变化自动更新(子类继承父类嘛,父类变了,子类肯定变)
  5. 自动属性初始化(你不初始化,它自动初始化到0或者null)
  6. 自动编辑器整合(UE的蓝图里或者实例的detail里能不饿能改一些属性之类)
  7. 运行类型信息可用(类型判定)
  8. 网络复制
    这些优势UStruct也有,它和UObject一样的反射和序列化能力,切一般被ue当作数值类型处理,且不会被垃圾回收

UObject的头文件

格式

  1. 包含生成的反射文件
  2. 有相应的宏写在接口或者类的上面,有需要可以加上导数宏,如果给引擎或蓝图用,类名前加上特定的标记(U,A,I)
  3. 给属性或者方法添加需要的宏FUNCTION,PROPERTY
  4. GENERATED_BODY宏不获取参数(必写宏),支持引擎要求的基础结构。UCLASS,USTRUCT,UINTERFACE都有此要求。
  5. WITH_EDITOR宏,通过此宏包裹可以让某些头文件中的方法属性在editor模式被定义。同理安卓等跨平台也有此类宏。
#if WITH_EDITOR
#include "Components/PrimitiveComponent.h"
#include "MaterialUtilities.h"
#include "MeshDescription.h"
#endif
  1. 更新对象:Ticking是ue中对象的更新方式,所有Actor均可每帧被tick,Actor和Actor组件在注册时自动调用tick函数,UObjects不具有嵌入更新能力。在必须的时候,可以使用inherits类说明符从FTickableCameObject继承即可添加此功能。

UObject的构建

NewObject

newobject是最简单的工厂方法,它接受一个可选的外部对象和类,并用自动生成的名称闯将一个新实例。

template <class T>
T* NewObject
(
	UObject* Outer = (UObject*)GetTransientPackage(),
	UClass* Class::StaticClass()
)
%% Outer:可选,一个要设置为待创建对象的外部UObject,Class:可选,一个用于指定创建对象类的UClass,返回值:指向指定类的生成实例指针。%%

NewNamedObject

template<class TClass>
TClass* NewNamedObject
(
	UObject* Outer,
	FName Name,
	EObjectFlags Flags = RF_NoFlags,
	UObject const* Template =NULL
)
%% Name:要设置为新对象的FName,Flags:可选,描述FObjectFlags枚举值;Template:可选,创建新对象时用做模板的UObject %%

ConstructObject

使用Construct()函数创建UObjects的新实例,更灵活。此函数调用StaticConstructObject(),它分配对象,执行ClassConstructor,并执行任何初始化,如加载配置属性,加载本地化属性和实例化组件。

template <class T>
T* ConstructObject(
	UClass* Class,
	UObject* Outer = (UObject*)GetTransientPackage(),
	FName Name=Name_None,
	EObjectFlags SetFlag=RF_NoFlags,
	UObject const* Template=NULL,
	bool bCopyTransientsFromClassDefault=false,
	struct FObjectInstancingGraph* InstanceGraph=NULL
)

对象标记

https://dev.epicgames.com/documentation/zh-cn/unreal-engine/creating-objects-in-unreal-engine

  1. Object Type
  2. RF_Public
  3. RF_Standalone
  4. RF_Native
  5. RF_Transactional
  6. RF_ClassDefaultObject
  7. RF_ArchetypeObject
  8. RF_Transient

TSubClassOf

在XGClassActor写入如下声明,甚至不用去实现定义。这种方式声明的变量在蓝图和C++都能用,是他俩的通讯方式。
../attachment/Pasted image 20250724125003.png../attachment/Pasted image 20250724124716.png
找到基于这个类的子类可以发现已经有MyClass属性了,这样我们就能spawn它了。
../attachment/Pasted image 20250724125406.png这样就能获取到这个类也就能spawn它了。好处是spawn限制了一个类得是某一类或它的子类。

类说明符

https://dev.epicgames.com/documentation/zh-cn/unreal-engine/class-specifiers

虚幻中的属性

UPROPERTY([specifier,specifier,...],[meta(key=value,key=value,...)])
Type VariableName;

UE的核心数据类型

uint8,uint16,uint32,uint64,int6,int16,int32,int64。一般常用int32(普通数据)和uint8(二进制数据,如颜色).
../attachment/Pasted image 20250724131130.png创建一个XGPropertyActor类测试数据类型。此处注释掉的都为UE不支持的类型。
../attachment/Pasted image 20250724131318.png创建一个对应子类,右侧确实有相关数据类型了。../attachment/Pasted image 20250724131557.png在此蓝图的事件蓝图可以访问此属性。

位掩码

位掩码属性

整数属性可以位掩码形式公开给编辑器。要将证书属性标记为位掩码,只许在meta分段添加bitmask即可

UPROPERTY(EditAnywhere,Meta=(Bitmask))
int32 BasicBits = 0;

../attachment/Pasted image 20250724215722.png
效果如图,这般定义后多了一个basic bits变量,且是个多选选项。且总共32个标记对应int32

位掩码方法

../attachment/Pasted image 20250724220823.png../attachment/Pasted image 20250724220839.png
不知道为啥不能自动生成定义,只能手动定义了。 编译成功后,我们创建新的关卡PropertyMap用于测试,拖入XGPropertyActor的子类。然后打开关卡蓝图,编辑如下测试样例。../attachment/Pasted image 20250724223935.png
经过测试设置为标记1的日志为1,标记2的日志为2,标记1和2的输出为3,无标记的输出为0。完全符合二进制位运算的状态。

使用枚举给位取名

//XGPropertyActor.h
UENUM(Meta=(Bitflags))
enum class EColorBits1
{
	ECB_Red,
	ECB_Green,
	ECB_Blue
};

UENUM(Meta = (Bitflags,UseEnumValuesAsMaskValuesInEditor = "true"))
enum class EColorBits2
{
	ECB_Red = 0x01,
	ECB_Green=0x02,
	ECB_Blue=0.04
};

//XGPropertyActor.h
UPROPERTY(EditAnywhere,Meta=(Bitmask,BitmaskEnum="EColorBits1"))
int32 ColorFlags1;

UPROPERTY(EditAnywhere,Meta=(Bitmask,BitmaskEnum="EColorBits1"))
int32 ColorFlags2;

注意:这俩都是写在头文件里的。
经过编译后此时XGPropertyActor的蓝图子类可以看到有变量如下
../attachment/Pasted image 20250724230923.png

方法配合位掩码

../attachment/Pasted image 20250724231621.png
因为带宏的函数没法自动创建,所以只好手动创建了。../attachment/Pasted image 20250724231702.png这里所作的就是将ColorBitsParam这个头文件中写出来的int32数据映射到EColorBits1里面。
../attachment/Pasted image 20250724232031.png此时直接进入这个类的蓝图子类尝试使用此方法就会发现,这个函数将位掩码这个枚举选项组作为参数了。

浮点类型

UE使用标准C++浮点类型、浮点和双精度。

布尔类型

直接用bool关键字

字符串

  • FString:典型的动态字符串数组
  • FName:对全局字符串表中不可变且不区分大小写的字符串引用。比FString更小,且更能高校传递,但更难以操控。一般不用于网络传输和同步功能。
  • FText:指定用于处理本地化的更可靠的字符串表示。本质上是相同的二进制,根据不同文本解码方式得到不同结果。
  • 大多情况下:虚幻依靠TCHAR类型来表示字符,TEXT()宏可用于表示TCHAR文字。

字符串:

../attachment/Pasted image 20250725002616.png../attachment/Pasted image 20250725002725.png

一维字符串数组

../attachment/Pasted image 20250725002539.png
../attachment/Pasted image 20250725002836.png

二维字符串数组

  1. 错误样例
%% 错误样例,编译报错 %%
UPROPERTY(BlueprintReadWrite,EditAnywhere,Category="XG")
TArray<TArray<FString>> MyStrArray;
  1. 正确方案
//XGPropertyActor.h
USTRUCT(BlueprintType)
struct FXGPropertyStruct
{
	GENERATED_USTRUCT_BODY();
public:
	UPROPERTY(BlueprintReadWrite,EditAnywhere,Category="XG")
	TArray<FString> OneStrArray;
}

UPROPERTY(BlueprintReadWrite,EditAnywhere,Category="XG")
TArray<FXGPropertyStruct> TwoTwoStrArray;

如此才能实现二位字符串数组,能简单点击右侧加号增加元素。
../attachment/Pasted image 20250725004515.png

属性说明符(没看懂)

没看懂,总之是进行一些功能的控制的。
../attachment/Pasted image 20250725002204.png
../attachment/Pasted image 20250725002314.png
多了一个EditString和一个Edite(控制是否警用EditString)

虚幻中的结构体

UStruct的定义很简单

USTRUCT([Specifier,Specifier,...])
struct FStructName
{
	GENERATED_BODY()
};

建议:

  1. UStruct可以使用虚幻引擎的只能指针和垃圾回收系统来防止垃圾回收删除UObjects
  2. 结构体最好用于简单数据类型。对于复杂的交互更适合使用UObject或者AActor子类代替
  3. UStructs不可以用于复制,但是UProperty变量可以用于复制
  4. 虚幻引擎可以自动为结构体创建Make和Break函数。
    1. Make函数出现在带有BlueprintType标签的Ustruct中
    2. 如果UStruct中至少由一个BlueprintReadOnly或BlueprintReadWrite属性,Break函数就会出现
    3. Break函数创建的纯节点为每个标记为BlueprintReadOnly或BlueprintReadWrite的资产提供一个输出引脚。
      改写原本的BaseStruct,结构体似乎不需要写cpp文件。
      ../attachment/Pasted image 20250727215624.png
      此时时可以使用的。
      ../attachment/Pasted image 20250727220111.png
      注意:在虚幻中结构体可以有方法,但是不可以被UFUNCTION。并且作为结构体的成员方法,它们内部可以自由访问外部的数据

UFuntion

声明方式

UFUNCTION([specifier1=setting1,specifier2,...],[meta(key="value1",key2,...)])
ReturnType FunctionName([Parameter1,Parameter2,...,ParameterN1=DefaultValueN1,ParameterN2=DefaultValueN2])[const[])

操作:这次我门的函数就别放在BaseType里了,创建一个XGFunctionActor,再在里面创建按MyFuncActor类吧。

  1. Pure与Callable
    ../attachment/Pasted image 20250727223520.png,创建cpp文件中的实现后,就可以在ue中创建子蓝图,并在事件图表中得到如下../attachment/Pasted image 20250727224718.png
    这就时Pure和Callable两种不通的标记的结果。
  2. 在作为接口时Pure与Callable:.h文件
    ../attachment/Pasted image 20250727235158.png
    .cpp文件../attachment/Pasted image 20250727235241.png
    在UE中创建一个新的关卡,放入这个MyFuncActor类的子蓝图,并编辑关卡蓝图,并选中此蓝图的实例,在关卡蓝图创建其引用。../attachment/Pasted image 20250727235950.png
    此时运行扣一绿色和紫色的输出都是10。
    添加另外两条逻辑。
    ../attachment/Pasted image 20250728000441.png
    运行并测试:按2输出两个9,再按2,输出两个8,再按,两个7,两个同步减小。
    重新运行并测试:按3,输出9,8;再3,输出7,6;再3,输出5,4。
    理解:经过这般尝试,我觉得purefunc和callfunc的不同取决于计算单元,callfunc本身就是操作单元,只有调用它他才会进行一次计算,并保持调用过后的状态;
    而purefunc本身不是操作单元,它只有在有一个操作单元调用时,将操作单元前面的所有purefunc逻辑从上一个操作单元开始重新走一便。

函数说明符

  1. BlueprintAuthorityOnly:若在具有网络权限的机器上运行,此函数将仅从蓝图代码执行。
  2. BlueprintCallable:
  3. BlueprintCosmetic:修饰性的,无法在专用服务器上运行。
  4. BlueprintTemplateableEvent
    例:添加如下代码,仅在.h文件,千万别再.cpp添加实现,BlueprintImplementableEvent标记,UE会自动实现代码,所以不可以手动实现。本质上时一个可以在蓝图重载的CPP类。
    ../attachment/Pasted image 20250728013500.png
    使用:打开此类的子蓝图类(不是关卡蓝图哈。)../attachment/Pasted image 20250728014119.png../attachment/Pasted image 20250728014139.png
    ../attachment/Pasted image 20250728014234.png
    相当于重写了一个函数。
  5. BlueprintNativeEvent:用它修饰的函数,需要在.cpp文件中的实现后面添加_Implementation
    如:可以在蓝图重载,或者使用_Implementation后缀的实现来重载。
//.h
public UFUNCTION(BlueprintCallable,BlueprintNativeEvent)
int32 BPAndCPPMustOverride(float InMyFloat);
//.cpp
int32 AXGFunctionActor::BPAndCPPMustOverride_Implementation(float InMyFloat)
{
	return int32();
}
  1. 1
    例:../attachment/Pasted image 20250728020212.png并创建默认实现。
    ../attachment/Pasted image 20250728020448.png这里就有了一个方法可以点击调用(常用来写Actor的日志。)

函数元标签

  1. AdvancedDisplay= "Parameter1,Parameter2,..":用逗号分隔的参数列表将显示为高级引脚,点箭头展开的那种。
    例:创建并实现如下
    ../attachment/Pasted image 20250728083123.png打开MyFuncActor子蓝图的事件图标。../attachment/Pasted image 20250728083400.png
    可见,除了返回值还多了一个出参。

接下来:../attachment/Pasted image 20250728084841.png
../attachment/Pasted image 20250728084903.png
../attachment/Pasted image 20250728084617.png
可以看见,每次begin运行上面的执行输出1,2,3,322没问题。但是begin连下面就是1,2,3。(原因引用传入和值传入。)

UInterface(不全:44)

接口时用于实现一批组件的,用于实现组合优于继承的效果。

接口声明

UINTERFACE([specifier,specifier,...],[meta(key=value,key=value,...)])
class UClassName:public UInterface
{
	GENERATED_BODY()
};

注:UINTERFACE类并不是一个实际的接口,他是一个空白的类,它的存在只是为了向虚幻引擎反射系统确保可见性。将由其他类继承的实际接口必须具有相同的类名,但开头字母必须是I。

如:

#pragma once
#include "ReactToTriggerInterface.generated.h"
UINTERFACE(MinimalAPI,Blueprintable)
class UReactToTriggerInterface : public UInterface
{
	GENERATED_BODY()
};
class IReactToTriggerInterface
{
	GENERATED_BODY()
public:
};

接口说明符

  1. BlueprintType:公开为蓝图中的变量的类型
  2. DependsOn = (ClassName1,ClassName2,...):依赖关系,在编译此类前先编译啥
  3. MinimalAPI:

创建一个Actor类并放入到UInterface文件夹。

TArray

TArray是UE中常用的容器类,速度快,内存消耗小,安全性好。

  1. 创建
TArray<int32> IntArray;
  1. 填充
IntArray.Init(10,5);//IniArray ==[10,10,10,10,10]
  1. 增添
TArray<FString> StrArr;
StrArr.Add(TEXT("Hello"));
StrArr.Emplace(TEXT("World"));
  1. Append添加
    像一个TArray添加另一个数组。
 FString Arr[] = {TEXT("of"),TEXT("Tomorrow")};
 TArray<FString> StrArr;
 StrArr.Append(Arr,UE_ARRAY_COUNT(Arr));
  1. AddUnique
    不想TArray添加存在了的数据
 StrArr.AddUnique(TEXT("!"));
 StrArr.AddUnique(TEXT("!"));
  1. 指定位置插入Isert
StrArr.Insert(TEXT("Brave"),1);
  1. SetNum设置容器尺寸
StrArr.SetNum(8);

如果容量大于内容物数,则填充Empty。若小于,则直接将多出来的删掉。
8. 迭代数组
1. range-for功能遍历
cpp FString JoinedStr1; TArray<FString> StrArr = {"Hello","World","of","Tomorrow","!"}; for (auto& Str :StrArr) { JoinedStr1 +=Str; JoinedStr2 += TEXT(" "); }
auto可以作为自动类型匹配,效果上,是提供了一个方式用for循环去访问结构体,访问map。
2. 基于纯索引
cpp for (int32 Index = 0;Index!=StrArr.Num();++Index) { JoinedStr += StrArr[Index]; JoinedStr += TEXT(" "); }
3. 基于迭代器

FString JoinedStr;
TArray<FString> StrArr = {"HEllo","World","of","Tomorrow","!"};
for (auto It = StrArr.CreateConstIterator();It;++It)
{
	JoinedStr +=*It;
	JoinedStr +=TEXT(" ");
}
  1. 排序
    1. 随机排序:忽视大小写,然后进行排序。
      TArray<FString> StrArr = {"AA","ab","DDDDA","BB","VE","ABC","CCCCC","AB","AD","aa"};
      StrArr.Sort();
      
    2. 堆排序:性质与效果根随机排序没啥区别。最多速度快些吧
      TArray<FString> StrArr = {"AA","ab","DDDDA","BB","VE","ABC","CCCCC","AB","AD","aa"};
      StrArr.HeapSort();
      
    3. 稳定排序:先基于大小排序,之后根据等值元素的先后顺序排序。
      StrArr.StableSort();
      

Add和Emplace的最终效果完全相同,区别在于Add会找一块内存创建好一个临时的数据,然后将他克隆过来,再放到StrArr中,Emplace是直接放入。对于基本数据类型,Add没有啥性能损失,对于复杂的数据类型,Add的性能损耗大了去了。

操作:创建一个XGArrayActor到Array文件夹。
../attachment/Pasted image 20250729222551.png
编辑声明../attachment/Pasted image 20250729233024.png
实现定义
../attachment/Pasted image 20250729233102.png
回到vs中打上断点,并选择DebugGame Editor模式,选择本地调试器启动ue!
../attachment/Pasted image 20250729234730.png

../attachment/Pasted image 20250729235236.png
在ue内容浏览器创建一个Array文件夹,在其中创建测试用的蓝图类BP_ArrayActor,和测试用的地图ArrayMap。并编写蓝图类。
../attachment/Pasted image 20250729234528.png
此时在UE中启动项目,就可以在vs中看到状态了。

匿名函数

cpp中的lambda,闭包,匿名函数,回调函数。

int32 OriginNum = 100;
auto Lambda = [OriginNum](int32 InNum,int32 InAddNum)->int32
{
	int32 Max = InNum+InAddNum;
	Max +=OriginNum;
	return Max;
};

int32 Calculate = Lambda(10,2);

[]会捕获OriginNum变量,并在操作的时候使用或修改它。

二元谓词

在数组中用匿名函数,通过闭包传递一个排序准则,但是Sort不保证没有随机性,HeapSort同样,只有StableSort保证没有随机性。

TArray<FString> StrArr = {"CCCC","BB","aa","ab","ABC","VE","AA","AD","AB","DDDDDDA","AA"};
StrArr.Sort([](const FString& A,const FString& B){
	return A.len()<B.len();
});

注意:我的理解是通过闭包传入一个而排序准则,然后用一个滑动窗口去遍历,根据返回值是真是假判断是否交换顺序,也就是说底层实现是基于快速排序的。

数组查询

//获取可查询元素个数
int32 Count = StrArr.Num();

//获取数组的指针
FString* StrPtr = StrArr.GetData();

//获取容器的大小
uint32 ElementSize = StrArr.GetTypeSize();

//获取元素
uint32 Elem1 = StrArr[1];

//获取元素的大小  
uint32 ElementSize1 = sizeof(FString);

数组索引

//判断索引是否存在
bool istrue_str1 = StrArr.isValidIndex(3);

//切换至大写
bool bGoodbye = StrArr[3].ToUpper();

//判断指定元素是否存在
TArray<FString> StrArr = {"Hello","World","of","Tomorrow","!"};
bool bHello = StrArr.Containes(TEXT("Hello"));

//获取索引,找到给元素,没找到给-1 
int32 Index = StrArr.Find(TEXT("Hello"));
int32 IndexLast = StrArr.FindLast(TEXT("Hello")); 
//FindLast和FInd获取两个参数,是查看第一个数据的索引放到第二个参数并把返回值给到是否存在此数据。若没找到LastIndex变为-1.
int32 LastIndex = -1;
bool bIndexFromLast = StrArr.FindLast(TEXT("Hello"),LastIndex);

//相比Find,Indexofbykey可以在返回查找的数据类型和声明符类型不兼容时强制转化元素类型
int32 Index = StrArr.IndexOfByKey(TEXT("Hello"));
int32 IndexP = StrArr.IndexOfByPredicate([](const FString& Str){
	return Str.Contains(TEXT("r"));

//不获取索引,而是获取一个指向找到的元素的指针,其他同IndexofByKey
auto* OfPtr = StrArr.FindByKey(TEXT("of"));

auto* LenSPtr = StrArr.FindByPredicate([](const FString& Str){
	return Str.Len() ==5;
})
});

移除元素

TArray<int32> ValArr;
int32 Temp[] = {10,20,30,5,10,15,20,25,30};
ValArr.Append(Temp,ARRAY_COUNT(Temp));
ValArr.Append(Temp,ValArr.Num());

//删除所有指定元素的等值元素
ValArr.Remove(20);

//删除首个指定元素的等值元素
ValArr.RemoveSingle(30);

//删除指定索引的元素
ValArr.RemoveAt(3);

//缩小容器的空间:容器删除元素后会空出空间。
ValArr.Shrink();

// 用谓词来做判断进行删除。
VallArr.RemoveAll([](int32 Val){
	return Val%3 ==0;
});

因为数组中不可能存在中间有空位的情况,因此移除元素必定导致数组中后面区域向前复制。RemoveSwap、RemoveAtSwap和RemoveAllSwap函数会减小此开销,但是不保证剩余元素的排序。

TArray<int32> ValArr2;
for(int32 i =0;i!=10;++i){
	VallArr2.Add(i%5);
}

ValArr2.RemoveSwap(2);

ValArr.RemoveAtSwap(1);

ValArr2.RemoveAllSwap([](int32 Val){
	return Val%3 ==0;
});

ValArr2.Empty()//清空所有元素。

运算符

TArray<int32> ValArr3;
ValArr3.add(1);
ValArr3.add(2);
ValArr3.add(3);

//直接用等于号复制是进行了数据的复制。
auto ValArr4 = ValArr3;
valArr4[0]=5;
//此时ValArr[3] =[1,2,3],ValArr[4] = [5,1,2];


//数组合并串联,+=和Append同效果。  
TArray<nt32> ValArr6;
ValArr6+=ValArr4;
ValArr6.Append(ValArr3);

//MoveTemmp可以将一个数组的数据覆盖到另一个数组。
TArray<int32> ValArr7 = {1,1,1};
ValArr7 = MoveTemp(ValArr6); 

//使用运算符 == 和!=判断两个数组是否全等,是忽视大小写的。
auto ValArr5 = ValArr3;
bool equals_arr = ValArr5 == ValArr3;

Slack

//Slack,标识容器容量和占用空间的差值,用GetSlack获取。
TArray<int32> SlackArray;
SlackArray.Add(1);
int32 SlackNum = SlackArray.GetSlack();
SlackArray.Num();
SlackArray.Max();

//清空整个数组并将容器尺寸设置为7,本质上会释放内存,重新申请内存
SlackArray.Empty(7);

//Reset类似与Empty,但是它只清空容器,默认不改变其容量,除非手动设置参数;本质上清空元素不释放内存。
SlackArray.Reset(0);//此时Slack为7,Num为0.     0比7小不,扩容
SlackArray.Reset(10);//此时Slack为10,Num为0.   10比7大,扩容


//Shrink会清0容器的Slack,换句话数,让容器的容量等同于容器的内容数。
SlackArray.Shrink();

原始内存

int32 SrcInts[] = {2,3,5,7};
TArray<int32> UninitInts;//UninitInt得到的就是一个未初始化的数组,内部是其他程序运行产生的数据。
UninitInts.AddUninitialized(4);//想UninitInts数组添加未初始化的空间4个。
FMemory::Memcpy(UninitInts.GetData(),SrcInts,4*sizeof(int32));//复制覆盖数据。

Zeroted和UninitInt族的函数一定谨慎用,很危险。

其他

  1. BulkSerialize,序列化函数,可用作替代运算符<<,将数组作为原始字节快进行序列化,而非执行逐元素序列化。如使用内置类型或纯数据结构体等浅显元素,可改善性能。
  2. CountByte和GetAllocatedSize用于估算当前内存占用量。CountBytes接受FArchive,可直接调用GetAllocatedSize。常用于统计报告
  3. Swap和SwapMemory函数均接受两个指数并交换此类指数上的元素值。这两个函数相同,不通点是Swap会对指数执行额外的错误检查,并断言索引是否越界。
  4. Reserve用于扩容数组,指定一个容量,满足就不变化,不满足就扩容

TMAP

基础

  1. 元素的存世类型实际上是TPair<KeyType,ElementType>,TMap是同质容器,这意味着存储的元素必须同类型。
  2. TMap是普通键值映射,存储新的元素会覆盖旧的,且是无序的。TMultiMap可以保留多版本的同键数据。
  3. TMap是散列容器,键类型必须支持GetTypeHash函数,并提供运算符==用于比较是否各个键等值
 TMap<int32,FString> FruitMap;
 FruitMap.Add(5,TEXT("Banana");
 FruitMap.Add(2,TEXT("Grapefruit");
 FruitMap.Add(7,TEXT("Pineapple");
 
//添加无值的键
FruitMap.Add(4);

//Emplace方法,不创建临时变量,直接添加
FruitMap.Emplace(3,"Orange");

//Append可以合并多个映射
TMap<int32,FString> FruitMap2;
FruitMap2.Emplace(4,TEXT("Kiwi");
FruitMap2.Emplace(9,TEXT("Melon");
FruitMap2.Emplace(5,TEXT("Mango");
FruitMap.Append(FruitMap2);

%% 使用UE反射宏   %%
UPROPERTY(BlueprintReadWrite,EditAnywhere,Category = MapsAndSets)
TMap<int32,FString> MyFruitMap;

迭代TMap

//通过循环。
for (auto& Elem:FruitMap)
{
	FPlantformMisc::LocalPrint(*FString::Printf(TEXT("AUTO-(%d,\%s\")\n"),Elem.Key,*Elem.Value));
}

for(const TPair<int32,FString>& Elem:FruitMap)
{
	FString Message = FString::Printf(TEXT("TPair- (%d,\%s\")\n"),Elem.Key,*Elem.Value);
	FPlatformMisc::LocalPrint(Message);
}


//迭代器
for(auto It = FruitMap.CreateConstIterator();It;++It)
{
	FPlantformMisc::LocalPrint(*FString::Printf(TEXT("Iterator-(%d,\%s\")\n"),It.Key(),*It.Value()));
}



//调用Num函数即可查询映射中保存的元素数量
int32 Count = FruitMap.Num();

//判断映射是否包含特定值。
bool bHas7 = FruitMap.Contains(7);

//取值,前者是原始数据的复制,后者是引用。 
FString Val7 = FruitMap[7];
FString& Val7Ref = FruitMap[7];

//Find 查询中指定的key,找到了给出指针,找不到返回null
FString* Ptr7 =FruitMap.Find(7);

//FindOrAdd,功能类Find,但是若找不到key就会创建这个key,但是value为"“。有一定现成安全性。
FStrin* Ptr8 = FruitMap.FinOrAdd(8);

//FindRef的返回是右值,临时值,引用是引用左值的。它和Find一样不会创建新值到map。若没有就创建一个临时的值返回,是值的副本。
FString Val6 = FruitMap.FindRef(6);
Val6 +=TEXT("998");  


//FindKey,查值的键的指针。线性查找,TMap太大会增加查找开销。
const int32* KeyMangoPtr = FruitMap.FindKey(TEXT("Mango"));

//提取key,value成为数组
TArray<int32> FruitKeys;
TArray<FString> FruitValues;
FruitMap.GenerateKeyArray(FruitKeys);
FruitMap.GenerateValueArray(FruitValues);

//移除,使用key移除对应键值对。返回值Key5toTemove是移除了的元素的个数。
FruitMap.Remove(8);
int32 Key5toTemove = FruitMap.Remove(5);

//FindAndRemoveChecked()的移除会将对应的键的值返回。但是check的性质等同于assert,一般开发不用,内部会断言某个键值对是否存在。
FString Removed7 = FruitMap.FindAndRemoveChecked(7);

//移除2这个key的元素,并将这个元素拷到后面的引用中去,若没有移除元素则不动后面的引用。
FString Removed=TEXT("YangHao");
bool bFound2 = FruitMap.RemoveAndCopyValue(2,Removed);

//清空TPair的元素,并将容器max设置为指定值
FruitMap.Empty(0);
FruitMap.Empty(10):

//仅仅清空元素,并改变max,无参数
FruitMap.Reset();

//ReverseMap(),判断容器的max是否比指定的max小,小则扩容,大则不动。
FruitMap.Reserve(3);

Map排序

//降序排序,排的是key
FruitMap.KeySort([](int32 A,int32 B){
	return A>B;
});

//顺序排序字符串长度,ValueSort
FruitMap.ValueSort([](const FString& A,const FString &B){
	return A.Len()< B.Len();
});

运算符

//UE的=被重载过,这里并非将指针给NewMap,二十复制一份这个Map给NewMap
TMap<int32,FString> NewMap = FruitMap;
NewMap[5] = "Apple";
NewMap.Remove(3);

//MoveTemp相当于剪切,将数据挪到了NewMap2后,FruitMap就空了。
TMap<int32,FString> NewMap2 = MoveTemp(FruitMap);

TMap Slack

%% 使用Reserve函数预先分配映射个数,最多可以为10个 %%
FruitMap.Reserve(10);
for(int32 i =0 ;i<10;++i){
FruitMap.Add(i,FString::Printf(TEXT("Fruit%d"),i));
}

%% 使用Collapse和Shrink函数可以移除TMap中的全部slack。Shrink将从容器的末端移除所有slack,但这回在中间或开始处留下空白元素。 %%
//TMap的移除和Array不同,Array移除是真的移除了元素并将后面的元素往前移动,TMap的移除本质上是将一个key打上标记表示移除了而已。
for(int32 i =0;i<10;i+=2)
{
	FruitMap.Remove(i);
}
//那如果真要移除元素就要使用Collapse和Shrink释放slack,但是却只从后往前移除并释放标记的元素而已,不会对开头的和中间的标记处理。
FruitMap.Shrink();

//compact将标记的空白空间进行组合到一起放在末尾,长于Shrink一起去清空TMap
FruitMap.Compact();

KeyFuncs(不全72)

只需要一个类型实现了==运算符的重载和非成员GetTypeHash的重载,就可以作为TMap的键,如果你不想重载这些函数,就需要为Key类型创建KeyFuncs,必须定义两个typedef和3个静态函数。

struct FMyStruct
{
	FString UniqueID;
	float SomeFloat;

	%% 显式的结构体构造函数 %%
	explicit FMyStruct(float InFloat):UniqueID(FGuid::NewGuid().ToString()),SomeFloat(InFloat)
	{
	}
};

template<typename ValueType>
struct TMayStructMapKeyFuncs:BaseKeyFuncs<TPair<FMyStruct,ValueType>,FString>
{
private:
	typedef BaseKeyFuncs<TPair<FMyStruct,ValueType>,FString>Super;
public:
	typedef typename Super::ElementInitType ElementInitType;
	typedef typename Super::KeyInitType KeyInitType;
	static KeyInitType GetSetKey(ElementInitType Element)
	{
		return Element.Key.UniqueID;
	}
	static bool Matchs (ElementInitType A,ElementInitType B)
	{
		return A.Compare(B,ESearchCase::CaseSensitive)==0;
	}
	static uint32 GetKeyHash(KeyInitType Key)
	{
		return FCrc::StrCrc32(*Key) ;
	}
}

FMyStruct有唯一标识和其他数据,GetTypeHash和运算符==要通用目的,GetTypeHash又只关注UniqueID的字段不能用于判断个体是否相同。所以需要定义KeyFuncs:

  1. 继承BaseKeyFuncs,它可以帮助定义某些类型,KeyInitType和ElementInitType.

其他

CountBytes和GetAllocatedSize

  1. GetAllocatedSize大概是返回uint32数据吧,是TMap的占用空间,max表示元素个数但无法表示内存占用。
  2. CountBytes

日志基础

  1. FPlantformMisc::LocalPrint(*FString::Printf(TEXT("Iterator-(%d,\%s\")\n"),It.Key(),*It.Value()));输出基本日志
  2. UE_LOG(LogTemp,Warning,TEXT("Auto-(%d,\"%s\"),Elem.Key,*Elem.Value);

TSet

创建

TSet类似于TMap和TMultiMap,后者是唯一标识键,用KVPair来存储数据,而TSet的值本身就是键,所以同样可以唯一标识。当然可以用一些参数让TSet支持重复。

  1. 他是快速容器类(排序不重要)
  2. 他是同质容器,所有元素必须同类型
  3. 他是值类型,支持常规复制,赋值,析构函数,
//创建
TSet<FString> FruitSet;
FruitSet.Add(TEXT("Banana"));
FruitSet.Add(TEXT("Grapefruit"));
FruitSet.Add(TEXT("Pineapple"));

//同TArray,Emplace可以替代Add,避免创建中间元素。
FruitSet.Emplace(TEXT("Orange"));

//Append函数进行合并来插入另一个集合的元素
Test<FString> FruitSet2;
FruitSet2.Emplae(TEXT("Kiwi"));
FruitSet2.Emplace(TEXT("Melon"));
FruitSet2.Emplace(TEXT("Mango"));
FruitSet.Apped(FruitSet2);

迭代

for(auto& Elem:FruitSet)
{
	Elem+=TEXT("1");
	FPlatformMisc::LocalPrint(*FString::Printf(TEXT(" \"%s\"\n"),*Elem));
}


//迭代器
for(auto It = FruitSet.CreateIterator();It,++It)
{
	(*It) +=TEXT("3");
	FPlatformMisc::LocalPrint(*FString::Printf(TEXT("(%s)\n"),*(*It)));
}

查询

//查询个数
int32 Count = FruitSet.Num();

//判断是否包含指定元素
bool bHasBanana = FruitSet.Contains(TEXT("Banana"));
bool bHasLemon = FruitSet.Contains(TEXT("Lemon"));

//通过索引查元素
//FSetElementId是一种特殊的结构体,表示索引。对TSet数据用Index获取。可配合[]符号来获取元素。
FSetElementId BananaIndex = FruitSet.Index(TEXT("Banana"));
FPlatformMisc::LocalPrint(*FString::Printf(TEXT(" \"%s\"\n"),*FruitSet[BananaIndex]));

//Find:若确实存在元素就返回指针,若不存在则返回null
FString* PtrBanana = FruitSet.Find(TEXT("Banana"));

%% 复制成Array,会覆盖原本的数组 %%
TArray<FString> FruitArray = FruitSet.Array();

移除

//删除指定索引的元素,时加上会留下空位的。
FruitSet.Remove(0);
//Remove会返回被删除者的索引,如果没找到则返回0
int32 RemovedAmoutPineapple = FruitSet.Remove(TEXT("Pineapple"));
int32 RemovedAmountLemon = FruitSet.Remove(TEXT("Lemon"));

//Empty和Reset
TSet<FString> FruitSetCopy = FruitSet;
FruitSetCopy.Empty(4);//释放一块内存并重新申请一下。
FruitSetCopy.Reset();//清空整个Set,默认不削减Max大小。

排序

FruitSet.Sort([](const FString& A,const FString& B){
	return A >B;
});

FruitSet.Sort([](const FString& A,const FString& B){
	return A.Len() <B.Len();
});

Slack

同map,slack就是已分配未使用的内存

%% 创建按slack %%
FruitSet.Reset();//没有释放内存
FruitSet.Reserve(10);
FruitSet.Shrink();//清空末尾空内存,

FruitSet.CompactStable();
FruitSet.Shrink();
posted @ 2025-08-09 19:28  抓泥鳅的小老虎  阅读(29)  评论(0)    收藏  举报