【UE4学习】20200824 UC++ 基础

前言

参照官方教程,记录一些值得注意的点。

Actor

可以被放到关卡中的对象。

联网时可以被复制的基本类型

生成Actor的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
AActor* UWorld::SpawnActor
(
UClass* Class,
FName InName,
FVector const* Location,
FRotator const* Rotation,
AActor* Template,
bool bNoCollisionFail,
bool bRemoteOwned,
AActor* Owner,
APawn* Instigator,
bool bNoFail,
ULevel* OverrideLevel,
bool bDeferConstruction
)

其中,UClass大概是

1
UClass class = XXXClass::StaticClass();

更简便的方法是通过模板类实现的。下面这段代码的第一个参数是Owner,第二个是Instigator

1
MyHUD = SpawnActor<AHUD>(this, Instigator);

销毁方法

Actor继承自UObject,使用Destroy销毁即可。

反射系统

  • GENERATE_BODY()实际上是插入一些必要的模板
    • [ ] 具体插入了啥,还需要有时间的时候再研究下
  • [ ] 反射头文件,也不知道插入了啥,查看一些博客研究下

常用说明符

UCLASS

  • Blueprintable 该类可以由蓝图扩展
  • NotBlueprintable 与上一条相反
  • Abstract 防止该类被实例化

构造函数生成CDO

构造函数中初始化的变量值,会形成类默认对象(CDO)。

UFUNCTION

说几个值得一提的函数说明符和元数据。

蓝图相关

  • BlueprintImplementablEvent 此函数可以在蓝图中实现
  • BlueprintNativeEvent 有原生的默认实现,但是可以在蓝图中覆盖实现

元数据:

  • DisplayName="Blueprint Node Name" 在蓝图中显示的名字
  • HidePin="Parameter" 在蓝图中隐藏引脚,只能隐藏一个
  • HideSelfPin 在蓝图中隐藏self引脚
  • ShortToolTip="Short tooltip" 提示
  • ToolTip="Hand-written tooltip" 提示

参数说明符

  • Out 传入引用给函数,函数可以对其进行更改

Tips

C++中定义了一个函数,传入了一个引用类型。如果多加点什么的话,到了蓝图层会发现参数变成了输出。为了避免这种情况,需要加UPARAM(ref),比如

1
2
3
>  UFUNCTION(BlueprintCallable)
> void ModifyVector(UPARAM(ref) FVector& Vector);
>

用以实现工具

  • ‘CallInEditor’ 可以在details中调用,一般拿来做工具啥的,挺有意思的
  • Exec 此函数可从游戏内控制台执行。仅在特定类中声明时,Exec命令才有效。

网络

  • ServiceRequest 此函数为RPC(远程过程调用)服务请求。这意味着 NetMulticast 和 Reliable。
  • ServiceResponse 此函数为RPC服务响应。这意味着 NetMulticast 和 Reliable。
  • Server 仅在服务器上运行
  • Unreliable 此函数将通过网络复制,但是可能会因带宽限制或网络错误而失败

调试

  • DevelopmentOnly 只有在开发时会被执行

Property

位掩码

参考Property

可以在Details面板中显示多选项。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
UENUM(Meta = (Bitflags))
enum class EColorBits
{
ECB_Red,
ECB_Green,
ECB_Blue
};

UCLASS()
class MyActor : public Actor
{
GENERATE_BODY()

UPROPERTY(EditAnywhere, Meta=(Bitmask, BitmaskEnum="EColorBits"));
int32 ColorFlags;
}

Event相关

对于DYNAMIC_MULTICAST_DELEGATE类型的Event,可以定义BlueprintCallable,从蓝图发送事件。

对于delegate来说,如果定义为BlueprintAssignable,可以在蓝图绑定事件。

USTRUCT

参考结构体

属于反射系统,但是不属于UObject生态圈,无法垃圾回收系统处理。但是,快。

支持UPROPERTY,但是不支持UFUNCTION(虽然原生c++ struct支持定义函数)。

蓝图split的支持

如果想要让结构体在蓝图中可以执行split,那么必须定义为Blueprintable

蓝图中break和make

  • make:HasNativeBreak="Module.Class.Function"
  • break: HasNativeMake="Module.Class.Function"

UINTERFACE

