主頁 >  其他 > Programming with C++ 章節學習(持續更新,如有錯誤,感謝指正)

Programming with C++ 章節學習(持續更新,如有錯誤,感謝指正)

2022-08-15 08:24:31 其他

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不會被當作垃圾回收的條件有三種:

  1. UObject物件被加入到root set上(呼叫AddRoot函式),
  2. 直接或者間接被root set 里的物件參考(如UPROPERTY宏修飾的UObject成員變數 注:UObject放在UPROPERTY宏修飾的TArray、TMap中也可以)
  3. 直接或間接被存活的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 integer
  • int16/uint16: 16-bit signed/unsigned integer
  • int32/uint32: 32-bit signed/unsigned integer
  • int64/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的三種等價體系, checkverifyensure,三個有細微差別,但主要作用相同,都宣告于 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的組件,

1

有兩種映射輸入的型別: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

標籤:其他

上一篇:遞回回呼的實作

下一篇:類暗黑破壞神屬性系統思路

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 網閘典型架構簡述

    網閘架構一般分為兩種:三主機的三系統架構網閘和雙主機的2+1架構網閘。 三主機架構分別為內端機、外端機和仲裁機。三機無論從軟體和硬體上均各自獨立。首先從硬體上來看,三機都用各自獨立的主板、記憶體及存盤設備。從軟體上來看,三機有各自獨立的作業系統。這樣能達到完全的三機獨立。對于“2+1”系統,“2”分為 ......

    uj5u.com 2020-09-10 02:00:44 more
  • 如何從xshell上傳檔案到centos linux虛擬機里

    如何從xshell上傳檔案到centos linux虛擬機里及:虛擬機CentOs下執行 yum -y install lrzsz命令,出現錯誤:鏡像無法找到軟體包 前言 一、安裝lrzsz步驟 二、上傳檔案 三、遇到的問題及解決方案 總結 前言 提示:其實很簡單,往虛擬機上安裝一個上傳檔案的工具 ......

    uj5u.com 2020-09-10 02:00:47 more
  • 一、SQLMAP入門

    一、SQLMAP入門 1、判斷是否存在注入 sqlmap.py -u 網址/id=1 id=1不可缺少。當注入點后面的引數大于兩個時。需要加雙引號, sqlmap.py -u "網址/id=1&uid=1" 2、判斷文本中的請求是否存在注入 從文本中加載http請求,SQLMAP可以從一個文本檔案中 ......

    uj5u.com 2020-09-10 02:00:50 more
  • Metasploit 簡單使用教程

    metasploit 簡單使用教程 浩先生, 2020-08-28 16:18:25 分類專欄: kail 網路安全 linux 文章標簽: linux資訊安全 編輯 著作權 metasploit 使用教程 前言 一、Metasploit是什么? 二、準備作業 三、具體步驟 前言 Msfconsole ......

    uj5u.com 2020-09-10 02:00:53 more
  • 游戲逆向之驅動層與用戶層通訊

    驅動層代碼: #pragma once #include <ntifs.h> #define add_code CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS) /* 更多游戲逆向視頻www.yxfzedu.com ......

    uj5u.com 2020-09-10 02:00:56 more
  • 北斗電力時鐘(北斗授時服務器)讓網路資料更精準

    北斗電力時鐘(北斗授時服務器)讓網路資料更精準 北斗電力時鐘(北斗授時服務器)讓網路資料更精準 京準電子科技官微——ahjzsz 近幾年,資訊技術的得了快速發展,互聯網在逐漸普及,其在人們生活和生產中都得到了廣泛應用,并且取得了不錯的應用效果。計算機網路資訊在電力系統中的應用,一方面使電力系統的運行 ......

    uj5u.com 2020-09-10 02:01:03 more
  • 【CTF】CTFHub 技能樹 彩蛋 writeup

    ?碎碎念 CTFHub:https://www.ctfhub.com/ 筆者入門CTF時時剛開始刷的是bugku的舊平臺,后來才有了CTFHub。 感覺不論是網頁UI設計,還是題目質量,賽事跟蹤,工具軟體都做得很不錯。 而且因為獨到的金幣制度的確讓人有一種想去刷題賺金幣的感覺。 個人還是非常喜歡這個 ......

    uj5u.com 2020-09-10 02:04:05 more
  • 02windows基礎操作

    我學到了一下幾點 Windows系統目錄結構與滲透的作用 常見Windows的服務詳解 Windows埠詳解 常用的Windows注冊表詳解 hacker DOS命令詳解(net user / type /md /rd/ dir /cd /net use copy、批處理 等) 利用dos命令制作 ......

    uj5u.com 2020-09-10 02:04:18 more
  • 03.Linux基礎操作

    我學到了以下幾點 01Linux系統介紹02系統安裝,密碼啊破解03Linux常用命令04LAMP 01LINUX windows: win03 8 12 16 19 配置不繁瑣 Linux:redhat,centos(紅帽社區版),Ubuntu server,suse unix:金融機構,證券,銀 ......

    uj5u.com 2020-09-10 02:04:30 more
  • 05HTML

    01HTML介紹 02頭部標簽講解03基礎標簽講解04表單標簽講解 HTML前段語言 js1.了解代碼2.根據代碼 懂得挖掘漏洞 (POST注入/XSS漏洞上傳)3.黑帽seo 白帽seo 客戶網站被黑帽植入劫持代碼如何處理4.熟悉html表單 <html><head><title>TDK標題,描述 ......

    uj5u.com 2020-09-10 02:04:36 more
