Introduction to C++ Programming in UE4 章节学习(持续更新)
作者:互联网
Introduction to C++ Programming in UE4
先是一些入门的小东西。
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(如下),是引擎最基本的两个类。
- 映射properties和methods
- properities的序列化
- 垃圾回收
- 通过名字寻找UObject
- 给propeties配置值
- properties和methods的网络工作支持
每一个继承自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
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)
{
// ...
}
Memory Management and Garbage Collection
说在前头,垃圾回收机制,清除的是不再被引用(被指针指向)或已被显式标记为即将被回收的内存。
你创建了一个类A,在类中定义一些成员指针变量,其类型是B,它会指向一块内存,该指针便是对这块内存的一个reference,垃圾回收便是把这指针所指向的内存回收,并把指针设置为nullptr。
至于你所建的类A,在其它类中,可能会有A类型的指针,它申请一块内存,至于这块内存是否在垃圾回收系统范畴,就看你建的这个类A,是否符合规定。
所以,你在你所建立的类里应该讨论的是成员变量,讨论类本身在类内是没有意义的。
UObject
UE使用映射系统来执行垃圾回收,需要垃圾回收机制的类需是UObject或其子类。
垃圾回收有一个概念叫做root set,即一个包含一些对象的列表,回收系统保证不会回收这些对象。把这个列表想象成一棵树,树所触及不到的实例对象,全都当垃圾回收了。垃圾回收会一轮一轮在固定时间进行。
UObject不会被当作垃圾回收的条件有三种:
- UObject对象被加入到root set上(调用AddRoot函数)。
- 直接或者间接被root set 里的对象引用(如UPROPERTY宏修饰的UObject成员变量 注:UObject放在UPROPERTY宏修饰的TArray、TMap中也可以)
- 直接或间接被存活的FGCObject对象引用(后面会讲)
举例
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自定义的智能指针。\
UE Type
Class
特殊的命名规则,给予特殊的便利与保护。
- 继承自AActor,名前会加上A
- 继承自UObject,名前会加上U
- Enums类型前会加上E
- Interface类型前会加上I
- Template类型前会加上T
- 继承自SWidget (Slate UI),名前会加上S
- 其它都会加上F
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)。
Asserts
首先回顾一下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版本被删掉了)
Programming Basics
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"
标签:UObject,Introduction,void,Programming,回收,垃圾,UPROPERTY,C++,指针 来源: https://www.cnblogs.com/cordial-12/p/16562328.html