UE4里面的接口比较古怪。要定义一个接口,总共要定义两个类。其中,第一个类只是给反射系统用,是个空类,必须以U开头并继承于UInterface。第二个类才是真正的接口所在,名字以I开头,其余部分与前者相同,且不需要继承于任何类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#pragma once

#include "ReactToTriggerInterface.generated.h"

/* 这个只是给反射系统看的,是个空类 */
UINTERFACE(MinimalAPI, Blueprintable)
class UReactToTriggerInterface : public UInterface
{
GENERATED_BODY()
};

/* 这个才是真正的接口内容,注意名字要和前面那个U开头的相同,C++类要实现的是这个类 */
class IReactToTriggerInterface
{
GENERATED_BODY()

public:
/** 在此处添加接口函数声明 */
UFUNCTION(BlueprintCallable, BlueprintImplementableEvent)
void Test();
};

如果接口既想要被蓝图实现,又想在C++先有个默认实现,那么需要做几件事:

  1. 接口中的函数需要被定义为BlueprintCallable, BlueprintNativeEvent
  2. C++实现接口,要实现接口的方法,但是不是直接实现,而是要在方法名后面加上_Implementation

猜测当UFUCTION定义了BlueprintNativeEvent之后,UE4发现某某类实现了该接口,那么GENERATED_BODY()里会做两件事:

  1. 接口类中会因为GENERATED_BODY()出现一个xxx_Implementation()的函数
  2. 实现类再去override这个函数,刚好

举个完整的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
UINTERFACE(MinimalAPI, Blueprintable)
class UTestable : public UInterface
{
GENERATED_BODY()
}

class ITestable
{
GENERATED_BODY()

UFUNCTION(BlueprintCallable, BlueprintNativeEvent)
void Test();
}

UCLASS()
class MyTest() : public Actor, public ITestable
{
/** 此处用override关键字不会打包报错 */
void Test_Implementation() override;
}

void MyTest::Test_Implementation()
{
UE_LOG(LogTemp, Warning, TEXT("Print From Test_Implementation()"));
}

判断类是否实现了接口

1
2
3
4
5
bool bIsImplemented = OriginalObject->GetClass()->ImplementsInterface(UReactToTriggerInterface::StaticClass()); // bIsImplemented will be true if OriginalObject implements UReactToTriggerInterface.

bIsImplemented = OriginalObject->Implements<UReactToTriggerInterface>(); // bIsImplemented will be true if OriginalObject implements UReactToTriggerInterfacce.

IReactToTriggerInterface* ReactingObjectA = Cast<IReactToTriggerInterface>(OriginalObject); // ReactingObject will be non-null if OriginalObject implements UReactToTriggerInterface.

UObject/Actor迭代器

构造完了就可以用。

1
TObjectIterator<UObject> it()

拿到value的方法:

1
UObject *value = *it;

垃圾回收

狭义来说,只针对UObject。

广义上来说,还可以通过:

  • 定义类继承自FGCObject并重写必要的方法AddReferencedObjects()
  • 智能指针

USTRUCT是不会被自动回收的,如果真的希望USTRUCT被内存管理,那么使用智能指针便是。

回收时机

Destroy只是做了个标志位,真正回收的时间还要等系统决定。

标记了Destroy和真正被回收之间的这个状态,称为一条腿踏进了鬼门关,简称等死,可以用IsPendingKill()函数来判定。

命名规范

不照着命名规范走,有可能就直接编译错误了。

  • 派生自 Actor 的类带有 A 前缀,如 AController。
  • 派生自 Object 的类带有 U 前缀,如 UComponent。
  • Enums 的前缀是 E,如 EFortificationType。
  • Interface 的前缀通常是 I,如 IAbilitySystemInterface。
  • Template 的前缀是 T,如 TArray。
  • 派生自 SWidget 的类(Slate UI)带有前缀 S,如 SButton。
  • 其他类的前缀为字母F ,如 FVector。

数字类型

  • int8/uint8
  • int16/uint16
  • int32/uint32
  • int64/uint64
  • float (32 bit)
  • double (64 bit)

字符串处理

FString

是一个可变字符串。

创建使用TEXT

多说一句