最新发布
  • 2023年最新微信小程式抓包教程

    01 開門見山 隔一個月發一篇文章,不過分。 首先回顧一下《微信系結手機號資料庫被脫庫事件》,我也是第一時間得知了這個訊息,然后跟蹤了整件事情的經過。下面是這起事件的相關截圖以及近日流出的一萬條資料樣本: 個人認為這件事也沒什么,還不如關注一下之前45億快遞資料查詢渠道疑似在近日復活的訊息。 訊息是 ......

    uj5u.com 2023-04-20 08:48:24 more
  • web3 產品介紹:metamask 錢包 使用最多的瀏覽器插件錢包

    Metamask錢包是一種基于區塊鏈技術的數字貨幣錢包,它允許用戶在安全、便捷的環境下管理自己的加密資產。Metamask錢包是以太坊生態系統中最流行的錢包之一,它具有易于使用、安全性高和功能強大等優點。 本文將詳細介紹Metamask錢包的功能和使用方法。 一、 Metamask錢包的功能 數字資 ......

    uj5u.com 2023-04-20 08:47:46 more
  • vulnhub_Earth

    前言 靶機地址->>>vulnhub_Earth 攻擊機ip:192.168.20.121 靶機ip:192.168.20.122 參考文章 https://www.cnblogs.com/Jing-X/archive/2022/04/03/16097695.html https://www.cnb ......

    uj5u.com 2023-04-20 07:46:20 more
  • 從4k到42k,軟體測驗工程師的漲薪史,給我看哭了

    清明節一過,盲猜大家已經無心上班,在數著日子準備過五一,但一想到銀行卡里的余額……瞬間心情就不美麗了。最近,2023年高校畢業生就業調查顯示,本科畢業月平均起薪為5825元。調查一出,便有很多同學表示自己又被平均了。看著這一資料,不免讓人想到前不久中國青年報的一項調查:近六成大學生認為畢業10年內會 ......

    uj5u.com 2023-04-20 07:44:00 more
  • 最新版本 Stable Diffusion 開源 AI 繪畫工具之中文自動提詞篇

    🎈 標簽生成器 由于輸入正向提示詞 prompt 和反向提示詞 negative prompt 都是使用英文,所以對學習母語的我們非常不友好 使用網址:https://tinygeeker.github.io/p/ai-prompt-generator 這個網址是為了讓大家在使用 AI 繪畫的時候 ......

    uj5u.com 2023-04-20 07:43:36 more
  • 漫談前端自動化測驗演進之路及測驗工具分析

    隨著前端技術的不斷發展和應用程式的日益復雜,前端自動化測驗也在不斷演進。隨著 Web 應用程式變得越來越復雜,自動化測驗的需求也越來越高。如今,自動化測驗已經成為 Web 應用程式開發程序中不可或缺的一部分,它們可以幫助開發人員更快地發現和修復錯誤,提高應用程式的性能和可靠性。 ......

    uj5u.com 2023-04-20 07:43:16 more
  • CANN開發實踐:4個DVPP記憶體問題的典型案例解讀

    摘要:由于DVPP媒體資料處理功能對存放輸入、輸出資料的記憶體有更高的要求(例如,記憶體首地址128位元組對齊),因此需呼叫專用的記憶體申請介面,那么本期就分享幾個關于DVPP記憶體問題的典型案例,并給出原因分析及解決方法。 本文分享自華為云社區《FAQ_DVPP記憶體問題案例》,作者:昇騰CANN。 DVPP ......

    uj5u.com 2023-04-20 07:43:03 more
  • msf學習

    msf學習 以kali自帶的msf為例 一、msf核心模塊與功能 msf模塊都放在/usr/share/metasploit-framework/modules目錄下 1、auxiliary 輔助模塊,輔助滲透(埠掃描、登錄密碼爆破、漏洞驗證等) 2、encoders 編碼器模塊,主要包含各種編碼 ......

    uj5u.com 2023-04-20 07:42:59 more
  • Halcon軟體安裝與界面簡介

    1. 下載Halcon17版本到到本地 2. 雙擊安裝包后 3. 步驟如下 1.2 Halcon軟體安裝 界面分為四大塊 1. Halcon的五個助手 1) 影像采集助手:與相機連接,設定相機引數,采集影像 2) 標定助手:九點標定或是其它的標定,生成標定檔案及內參外參,可以將像素單位轉換為長度單位 ......

    uj5u.com 2023-04-20 07:42:17 more
  • 在MacOS下使用Unity3D開發游戲

    第一次發博客,先發一下我的游戲開發環境吧。 去年2月份買了一臺MacBookPro2021 M1pro(以下簡稱mbp),這一年來一直在用mbp開發游戲。我大致分享一下我的開發工具以及使用體驗。 1、Unity 官網鏈接: https://unity.cn/releases 我一般使用的Apple ......

    uj5u.com 2023-04-20 07:40:19 more