先是一些入门的小东西。
Tick()
Tick():Actor出现后每一帧都会call它,参数为上一次call它到现在的间隔时间,通常即为帧与帧之间的间隔时间,如果不需要该函数,请丢掉它,能节省一小部分性能,记住也要把Constructor里相关的东西删除指的就是
PrimaryActorTick.bCanEverTick = true;
UPROPERITY()
说明一下,_BlueprintReadOnly_相当于表示该属性为const,关于UPROPERTY宏更多的参数,参考Link。下面举个例。
UCLASS() class AMyActor : public AActor { GENERATED_BODY() public: UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Damage") int32 TotalDamage; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Damage") float DamageTimeInSeconds; UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Transient, Category="Damage") float DamagePerSecond; ... };
Transient
UPROPERTY宏的参数,表示短暂的,说明该属性加载时会被填充为0;
PostInitProperties()
当某个属性的初始化值需要由设计师在编辑内设置好其它属性的值,用这些值来产生该初始化值然后赋予,这就需要用到_Super::PostInitProperties()_函数,如下,便能在运行时也能改变那个值。
void AMyActor::PostInitProperties() { Super::PostInitProperties(); CalculateValues(); } void AMyActor::CalculateValues() { DamagePerSecond = TotalDamage / DamageTimeInSeconds; //DamagePerSecond,即为那个需要其他值来赋予值的属性 } #ifdef WITH_EDITOR void AMyActor::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) { CalculateValues(); Super::PostEditChangeProperty(PropertyChangedEvent); } #endif
_PostEditChangeProperty()_函数继承自Actor,当所属Actor的属性在编辑器被改变时会触发调用。
Super
是子类对父类的别称。
BlueprintImplementableEvent
UFUNCTION宏的一个参数,用来使函数被认定为是从蓝图中调用的,但蓝图中没有定义该函数,then do nothing。如果想有一个默认的函数体,使用_BlueprintNativeEvent_,并提供额外的默认函数,命名为[FuncionName]_Implementation,举例。
UFUNCION(BlueprintNativeEvent) void CalledFromCpp(); void CalledFromCpp_Implementation(); //再实现它 void [ClassName]::CalledFromCpp_Implementation() { //do something }
好的,讲解正式开始,下面介绍四大gameplay class。
UObject
它和UClass搭配,提供了UE最重要的一些services(如下),是引擎最基本的两个类。
每一个继承自UObject的类的实例,引擎都会自动创建一个包含所有元数据(metadata)的UClass供其使用。
AActor
继承自UObject,要么被直接放置再world当中,要么在运行中通过gameplay系统被加入world中。所有可以被放入level中的对象都继承自该类。它可以被显示消除,也可以通过垃圾回收系统自动消除,还可以通过Lifespan决定它存在多久,然后自动消除。
它的生命周期简单来说就三件事,BeginPlay(), Tick(), EndPlay(),直观一点就是被放入world,做事情,从level里消失。因为操纵一个Actor合理变化十分复杂,引擎提供了一个method,SpawnActor,是UWorld的一个成员。
UActorComponent
即Actor的组件,RootComponent是Actor的成员,根组件嘛,另外,组件和Actor共享Tick。
UStruct
注意,UStruct并不从UObject继承,没有垃圾回收等机制。它的内部应该全部为纯数据。
Unreal Reflection System
gameplay类用一些特殊的宏
来让我们轻易实现映射。下面介绍几种。
Macro | Description |
---|---|
UStruct() | 让引擎为这个struct产生映射数据 |
GENERATED_BODY() | 为该类产生模板式的constructor |
另外,所有产生的映射数据都会存到[ClassName].generated.h文件中,GENERATED_BODY()也在里面。
Object/Actor Iterators
Object Iterators可以将UObject所有的实例包括子类实例全都迭代一遍。如下
for (TObjectIterator<UObject> It; It; It++) { UObject* CurrentObject=*It; UE_LOG(LogTemp, Log, TEXT("Found UObject named: %s"), *CurrentObjec->GetName()); }
TObjectIterator<>里也可以指定UObject的子类,如那么将迭代该子类和该子类的子类的所有实例。
注意使用在PIE中使用Object Iterator会导致意外错误。编辑器加载完成后,迭代器会归还所有被放入world的对象实例和编辑器正在使用的实例。
Actor Iterator相当于TObjectIterator,Actor Iterator不会产生上述问题,且只归还被放入current level的对象实例。创建一个Actor Iterator,需要给它指向一个UWorld实例的指针。
APlayerController* MyPC = GetMyPlayerControllerFromSomewhere(); UWorld* World = MyPC->GetWorld(); // Like object iterators, you can provide a specific class to get only objects that are or derive from that class for (TActorIterator<AEnemy> It(World); It; ++It) { // ... }
说在前头,垃圾回收机制,清除的是不再被引用(被指针指向)或已被显式标记为即将被回收的内存。
你创建了一个类A,在类中定义一些成员指针变量,其类型是B,它会指向一块内存,该指针便是对这块内存的一个reference,垃圾回收便是把这指针所指向的内存回收,并把指针设置为nullptr。
至于你所建的类A,在其它类中,可能会有A类型的指针,它申请一块内存,至于这块内存是否在垃圾回收系统范畴,就看你建的这个类A,是否符合规定。
所以,你在你所建立的类里应该讨论的是成员变量,讨论类本身在类内是没有意义的。
UObject
UE使用映射系统来执行垃圾回收,需要垃圾回收机制的类需是UObject或其子类。
垃圾回收有一个概念叫做root set,即一个包含一些对象的列表,回收系统保证不会回收这些对象。把这个列表想象成一棵树,树所触及不到的实例对象,全都当垃圾回收了。垃圾回收会一轮一轮在固定时间进行。
UObject不会被当作垃圾回收的条件有三种:
举例
void CreateDoomedObject() { MyGCType* DoomedObject = NewObject<MyGCType>(); }
这里的DoomedOjbect指针就没有被UPROPERTY宏修饰(或在被UPROPERTY宏修饰的UE容器类里),即root set触及不到,会被垃圾回收消除。
Acotr
除了level关闭,Actor一般不会被垃圾回收,它们产生后,一般需要手动调用消除函数(只是从root set中移除,还需等待下轮垃圾回收),这之后它们会立马被排除在world外,然后垃圾回收系统就能检测到异端了,会在下一轮把它回收掉。举例
UCLASS() class AMyActor : public AActor { GENERATED_BODY() public: UPROPERTY() MyGCType* SafeObject; MyGCType* DoomedObject; AMyActor(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { SafeObject = NewObject<MyGCType>(); DoomedObject = NewObject<MyGCType>(); } }; void SpawnMyActor(UWorld* World, FVector Location, FRotator Rotation) { World->SpawnActor<AMyActor>(Location, Rotation); }
当我们调用SpawnMyActor函数时,MyActor会产生在world里。SafeObject前有UPROPERTY宏修,但DoomedObject并没有,它会被垃圾回收机制检测到并消除,留下一个dangling(空悬)指针。(解释一下,野指针是没有初始化的指针,根本不知道指的啥;空悬指针是指那种生命周期比所指对象还长的指针,在所指对象被回收后,它仍指向那块内存,若系统给那块内存分配了东西,会有意外发生)
但注意,当一块存储UObject的内存被回收之后,所有被UPROPERTY宏修饰并指向这块内存的指针都会被设置为nullptr,这样就消除了空悬指针,这也使得你在使用这些指针的时候,要先确认一下是否为nullptr,因为还有一点,手动调用函数消除实际上是把该指针所指对象从root set里移除,并等待下一轮的垃圾回收,用IsPendingKill检验是否在等待
if (MyActor->SafeObject != nullptr) { // Use SafeObject }
UStruct
没有垃圾回收机制,非要使用它的动态实例,则需要智能指针的登场。
Non-UObject References
普通的c++类(非继承自UObject)需继承自FGCObject类,并重载AddReferenceObject()就也能添加对其的reference且不会被垃圾回收系统强制回收。需要说明的是,垃圾回收系统是一种无差别攻击系统,不在名单里的统统消灭。举例
class FMyNormalClass : public FGCObject { public: UObject* SafeObject; FMyNormalClass(UObject* Object) : SafeObject(Object) { } void AddReferencedObjects(FReferenceCollector& Collector) override { Collector.AddReferencedObject(SafeObject); } };
我们用FReferenceCollector来手动添加对该UObject的hard reference,而在对象被删除且destructor正常运行时,它会自动消除所有reference。
更详细的剖析
说一下,垃圾回收系统有两套,分别处理UObject和非UObject,想要创建的类能够被加入垃圾回收系统,只要让继承自UObject类的变量套上UPROPERTY的宏就可以了,因为这样就是被root set里的对象引用了,而继承自非UObject类的变量——则需要干以下几件事。
让这个类一开始写的时候就继承FGCObject类。
如果成员变量中有UObject类,在复写的AddReferencedObjects()方法中,将引用的UObject变量加入到Collector中即可。
如果成员变量中有 非UObject类,则需要将其声明为UE自定义的智能指针。\
Class
特殊的命名规则,给予特殊的便利与保护。
Number
整数:
int8
/uint8
: 8-bit signed/unsigned integerint16
/uint16
: 16-bit signed/unsigned integerint32
/uint32
: 32-bit signed/unsigned integerint64
/uint64
: 64-bit signed/unsigned integer浮点数:
float (32-bit) and double (64-bit)
UE中还有一个Template,TNumericLimits
String
UE提供了很多,但这一篇里文档没讲。
FString
是一种mutable string,用TEXT(" ")创建,日志输出一般都是用它。
FText
与FString类似,但它是localized text,两种方式创建,一是用NSLOCTEXT宏,需要a namespace, key, and a value三个参数;二是LOCTEXT宏,只需namespace,value两个参数,举例
//第一种 FText MyText = NSLOCTEXT("Game UI", "Health Warning Message", "Low Health!") //第二种 // In GameUI.cpp #define LOCTEXT_NAMESPACE "Game UI" //... FText MyText = LOCTEXT("Health Warning Message", "Low Health!") //... #undef LOCTEXT_NAMESPACE // End of file
FName
它主要用来存储十分常用的字符串,如果有多个对象引用同一个字符串,FName能使用较小的空间存储索引来映射(map)到给定字符串,它更快也是因为引擎能够检查其索引值来确认其是否匹配,而无须检查每一个字符是否相同。
TCHAR
TCHAR类型是独立于所用字符集存储字符,考虑到的是字符集或许会因平台而异。实际上,UE的字符串使用 TCHAR 数组来存储 UTF-16 编码的数据。可以使用返回TCHAR的overloaded dereference operator来访问the raw data。
某些函数要用它,例如 FString::Printf()
FString Str1 = TEXT("World"); int32 Val1 = 123; FString Str2 = FString::Printf(TEXT("Hello, %s! You have %i points."), *Str1, Val1);
"%s" 字符串格式说明符要的是TCHAR,一般就给它_*FString_。
FChar类提供一系列static utility function处理TCHAR的单个字符,举例
TCHAR Upper('A'); TCHAR Lower = FChar::ToLower(Upper); // 'a'
接下来介绍一些Container。
TArray
类似于std::vector,但有更多功能,下面是一些普通的操作。
TArray<AActor*> ActorArray = GetActorArrayFromSomewhere(); // 看有多少elements int32 ArraySize = ActorArray.Num(); // 第一个元素的索引为0 int32 Index = 0; // 检索一个值。 AActor* FirstActor = ActorArray[Index]; // 在TArray末尾添加element AActor* NewActor = GetNewActor(); ActorArray.Add(NewActor); // 添加一个TArray里本不存在的element,若存在,则不添加 ActorArray.AddUnique(NewActor); // 将TArray里的所有NewActor移除 ActorArray.Remove(NewActor); // 移除索引处的值,并将后面的所有值往前挪一位,即不留空位 ActorArray.RemoveAt(Index); // 移除索引处的值,与上不同,会将TArray里最后一个值挪到空缺处 ActorArray.RemoveAtSwap(Index); // 清空 ActorArray.Empty();
另外,像之前说的一样,被UPROPERTY宏修饰的TArray的UObject成员拥有垃圾回收的权限。
UCLASS() class UMyClass : UObject { GENERATED_BODY(); // ... UPROPERTY() AActor* GarbageCollectedActor; UPROPERTY() TArray<AActor*> GarbageCollectedArray; TArray<AActor*> AnotherGarbageCollectedArray; // 是吧,这些也都是指针 };
TMap
类似于std::map,具体方法文档在该处给了个实例,这里截取一小部分,简单明了。
TMap<FIntPoint, FPiece> Data; Data.Contains(Position); FPiece Value = Data[Position]; Data.Add(Position, NewPiece); Data.Remove(OldPosition); Data.Empty();
TSet
类似于std::set,直接上例子,也是简单明了
TSet<AActor*> ActorSet = GetActorSetFromSomewhere(); int32 Size = ActorSet.Num(); AActor* NewActor = GetNewActor(); ActorSet.Add(NewActor); if (ActorSet.Contains(NewActor)) { // ... } ActorSet.Remove(NewActor); ActorSet.Empty(); // 创造一个包含TSet里所有elements的TArray TArray<AActor*> ActorArrayFromSet = ActorSet.Array();
Container Iterator
直接上例子。
void RemoveDeadEnemies(TSet<AEnemy*>& EnemySet) { for (auto EnemyIterator = EnemySet.CreateIterator(); EnemyIterator; ++EnemyIterator) { AEnemy* Enemy = *EnemyIterator; if (Enemy.Health == 0) { // RemoveCurrent()是TSet和TMap的方法 EnemyIterator.RemoveCurrent(); } } }
// 退回到前一个element --EnemyIterator; // 前进或后退offset个element EnemyIterator += Offset; EnemyIterator -= Offset; // 获得迭代器现在的索引 int32 Index = EnemyIterator.GetIndex(); // 让迭代器回到第一个element EnemyIterator.Reset();
For-Loop
下面是for循环适应于TArray,TSet,TMap的用法。
// TArray TArray<AActor*> ActorArray = GetArrayFromSomewhere(); for (AActor* OneActor : ActorArray) { // ... } // TSet - Same as TArray TSet<AActor*> ActorSet = GetSetFromSomewhere(); for (AActor* UniqueActor : ActorSet) { // ... } // TMap - Iterator returns a key-value pair TMap<FName, AActor*> NameToActorMap = GetMapFromSomewhere(); for (auto& KVP : NameToActorMap) { FName Name = KVP.Key; AActor* Actor = KVP.Value; // ... }
从上面的代码中可以看到auto不会自动识别指针和引用,需要手动添加 * 或 & 。
Using Your Own Types with TSet/TMap (Hash Functions)
TSet和TMap内部都需要哈希函数,大部分UE types都已经定义了专属的哈希函数,如果你自定义的类需要用在TSet或作为key用在TMap里,需要提供一个参数为你定义的这个类的指针或引用,返回值为uint32,这个返回值需是你的类独有代号,举例。
class FMyClass { uint32 ExampleProperty1; uint32 ExampleProperty2; // Hash Function作为friend friend uint32 GetTypeHash(const FMyClass& MyClass) { // HashCombine(),内部函数,结合两个哈希值 uint32 HashCode = HashCombine(MyClass.ExampleProperty1, MyClass.ExampleProperty2); return HashCode; } // 为了演示证明,使用两个相同类型的对象 // should always return the same hash code. bool operator==(const FMyClass& LHS, const FMyClass& RHS) { return LHS.ExampleProperty1 == RHS.ExampleProperty1 && LHS.ExampleProperty2 == RHS.ExampleProperty2; } };
如果用指针作为key,即TSet<ClassName*>
,那么上面相应位置应该这么用:
uint32 GetTypeHash(const ClassName* ValueName)。
首先回顾一下c++中关于assert的知识:
assert,意思是断言,需包含头文件assert.h
。assert其实是宏定义,而非函数,用在程序调试阶段检查错误,判断expression是否为假,为假时,会调用abort报警。
void assert(int expression); // 举例 assert(("a必须大于10", a > 10)); // 或者 assert(a > 10 && "a必须大于10"); // 输出结果样式如下 Assertion failed: expression, file [FileName], line [num].
assert只有在Debug中才有效,如果编译为Release则被忽略。
如果不想使用它,可以在#include
语句之前,插入#define NDEBUG
,就可以禁用assert了。
assert通常用来检查三种情况,指针是否为空、除数发是否为零、函数是否递归运行,当然代码要求的其他重要假设也可能会用到,但缺点是效率低。
某些情况下,assert 能在真正的崩溃 (crash)发生前,发现造成延迟崩溃的bug,像是删掉在之后的Tick中会用到的对象,帮助找到崩溃的源头,当然其最关键的feature,像之前说的一样,不会出现在shipping code中。
好,回到UE。
UE提供assert的三种等价体系, check
,verify
, ensure
,三个有细微差别,但主要作用相同,都声明于 AssertionMacros.h 头文件中。(注意这些都是体系,每个里面又很多可用的宏)
Check
check体系是三个当中最接近assert的,当在参数里发现为false的表达式时,立马停止运行,默认也不会在shipping版本中运行。下面是check体系的可用宏。
Macro | Parameters | Behavior |
---|---|---|
check / checkSlow | Expression | Expression为false时停止运行 |
checkf / checkfSlow | Expression, FormattedText, ... | Expression为false时停止运行,并在日志中输出FormattedText |
checkCode | Code | 在do-while循环中执行Code,while条件硬性规定为false,即只运行一次,主要用来准备其它Check所需要的信息 |
checkNoEntry | (none) | 一旦触及,停止运行,类似于check(false),但主要倾向于说明程序不能走向这里 |
checkNoReentry | (none) | 第二次触及这里,停止运行,就是只允许紧接其后的代码运行一次 |
checkNoRecursion | (none) | 第二次到这儿如果没有离开当前作用域,停止运行 |
unimplemented | (none) | 一旦触及,停止运行,类似于check(false),主要用于设计上希望被override且不会被调用的虚函数 |
这些宏当中,除了以Slow结尾的只在Debug中运行,其余的在Debug,Development中均可运行。
UE的Check体系中保留有一个USE_CHECKS_IN_SHIPPING
的宏定义,用以标记Check检查可在所有版本执行,其默认值为0,主要用于怀疑check中的代码在修改值,或者发现仅存于发布版本的bug。
// 这个函数的传入参数JumpTarget如果是nullptr,那么运行会停止 void AMyActor::CalculateJumpVelocity(AActor* JumpTarget, FVector& JumpVelocity) { check(JumpTarget != nullptr); // 计算速度需要JumpTarget,这里保证它不是nullptr }
// HasCycle()检查MyLinkedList中有没有闭环,因为检查闭环很费时间,我们只在Debug中检查 checkfSlow(!MyLinkedList.HasCycle(), TEXT("Found a cycle in the list!")); // (Walk through the list, running some code on each element.)
// IsEverythingOk()没有额外的作用,就是看有没有致命性的错误 // If this happens, terminate with a fatal error. // 因为这段没有其它作用且只是诊断检查,所以无需在shipping版本中运行 checkCode( if (!IsEverythingOK()) { UE_LOG(LogUObjectGlobals, Fatal, TEXT("Something is wrong with %s! Terminating."), *GetFullName()); } );
// 如果我们有一个新的Shape Type却没加入这段switch中,就会停止运行 switch (MyShape) { case EShapes::S_Circle: // (Handle circles.) break; case EShapes::S_Square: // (Handle squares.) break; default: // 不应该有没说明的Shape Type,所以此路不通 checkNoEntry(); break; }
Verify
和Check体系差不多,但它可以在Check被禁掉的版本中仍计算表达式的值,注意这并不会触发运行停止,所以当表达式需要在诊断检查之外独立运行时,才使用该宏。
举个例子:如果要停止运行并检查(即断言检查)一个函数,假设函数返回bool值并以此作为断言参数,此时check和verify的行为一致,而在shipping版本中,它们开始有差异,verify在发行版本中会忽略函数的返回值(即不进行断言检查),但仍然会执行函数,而check则不会执行。
就是说,如果需要断言检查的参数表达式始终执行,则使用verify体系。
Macro | Parameters | Behavior |
---|---|---|
verify / verifySlow | Expression | Expression为false时,停止运行 |
verifyf / verifyfSlow | Expressin, FormattedText, ... | Expression为false时,停止运行,并在日志中输出FormattedText |
同Check体系一样,这些宏当中,除了以Slow结尾的只在Debug中运行,其余的在Debug,Development中均可运行,而且如上面所说,在所有版本中,包括shipping版本,Verify体系都会计算表达式的值。
同Check体系一样,Verify体系留有一个USE_CHECKS_IN_SHIPPING
的宏定义,默认为1,如果overide它,那么在除了1的其它所有情况下,Verify体系都只会计算表达式的值,而不会停止运行。
另外verifyfSlow宏貌似在某个版本中被删除了。
// 设置Mesh的值并确认是否为null,如果是,停止运行 // 这里使用verify的原因是不管怎样,Mesh都需要设置一个值 verify((Mesh = GetRenderMesh()) != nullptr);
Ensure
类似于Verify体系,Ensure和Verify一样始终(在shipping中也如此)计算表达式的值,但不同的是,它不会停止运行,而是通知crash reporter,程序接着run。
需要特别注意的是,为了防止crash reporter死命报告错误,一次引擎或编辑器会话中触发 ensure 断言只会报告一次,如果想总是报告,用带有Always的Ensure宏。
Macro | Parameters | Behavior |
---|---|---|
ensure | Expression | Expression为false时,通知crash reporter |
ensureMsgf | Expression, FormattedText, ... | Expression为false时,通知crash reporter,并在日志中输出FormattedText |
ensureAlways | Expression | 带有Always |
ensureAlwaysMsgf | Expression, FormattedText, ... | 带有Always |
在所有版本中都会计算表达式的值,但只会在Debug, Development, Test, and Shipping Editor builds版本中通知crash reporter。
// 这段代码可能会在shipping版本中有一个细小的错误,小到无需为它停止程序,就是想到也许已经修好了它,来验证一下 void AMyActor::Tick(float DeltaSeconds) { Super::Tick(DeltaSeconds); // 确保bWasInitialized是true,不是的话就会在log中输出信息 if (ensureMsgf(bWasInitialized, TEXT("%s ran Tick() with bWasInitialized == false"), *GetActorLabel())) { // (Do something that requires a properly-initialized AMyActor.) } }
(说一点,shipping editor版本被删掉了)
Game-Controlled Cameras
讲解如何控制Cameras,首先把Camera扔到level里。
创建一个继承自AActor的c++类,命名为CameraDirector。
// 在.h文件中添加以下成员变量,加到第二个个public里,为什么有两个public的区分(???) UPROPERTY(EditAnywhere) AActor* CameraOne; UPROPERTY(EditAnywhere) AActor* CameraTwo; float TimeToNextCameraChange; // 然后在ACameraDirector::Tick函数里添加以下代码 const float TimeBetweenCameraChanges = 2.0f; const float SmoothBlendTime = 0.75f; TimeToNextCameraChange -= DeltaTime; if (TimeToNextCameraChange <= 0.0f) { TimeToNextCameraChange += TimeBetweenCameraChanges; // 获取自己控制的actor,这里的重点就是获取自己的APlayerController // APlayerController是一个类,为什么这个类能在这里用,哈,你引入的头文件里面也引入了其它头文件,错综复杂,最终绝对引入了APlayerController.h,至于UGameplayStatics类,它就在GameplayStatics.h里 APlayerController* OurPlayerController = UGameplayStatics::GetPlayerController(this, 0); if (OurPlayerController) { // 开始换Camera了 if ((OurPlayerController->GetViewTarget() != CameraOne) && (CameraOne != nullptr)) { // Cut instantly to camera one. OurPlayerController->SetViewTarget(CameraOne); } else if ((OurPlayerController->GetViewTarget() != CameraTwo) && (CameraTwo != nullptr)) { // Blend smoothly to camera two. OurPlayerController->SetViewTargetWithBlend(CameraTwo, SmoothBlendTime); } } } // 这段代码会让我们每三秒切换一次Camera
接下来在Editor的C++文件夹里找到你的CameraDirector类,扔进level里,再在Detial面板里设置CameraOne和CameraTwo,其实设置成不是CameraActor的类也行(阿这)。
文档里的练习跟着做一下。Exercise
Player Input and Pawns
用Pawn类来接受player输入。
创建一个继承自Pawn的c++类,就命名为MyPawn。
// 在Constructor里添加以下代码,先让它能自动接受输入信息,并将它设置成由第一位player控制 AutoPossessPlayer = EAutoReceiveInput::Player0; // 在.h文件里引入以下头文件 #include "Kismet/GameplayStatics.h" #include "Camera/CameraComponent.h" // 在头文件里创建Component,如下 UPROPERTY(EditAnywhere) USceneComponent* OurVisibleComponent; UPROPERTY(EditAnywhere) UCameraComponent* OurCamera; // 回到Constructor里,添加以下代码 // 创建一个虚的root component,相当于一个pivot point,这里的RootComponent是Actor的成员变量,即再Actor.h里定义的 RootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("RootComponent")); // 创建一个Camera Component,并给之前声明的OurVisibleComponent赋值 // 这里的CreateDefaultSubobject的返回值就是USenceComponent及其子类(写在<>里的),应该就是用来创建组件的 OurCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("OurCamera")); OurVisibleComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("OurVisibleComponent")); // 将Camera和VisibleComponent连到root component上,并转一下Camera // 这里的Relative位置应该就是根组件的相对位置,也就是这虚假的root component所在的pivot point的相对位置 OurCamera->SetupAttachment(RootComponent); OurCamera->SetRelativeLocation(FVector(-250.0f, 0.0f, 250.0f)); OurCamera->SetRelativeRotation(FRotator(-45.0f, 0.0f, 0.0f)); OurVisibleComponent->SetupAttachment(RootComponent);
记住把MyPawnActor扔到level里后还需选中它然后选择一个StaticMesh给它用,先选择这个组件再把StaticMesh拖过去,如图
有两种映射输入的类型:Action和Axis(轴)。
Action Mapping:适用于Yes/No的输入,像是按下鼠标或手柄,按下、松开、双击或短按,长按都可以用这种映射方式。
Axis Mapping:适用于那种连续的输入,像是一直推着手柄操纵杆,或是鼠标光标的位置,即使没有发生改变,它们仍会每一帧地报告自己的值。
尽管设置银蛇输入可以在代码中进行,但一般我们在Editor里弄这玩意。
在Project Setting->Engine->Input自己去设置吧,很简单,这里设置的是你Action Mapping的按键,Axis Mapping的按键和每一帧会产生的值。
下面就在代码中使用这些值。
// 首先再头文件里声明这些函数和变量 //Input functions void Move_XAxis(float AxisValue); void Move_YAxis(float AxisValue); void StartGrowing(); void StopGrowing(); //Input variables FVector CurrentVelocity; bool bGrowing; // 在.cpp文件里实现它们 void AMyPawn::Move_XAxis(float AxisValue) { // 每秒向前或向后移动100个单元,这里的单元应该指AxisValue,即你在Editor里设置的数字 CurrentVelocity.X = FMath::Clamp(AxisValue, -1.0f, 1.0f) * 100.0f; } void AMyPawn::Move_YAxis(float AxisValue) { // 每秒向前或向后移动100个单元,这里的单元应该指AxisValue,即你在Editor里设置的数字 CurrentVelocity.Y = FMath::Clamp(AxisValue, -1.0f, 1.0f) * 100.0f; } // FMath::Clamp()函数能限定值在一定范围内,如果有多个键能对该值造成影响,可以防止同时按下这几个键时,该值偏离过大 void AMyPawn::StartGrowing() { bGrowing = true; } void AMyPawn::StopGrowing() { bGrowing = false; } // 下面代码添加到AMyPawn::SetupPlayerInputComponent里去,就是将按键所传达的值经上面几个函数转化后,与InputComponent绑定 // 绑定Action Mapping,实质是判断你“Grow”这个键有没有按下 InputComponent->BindAction("Grow", IE_Pressed, this, &AMyPawn::StartGrowing); InputComponent->BindAction("Grow", IE_Released, this, &AMyPawn::StopGrowing); // 绑定Axis Mapping,实质是判断你“Move_X/Y”这个键有没有按下 InputComponent->BindAxis("MoveX", this, &AMyPawn::Move_XAxis); InputComponent->BindAxis("MoveY", this, &AMyPawn::Move_YAxis); 上面都是绑定,下面就是绑定后能用按下按键所传入的值做些什么 // 基与“Grow” Action放大或缩小 { float CurrentScale = OurVisibleComponent->GetComponentScale().X; if (bGrowing) { CurrentScale += DeltaTime; } else { CurrentScale -= (DeltaTime * 0.5f); } // 确保不会比一开始的尺寸小,以及一次不会变大两倍 CurrentScale = FMath::Clamp(CurrentScale, 1.0f, 2.0f); OurVisibleComponent->SetWorldScale3D(FVector(CurrentScale)); } // 基与“Move_X/Y”Axis控制移动 { if (!CurrentVelocity.IsZero()) { FVector NewLocation = GetActorLocation() + (CurrentVelocity * DeltaTime); SetActorLocation(NewLocation); } }
Components and Collision
介绍如何用Components让Pawn于Collision等等交互。
一样,创建一个继承自Pawn的c++类,命名为CollidingPawn。
// 在.h文件里加入以下成员变量 // 在.CPP文件里引入以下头文件,都是要用到的,也就实现两个东西,基础的物理碰撞,和一点小小的里子特效(摩擦起火) #include "UObject/ConstructorHelpers.h" #include "Particles/ParticleSystemComponent.h" #include "Components/SphereComponent.h" #include "Camera/CameraComponent.h" #include "GameFramework/SpringArmComponent.h"