TEXT()返回的其实是WCHAR数组,只不过被隐式转换了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
> FORCEINLINE FString(const CharType* Src)
> {
> if (Src && *Src)
> {
> int32 SrcLen = TCString<CharType>::Strlen(Src) + 1;
> int32 DestLen = FPlatformString::ConvertedLength<TCHAR>(Src, SrcLen);
> Data.AddUninitialized(DestLen);
>
> FPlatformString::Convert(Data.GetData(), DestLen, Src, SrcLen);
> }
> }
> '''

转换成数字:

```cpp
int32 TestInt = FCString::Atoi(*MyFString);
float TestFloat = FCString::Atof(*MyFString);

数字转换成string:

1
2
FString TestString = FString::FromInt(MyInt);
FString TestString = FString::SanitizeFloat(MyFloat);

FText

跟FString差不多,主要是多了个本地化的功能。

创建:

1
FText MyText = NSLOCTEXT("Game UI", "Health Warning Message", "Low Health!")

FName

一个字「」。

其实也是字符串。

一般用作常出现的字符串,在内存中只出现一份拷贝。不区分大小写,也不可变,无法被操作。

可以提高性能。用作字符串匹配时(例如依名寻物时),可以直接用索引值来匹配,而不需要每个字符分别对比。

TCHAR

字符串的真实载体。

一般啥时候用到呢?比如FString::Printf(TEXT("%S"))中的%s,其实需要的就是一个TCHAR数组。

FString重写过其*操作符,用以返回其指向的WCHAR数组的地址。

1
2
3
4
5
// File: UnrealString.h
FORCEINLINE const TCHAR* operator*() const
{
return Data.Num() ? Data.GetData() : TEXT("");
}

新建

1
const TCHAR* message = TEXT("Hello world.");

TEXT的本质是给字符串前面加了个L,也就是说前面的处理后,会变成L"Hello world."

TCHAR的处理

UE4在FChar类中封装了一些静态函数,可以处理WCHAR数组。

容器

TArray

获取array长度:

1
int32 ArraySize = Array.Num();

直接取值(说明重写了[]):

1
2
int32 Index = 0;
AActor* FirstActor = ActorArray[Index];

添加新元素

1
ActorArray.Add(NewActor)

如果数组中不存在新元素,那么添加新元素

1
ActorArray.AddUnique(NewActor);

移除元素:

1
2
3
4
5
6
7
8
ActorArray.Remove(NewActor);

ActorArray.RemoveAt(0);

/* 高效,但是可能会打乱顺序 */
ActorArray.RemoveAtSwap(Index);
/* 直接清除所有 */
ActorArray.Empty();

TArray是支持垃圾回收的,前提是元素必须都继承于UObject

TMap

字典。

TSet

集合。

容器迭代器

创建迭代器

1
2
3
4
5
6
7
8
9
10
11
auto Iterator = xxxSet.CreateIterator();

for(; Iterator; ++Iterator)
{
auto Element = *Iterator;
if (Element.Invalid())
{
// 移除当前元素
Iterator.RemoveCurrent();
}
}

获取迭代器索引

1
int32 Index = Iterator.GetIndex();

复位到第一个元素

1
Iterator.Reset();

for-each

除了map以外都是普通的for-each,无须赘述。

map的话for-each返回的是一个键值对,用的时候可以这么用:

1
2
3
4
5
for (auto& KeyValuePair : XXXMap)
{
auto Key = KeyValuePair.Key;
auto Value = KeyValuePair.Value;
}

自定义散列函数

Set和Map的键是需要能够转换成哈希值的。

UE4提供的能够作为键的类型,都是实现了GetTypeHash()函数的,能够返回唯一的一个哈希值。

如果我们要实现自己的类,并且让它能够作为键,那么需要实现:

  • GetTypeHash()
  • operator==
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class FMyClass
{
uint32 ExampleProperty1;
uint32 ExampleProperty2;

// 散列函数
friend uint32 GetTypeHash(const FMyClass& MyClass)
{
// HashCombine是将两个散列值合并的效用函数。
uint32 HashCode = HashCombine(MyClass.ExampleProperty1, MyClass.ExampleProperty2);
return HashCode;
}

// 为了演示目的,两个相同的对象
// 应该始终返回相同的散列代码。
bool operator==(const FMyClass& LHS, const FMyClass& RHS)
{
return LHS.ExampleProperty1 == RHS.ExampleProperty1
&& LHS.ExampleProperty2 == RHS.ExampleProperty2;
}
};
Buy Me A Coffee / 捐一杯咖啡的钱
分享这篇文章~
0%
//