Programming with C++
先是一些入門的小東西,
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,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)
{
// ...
}
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 = https://www.cnblogs.com/cordial-12/p/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拖過去,記住一定要點到那個用來Visual的組件,
有兩種映射輸入的型別: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();
// 注意與ActionMapping對應的函式無需引數
//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()函式能限定值在一定范圍內,如果有多個鍵能對該值造成影響,可以防止同時按下這幾個鍵時,該值偏離過大
// 注意與ActionMapping對應的函式無需引數
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,na
// 在.h檔案里加入以下成員變數,這里加上class,作用是宣告,因為懶得在頭檔案里為這一個而引入頭檔案,另外,有些Component需要在頭檔案里宣告,有些不要,區別在于你需不需要一直追蹤它,就是在很多地方使用它,需要那就宣告
UPROPERTY()
class UParticleSystemComponent* OurParticleSystem;
// 在.CPP檔案里引入以下頭檔案,都是要用到的,也就實作兩個東西,基礎的物理碰撞,一點小小的粒子特效(摩擦起火),和彈性Camera
#include "UObject/ConstructorHelpers.h"
#include "Particles/ParticleSystemComponent.h"
#include "Components/SphereComponent.h"
#include "Camera/CameraComponent.h"
#include "GameFramework/SpringArmComponent.h"
// 下面的代碼全都加在Constructor里
// 下面設定球形碰撞網格體,并把它設定成root component
USphereComponent* SphereComponent = CreateDefaultSubobject<USphereComponent>(TEXT("RootComponent"));
// 下面這行位置不要搞反,畢竟*RootComponent是USenceComponent的,是USphereComponent的父類
RootComponent = SphereComponent;
SphereComponent->InitSphereRadius(40.0f);
SphereComponent->SetCollisionProfileName(TEXT("Pawn"));
// 下面再設定可見球體組件,要注意的是,上面我們把球形碰撞體半徑設定成了40.0f,下面綁上去的mesh資源實際上半徑有50.0f,所以得縮小
UStaticMeshComponent* SphereVisual = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("VisualRepresentation"));
SphereVisual->SetupAttachment(RootComponent);
// 如果想直接在Editor里編輯的話,下面這一步和那個if就都不需要,和之前講的MyPawn類一樣,在類里宣告該Component,并設定為EditAnywhere,下面這個指定資源和那個if就都不用了,但if里的那個位置和大小得在編輯器里有所設定
static ConstructorHelpers::FObjectFinder<UStaticMesh> SphereVisualAsset(TEXT("/Game/StarterContent/Shapes/Shape_Sphere.Shape_Sphere"));
if (SphereVisualAsset.Succeeded())
{
SphereVisual->SetStaticMesh(SphereVisualAsset.Object);
SphereVisual->SetRelativeLocation(FVector(0.0f, 0.0f, -40.0f));
SphereVisual->SetWorldScale3D(FVector(0.8f));
}
// 上面這里以及即將要說的下面都要明白一點,就是它們都是直接用代碼把mesh資源綁上去,而一般的做法是直接在.h檔案里宣告Visible Component成員變數并設定為EditAnywhere,然后Constructor里設定該Component,再就是像上一小節所講的一樣,進Editor里直接把mesh拖過去,不過嘛,直接用代碼的話,也方便Debug及創建新features
// 下面將之前在頭檔案里宣告的粒子特效組件給實作了
OurParticleSystem = CreateDefaultSubobject<UParticleSystemComponent>(TEXT("MovementParticles"));
// 注意這里將其與SphereVisual相連,并設定在SphereVisual的底部,讓其更像腳底生花(火花)
OurParticleSystem->SetupAttachment(SphereVisual);
OurParticleSystem->bAutoActivate = false;
OurParticleSystem->SetRelativeLocation(FVector(-20.0f, 0.0f, 20.0f));
static ConstructorHelpers::FObjectFinder<UParticleSystem> ParticleAsset(TEXT("/Game/StarterContent/Particles/P_Fire.P_Fire"));
if (ParticleAsset.Succeeded())
{
OurParticleSystem->SetTemplate(ParticleAsset.Object);
}
// 下面添加彈性攝像頭(Spring Arm Camera),就是那種一旦你跑快了,視角追不上你的那種感覺
// 說明一下,這里是帶有彈性的攝像頭,Play后會是第三人稱視角,如果不設定攝像頭位置或干脆不要這個彈性攝像頭組件,那么會默認第一人稱視角
USpringArmComponent* SpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraAttachmentArm"));
SpringArm->SetupAttachment(RootComponent);
SpringArm->SetRelativeRotation(FRotator(-45.f, 0.f, 0.f));
// 距離pivot point的距離
SpringArm->TargetArmLength = 400.0f;
// 彈性功能的開關,主要體現在按下移動鍵后攝像頭會延遲移動,再配合攝像頭移動速度,從而實作彈性
SpringArm->bEnableCameraLag = true;
// 攝像頭速度
SpringArm->CameraLagSpeed = 3.0f;
UCameraComponent* Camera = CreateDefaultSubobject<UCameraComponent>(TEXT("ActualCamera"));
// 這里的Socket也相當于組件,是“插”上去的
Camera->SetupAttachment(SpringArm, USpringArmComponent::SocketName);
// 讓Player0的輸入資訊控制這些玩意
AutoPossessPlayer = EAutoReceiveInput::Player0;
再在Editor里設定輸入,如下,記住這些名字要與代碼里的PlayerInputComponent->BindAction/Axis()里的第一個引數對應,
這里換一種方法,就是不把控制移動的代碼也塞入CollidingPawn類里,而是另建新類來專門設定控制移動,在添加c++類選擇繼承類時,點擊右上角ShowAllClass,然后搜索PawnMoveComponent類,就命名為CollidingPawnMovementComponent,
// 首先需要宣告多載一個TickComponent來決定每一幀的運動,它類似于Actor的Tick
public:
virtual void TickComponent(float DeltaTime, enum EleveTick TickType, FActorComponentTickFunction *ThisTickFunction) override;
// 簡單回顧一下virtual和override,virtual會允許其派生類多載該函式,若函式后面加上=0,則為純虛函式,那么該類無法實作它,只提供一個介面,供子類多載,override能確保該函式是對父類函式的多載,不然會報錯
// 接下來在.cpp檔案里實作它
void UCollidingPawnMovementComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction)
{
// 再說明一下,Super會自動代指擁有這個函式的某個父類
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
// 確保引數都是合規的
if (!PawnOwner || !UpdatedComponent || ShouldSkipUpdate(DeltaTime))
{
return;
}
// ConsumeInputVector()能得到并清空我們用來存盤movement inputs的內置變數
FVector DesiredMovementThisFrame = ConsumeInputVector().GetClampedToMaxSize(1.0f) * DeltaTime * 150.0f;
if (!DesiredMovementThisFrame.IsNearlyZero())
{
FHitResult Hit;
// 這里創建一個FHitResult,用來存盤撞擊所產生的一些值
SafeMoveUpdatedComponent(DesiredMovementThisFrame, UpdatedComponent->GetComponentRotation(), true, Hit);
// 現在就可以用FHitResult所存盤的值來判斷,撞擊后會沿著障礙物表面跑,沒有這些的話,會直接黏在障礙物上
if (Hit.IsValidBlockingHit())
{
SlideAlongSurface(DesiredMovementThisFrame, 1.f - Hit.Time, Hit.Normal, Hit);
}
}
};
// UPawnMovementComponent類提供了一些很強的的函式,就如上面提到的ConsumeInpuVector(), SafeMoveUpdatedComponent(), SlideAlongSurface(),當然還有其它如Floating Pawn Movement, Spectator Pawn Movement, or Character Movement Component,也都很jb強
兩邊的基礎設定都已經弄好了,現在在CollidingPawn里使用CollidingPawnMovementComponent類,
// 在CollidingPawn頭檔案里宣告,同樣,若是懶得為這一個成員而引入一個頭檔案,那就加個class直接宣告
UPROPERTY()
class UCollidingPawnMovementComponent* OurMovementComponent;
// 為了能持續跟蹤上面這個成員(???),在CollidingPawn源檔案里添加相應的頭檔案,記得加到GameFramework/Pawn.h下面
#include "CollidingPawnMovementComponent.h"
// 在Constructor里添加下面的代碼,為OurMovementComponent添加一個實體,并將其UpdatedComponent成員設為RootComponent,即讓它去更新RootComponent
OurMovementComponent = CreateDefaultSubobject<UCollidingPawnMovementComponent>(TEXT("CustomMovementComponent"));
OurMovementComponent->UpdatedComponent = RootComponent;
// 多載下面這個函式,在頭檔案里宣告,該函式由Pawn類提供
virtual UPawnMovementComponent* GetMovementComponent() const override;
// 并在源檔案里實作
UPawnMovementComponent* ACollidingPawn::GetMovementComponent() const
{
return OurMovementComponent;
}
// 接下來就又是運動和特效的一些宣告和定義了
void MoveForward(float AxisValue);
void MoveRight(float AxisValue);
void Turn(float AxisValue);
void ParticleToggle();
// 實作它們,都需要確認一下你實體化的自定義PawnMoveComponent子類是否存在
void ACollidingPawn::MoveForward(float AxisValue)
{
if (OurMovementComponent && (OurMovementComponent->UpdatedComponent == RootComponent))
{
OurMovementComponent->AddInputVector(GetActorForwardVector() * AxisValue);
}
}
void ACollidingPawn::MoveRight(float AxisValue)
{
if (OurMovementComponent && (OurMovementComponent->UpdatedComponent == RootComponent))
{
OurMovementComponent->AddInputVector(GetActorRightVector() * AxisValue);
}
}
void ACollidingPawn::Turn(float AxisValue)
{
FRotator NewRotation = GetActorRotation();
NewRotation.Yaw += AxisValue;
SetActorRotation(NewRotation);
}
void ACollidingPawn::ParticleToggle()
{
// 這里就要看你的ParticleSystemComponent有沒有實體成功以及Template有沒有設定
if (OurParticleSystem && OurParticleSystem->Template)
{
OurParticleSystem->ToggleActive();
}
}
// 以及在ACollidingPawn::SetupPlayerInputComponent()里映射這些運動定義,ActionMapping和AxisMapping
PlayerInputComponent->BindAction("ParticleToggle", IE_Pressed, this, &ACollidingPawn::ParticleToggle);
PlayerInputComponent->BindAxis("MoveForward", this, &ACollidingPawn::MoveForward);
PlayerInputComponent->BindAxis("MoveRight", this, &ACollidingPawn::MoveRight);
PlayerInputComponent->BindAxis("Turn", this, &ACollidingPawn::Turn);
總結一下流程,就是說:
先在繼承自Pawn的類里將一系列組件(如彈性Camera,粒子效果,VisualSphere)系結好;
然后創建繼承自PawnMovementComponent的類,這個父類本身就已經定義了很多東西,我們只需在Constructor里用Super繼承過來就行,所以上面只添加了一個碰撞后沿碰撞體表面移動的代碼;
再回到那個定義了一系列組件的類里,將剛剛創建好的那個繼承自PawnMovementComponent的類實體化,并使之更新root Component,之后就可以在定義運動和特效的一些函式里使用這個實體,很方便;
最后,就只需要在SetupPlayerInputComponent里,將Action和Axis兩種映射系結好就行了,
Variables, Timers, and Events
介紹一下如何將變數和函式暴露給Editor,如何用Timer延遲或重復代碼運行,如何用Events與Actor互動,
創建一個繼承自Actor的類,命名為Countdown,
// 添加到頭檔案里
#include "Components/TextRenderComponent.h"
UPROPERTY(EditAnywhere)
int32 CountdownTime;
// 可渲染文本組件…………
UTextRenderComponent* CountdownText;
void UpdateTimerDisplay();
// Constructor實作
ACountdown::ACountdown()
{
// 讓這個Actor每幀都回應一下,如果不需要可以關閉
PrimaryActorTick.bCanEverTick = false;
CountdownText = CreateDefaultSubobject<UTextRenderComponent>(TEXT("CountdownNumber"));
// 設定需要該component與之對齊的水平線
CountdownText->SetHorizontalAlignment(EHTA_Center);
CountdownText->SetWorldSize(150.0f);
// 就將它作為根組件
RootComponent = CountdownText;
// 之前宣告的是int32嘛,這里應該是沒必要設定成float
CountdownTime = 3;
}
// 實作UpdateTimeDisplay,設定需要顯示出來的Text,這里設定的是剩下多少秒
void ACountdown::UpdateTimeDisplay()
{
CountdownText->SetText(FString::FromInt(FMath::Max(CountdownTime, 0)));
}
// 當一個函式需要定時器Timer時,我們首先在.h檔案中定義定時器句柄TimeHandle----也就是定義一個定時器手柄,用來控制Timer,在頭檔案里宣告下面的東西
FTimerHandle CountdownTimerHandle; // 宣告的TimeHandle
void AdvanceTimer();
// 回顧一下,BlueprintNativeEvent,除了可以讓藍圖呼叫此c++函式之外,就是函式在c++里宣告并實作的時候,它可以在藍圖里再實作一份,這樣兩邊的函式都會在被呼叫時生效執行,但這需要你在c++里宣告該函式后,不用實作它,而是另外宣告并實作它相應的一個虛函式,該虛函式的函式名加個后綴_Implementation(),并在藍圖中的Event [FunctionName]節點后插進一個Parent:[FunctionName]節點,方法是右鍵Event [FunctionName]節點選擇Add call to parent function
UFUNCTION(BlueprintNativeEvent)
void CountdownHasFinished();
virtual void CountdownHasFinished_Implementation();
// 實作它們
void ACountdown::AdvanceTimer()
{
--CountdownTime;
UpdateTimerDisplay();
if (CountdownTime < 1)
{
// 倒計時完畢,讓Timer停下來
GetWorldTimerManager().ClearTimer(CountdownTimerHandle);
CountdownHasFinished();
}
}
void ACountdown::CountdownHasFinished_Implementation()
{
CountdownText->SetText(TEXT("GO!"));
}
// 在BeginPlay()中添加一下代碼,GetWorldTimerManager()需要在BeginPlay() 中呼叫,在建構式中叫他人家會崩潰
UpdateTimerDisplay();
GetWorldTimerManager().SetTimer(CountdownTimerHandle, this, &ACountdown::AdvanceTimer, 1.0f, true);
// SetTimer()是這樣的,第一個引數得是你所設立的TimerHandle,第二個引數時哪個物件使用這個Timer,第三個是每隔一段時間,這個時間就是第四個引數,會執行的函式,第五個就是第一次執行該函式后,要不要把每隔一段時間執行函式這個操作一直做下去
接下來,把該類直接拖進Viewport中,然后生成一個它的藍圖,在Event Graph里添加節點Event Countdown Has Finished,然后拉出線生成一個叫Spawn Emitter At Location的節點,Emitter是輻射源的意思,用以生成一個粒子效果,只要在左邊的pin中將Location拉出來生成一個Get Actor Location節點,使粒子效果在Actor腳下生成,在Emitter Template注腳中選擇特效即可,最后就是插入一個Parent節點,讓c++里的CountdownHasFinished函式也能生效,方法上面說過了,
另外,可以在后面跟著加上Delay和DestroyActor兩個節點來讓它顯示完后消失,
User Interface With UMG
介紹創建一個簡單的Game開始選單,
首先在我們專案的.Build.cs檔案,做以下兩個修改,目的是添加一些模塊,
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "UMG" });
PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
// 注意一個時Public,一個時Private
然后,創建繼承自GameMode的類,如果啟動專案時打開了c++,那它會自動幫你創一個,添加以下成員,
public:
// 這里竟然直接復制一個實體傳過去???,不過這里確實也很特殊,必須在Editor里指定這個NewWidgetClass所代表的UMG資源,不然CreateWidget函式那里會報錯
UFUNCTION(BlueprintCallable, Category = "UMG Game")
void ChangeMenuWidget(TSubclassOf<UUserWidget> NewWidgetClass);
// Widget,即控制元件,這一節可以大致理解為選單
protected:
// 手動宣告此多載函式
virtual void BeginPlay() override;
// 作為在Editor里選擇的UMG資源的參考
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UMG Game")
TSubclassOf<UUserWidget> StartingWidgetClass;
// 在Widget創造后指向它的指標
UPROPERTY()
UUserWidget* CurrentWidget;
// 以及引入的頭檔案
#include "Blueprint/UserWidget.h"
// 下面是實作
void A[ProjectName]GameMode::BeginPlay()
{
Super::BeginPlay();
ChangeMenuWidget(StartingWidgetClass);
}
// 如果本來Viewport上有正在顯示的widget,那么移除它,并把新的放進去
void A[ProjectName]GameMode::ChangeMenuWidget(TSubclassOf<UUserWidget> NewWidgetClass)
{
if (CurrentWidget != nullptr)
{
CurrentWidget->RemoveFromViewport();
CurrentWidget = nullptr;
}
if (NewWidgetClass != nullptr)
{
// 為你在Editor里指定的那個UMG資源給個UUserWidget記憶體空間并用指標記住它,有一種“正名化為UserWidget”的感覺
CurrentWidget = CreateWidget<UUserWidget>(GetWorld(), NewWidgetClass);
if (CurrentWidget != nullptr)
{
CurrentWidget->AddToViewport();
}
}
}
再創建一個繼承自PlayerController的類,隨便命個名,如MyPlayerController,
// 宣告并實作,這些東西的作用就是讓選單能與滑鼠游標互動
public:
virtual void BeginPlay() override;
void AHowTo_UMGPlayerController::BeginPlay()
{
Super::BeginPlay();
SetInputMode(FInputModeGameAndUI());
}
現在已經弄完顯示和關閉選單的基本代碼框架了,接下來到Editor里弄一下選單上需要有的一些圖示,Editor->Add New->User Interface->Widget Blueprint,命名為NewGameWidget,打開創建的Widget藍圖類,從左邊的Palette面板里將Button和Text拖出來,Text直接放到Button上面,這里不用圖片而是用Text直接代表Button上需要顯示的內容,調整Button實體和Text實體的細節,如下,
(將Visiblity設為Not Hit-Testable(Self&All Children),上圖是老版本的,可以防止文本塊阻礙滑鼠點擊下方按鈕)
然后像上面一樣,創建一個用于退出選單的按鈕,同樣拖出Button和Text,Button實體命名為QuitButton,Text命名為QuitText,其具體顯示文本命名為Quit,前者位置設定為 (-200, -400),大小為(200, 100),后者直接放到前者上面,會自動粘合,
接下來在NewGameButton和QuitButton的細節面板的Events里添加OnClicked事件,如下設定藍圖節點,這里就呼叫了之前設定的c++函式,
(這里ChangeMenuWidget節點的NewWidgetClass引數用的就是我們創建的這個叫NewGameWidget的UserWidget藍圖類,這就是為之前代碼部分頭檔案里宣告的那個TSubclassOf<UUserWidget>成員變數指定一個UMG資源,這里就是為什么我們在這里只做完一級選單后點New Game按鈕仍沒效果的原因)
往后,我們創建一個繼承自我們創建的[ProjectName]GameMode類的藍圖類,命名為MenuGameMode,再創建一個繼承自PlayerController類(就在常用類里)的藍圖類,命名為MenuPlayerController,
打開MenuPlayerController,在細節面板里的Mouse Interface里,打開Show Mouse Cusor選項,
打開MenuGameMode,看向細節面板,StatingWidgetClass必須設定為我們之前創建的那個叫NewGameWidget的UserWidget藍圖類;Classes下的PlayerControllerClass需設定為上面創建的MenuPlayerController,這樣打開選單時才有滑鼠的游標,DefaultPawnClass需設定為Pawn,不然在打開選單的時候Player他可能會跟著滑鼠或鍵盤亂飛,
現在到WorldSetting里,將GameModeOverride改為我們的MenuGameMode藍圖類,不要用成創建的C++類,這個MenuGameMode畢竟是它的子類,功能在此完善,這樣,簡單的一級選單構建完成
(說明一下,這里如果就Play的話,點擊New Game按鈕沒用,是因為在按鈕的藍圖節點里我們設定的用來切換的選單就是它自己,所以沒用,在之后用這個來喚起二級選單)
下面創建第二級選單,含有輸入名字的視窗,回傳一級選單的按鈕,以及不輸入名字就按不了的Play按鈕,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/501835.html
標籤:其他
上一篇:遞回回呼的實作
下一篇:類暗黑破壞神屬性系統思路
