TomLooman_ActionRoguelike_第七章UMG和Player屬性
該專欄用于保存對TomLooman的ActionRoguelike項目的學(xué)習(xí)筆記,學(xué)習(xí)過程中的思考與記錄不一定準(zhǔn)確。
教程參考:https://github.com/tomlooman/ActionRoguelike
基于UE5.0的項目實現(xiàn):https://github.com/CarolBaggins2023/TomLooman_ActionRoguelike_Tutorial

2023_08_05
UMG和Player屬性:屬性組件,帶有數(shù)據(jù)綁定的UMG,基于事件的UMG,用屬性進(jìn)行傷害
?
我們希望用一個東西來表示角色和物體的血量之類的屬性,最直接的方法是將這些屬性作為角色類的成員變量,但是由此而來的一個問題是角色類將會變得非常大,難以維護(hù),而且這些屬性只能用在某一個類中,復(fù)用性差,所以我們將這些屬性抽象成一個屬性類。
這個屬性組件類繼承自Actor組件,和交互組件類一樣。BlueprintSpawnableComponent是類元數(shù)據(jù)說明符,表示組件類可由藍(lán)圖生成。

?
在類中,我們聲明一個成員變量表示生命值,并想要通過一個成員函數(shù)修改它。



定義了屬性組件類之后,我們要將其作為角色類的一個成員變量,這樣一來,角色類通過屬性組件類的成員變量,相當(dāng)于擁有了屬性組件類中定義的那些屬性。


?
目前為止,還沒有東西能觸發(fā)屬性組件類中修改血量函數(shù)的執(zhí)行。我們希望在角色被子彈擊中時改變角色血量,所以我們在子彈類中定義overlap事件。
注意,OnComponentBeginOverlap相當(dāng)于藍(lán)圖中的Event(事件),與組件相關(guān),當(dāng)有東西和這個組件overlap時觸發(fā)(be called)。OnActorOverlap是類的成員函數(shù),與整個類的實例相關(guān)。AddDynamic將UObject類的實例(SphereComp)和類成員函數(shù)(OnActorOverlap)通過動態(tài)多播代理(OnComponentBeginOverlap)綁定到一起。所以這一行代碼表示,一旦觸發(fā)了SphereComp->OnComponentBeginOverlap這一事件,就執(zhí)行函數(shù)ASMagicProjectile::OnActorOverlap。

?
觸發(fā)事件后執(zhí)行的成員函數(shù)定義如下,這里的函數(shù)形式繼承自Actor基類。UFUNTCION()讓類的成員函數(shù)變?yōu)閁Function類型,UFunction是一種C++函數(shù),可以被UE的反射系統(tǒng)識別。


在函數(shù)的實現(xiàn)中,我們首先判斷了觸發(fā)碰撞組件overlap事件的Actor非空,且不是發(fā)出子彈的角色(這一點解決了之前向右移動時發(fā)出子彈,子彈會在角色身上explode的問題)。
然后我們獲取觸發(fā)事件的Actor上的屬性組件成員,通過GetComponentByClass函數(shù)實現(xiàn),其函數(shù)原型為UActorComponent* GetComponentByClass(TSubclassOf<UActorComponent> ComponentClass) const,因此需要一個類作為函數(shù)參數(shù),可通過類名::StaticClass()獲得一個類的引用。同時,因為GetComponentByClass得到的UActorComponent*類型的對象,而不是我們想要的屬性組件類對象這里需要進(jìn)行以此類型轉(zhuǎn)換,用Cast實現(xiàn)。
再然后我們判斷觸發(fā)事件額Actor是否有屬性組件成員,如果有的話,執(zhí)行該屬性組件成員中修改血量的成員函數(shù)。
為什么這里要定義碰撞組件的overlap事件,而不是hit事件?因為比方在游戲射擊游戲中,我們不想讓子彈對友方也造成傷害,但是友方和敵方的碰撞屬性又一般是同樣的,也就無法通過碰撞屬性區(qū)分,此時就可以對友方和敵方都視為overlap,但在overlap事件中判斷OtherActor是友方還是敵方。
另外,我們要修改子彈與世界中其他物體的碰撞屬性,將我們希望子彈能對它造成傷害的類型的碰撞屬性設(shè)為overlap。否則,如果還是block,會出現(xiàn)教程中所說的,角色發(fā)射子彈時,角色會產(chǎn)生一個位移,因為此時角色和子彈是block的,會發(fā)生hit事件,所以角色會被子彈推動。
?
有了角色的屬性之后,我們自然地想將屬性,比如血量,顯示在屏幕上,這里就要用到UMG,之前做準(zhǔn)心時使用過。
這里我們放置了一個Text表示屬性值,一個ProgressBar對屬性值進(jìn)行可視化,還用了一個HorizontalBox,這樣我們就不用自己布局進(jìn)行對其了。

我們可以將Text綁定到某個函數(shù)上,將函數(shù)返回值作為Text顯示的值。需要注意的是,這個綁定的函數(shù)是在每個Tick都會執(zhí)行的,所以可能開銷較大。

該函數(shù)的藍(lán)圖表示如下,

GetOwningPlayer的函數(shù)原型是virtual?APlayerController?* GetOwningPlayer() const,獲得的是PlayerController。GetOwningPlayerPawn的函數(shù)原型是APawn?* GetOwningPlayerPawn() const,獲得的是玩家控制的Character。

這里想要獲得角色的屬性組件成員的血量成員,還有一種方法,利用Cast將APawn*轉(zhuǎn)換為玩家控制的Character的類型,然后直接訪問成員變量。因為GetComponentByClass的做法需要遍歷Actor的組件,所以可能效率低一些。

另外,一開始對Character是否為空的判定是有用的,因為游戲一開始時角色可能還沒生成。
?
我們之前提到了這與Text綁定的函數(shù)會在每個Tick都執(zhí)行,所以它除了低效之外,還無法告知我們屬性變化的時間信息。如果我們想要在屬性變化,比如受傷減血時,顯示一些動畫,比如屏幕閃爍紅光,就需要一個類似Event事件的東西來觸發(fā)。
就像子彈類中做的那樣,觸發(fā)OnActorOverlap事件后,執(zhí)行被overlap的對象中的某個函數(shù)。但是現(xiàn)有的事件中并沒有能夠表示“角色的屬性組件成員中的血量成員發(fā)生變化”的事件,所以我們要自定義一個“事件”(C++中的委托在概念好像可以這么解釋)。








?
這個委托(Delegate)不一定要定義在屬性組件類中,當(dāng)委托被多個類使用時,也可以定義在一個單獨的頭文件中,然后在要使用該委托的文件中include即可。
(1)

從名字可以看出這里聲明了一個有四個參數(shù)的動態(tài)多播委托。第一個值是委托的名字,之后是參數(shù)。注意,和函數(shù)的參數(shù)列表不同的是,委托中的參數(shù)類型和參數(shù)用逗號隔開。
?
類似于函數(shù)類,我們聲明之后還需要實例化。我們在屬性組件類中將其實例化為一個成員變量。
同時,從它使用UPROPERTY中也可以看出,委托并不是一個函數(shù),而是更像一個類。
BlueprintCallable讓我們在UI中也可以調(diào)用這個事件。
(2)

?
目前為止,還無人觸發(fā)這個事件。因為這個事件是通知其它對象“角色血量改變”這一事實的,所以我們應(yīng)該在改變血量的函數(shù)中觸發(fā)該事件,這里的“觸發(fā)”對于委托來說就是“廣播”。
可以看出,委托的廣播與函數(shù)調(diào)用的形式類似,或者說類似于調(diào)用類的成員函數(shù),需要傳入委托聲明時要求的參數(shù)。
這里的InstigatorActor暫時為空,后續(xù)會補(bǔ)充。觸發(fā)該事件的就是這個屬性組件本身,所以第二個參數(shù)為this。
(4)

此時我們可以發(fā)現(xiàn),就像碰撞組件有OnComponentHit那些事件一樣,屬性組件中出現(xiàn)了我們自定義的OnHealthChanged事件。
我們也可以生成事件的藍(lán)圖節(jié)點,參數(shù)就是我們廣播傳入的參數(shù)。當(dāng)觸發(fā)該事件后,就進(jìn)行事件節(jié)點后的藍(lán)圖執(zhí)行流。



?
當(dāng)我們把“角色血量變化”構(gòu)建為一個事件,我們就可以在之前的UI中調(diào)用了。
首先看以下的藍(lán)圖部分,在這里我們先獲取玩家控制的Character,然后獲取Character中的屬性組件成員變量,因為屬性組件成員變量有我們想調(diào)用的FOnHealthChanged委托的示例OnHealthChanged。再然后是關(guān)鍵的一步,我們將這個委托綁定到另一個事件上,那么當(dāng)委托OnHealthChanged被觸發(fā)時,就會觸發(fā)所綁定的事件。
(3)

OnHealthChanged委托綁定的事件就是要修改我們血條和血量文字。以下是用ProcessBar實現(xiàn)血條時的藍(lán)圖。

?
上面涉及到了UI中的EventConstruct和EventPreConstruct,兩者的區(qū)別在于EventConstruct在我們運行游戲時才運行,而EventPreConstruct在設(shè)計時就運行。例如,如果我們在EventConstruct修改Text的文字,只有游戲運行時我們才能看到Text的變化,而如果我們在EventPreConstruct,我們在設(shè)計頁面就能看到Text的變化。
?
關(guān)于UE的委托(Delegate),最重要的就是上面內(nèi)容中的(1)(2)(3)(4),其中(3)和(4)的出現(xiàn)順序并沒有反。
根據(jù)這個例子,總結(jié)委托的使用步驟如下,
1. **在頭文件中聲明委托**:在您的類的頭文件中,使用 `DECLARE_DYNAMIC_MULTICAST_DELEGATE_FourParams` 宏來聲明您的委托類型。例如:
?
```cpp
DECLARE_DYNAMIC_MULTICAST_DELEGATE_FourParams(FOnHealthChanged, AActor*, InstigatorActor, USAttributeComponent*, OwningComp, float, NewHealth, float, Delta);
```
?
2. **聲明委托實例**:在您的類中,聲明一個具體的委托實例。這將成為您在需要時綁定和觸發(fā)事件的實例。例如:
?
```cpp
UCLASS()
class YourClass : public UObject
{
??? GENERATED_BODY()
?
public:
??? FOnHealthChanged OnHealthChangedDelegate;
};
```
?
3. **綁定函數(shù)到委托**:在適當(dāng)?shù)臅r候,您可以將函數(shù)綁定到委托實例上。這些函數(shù)將在委托被觸發(fā)時執(zhí)行。(在上面的例子中,我們沒有綁定函數(shù),而是綁定了一個事件,下面會有綁定函數(shù)的例子)例如:
?
```cpp
OnHealthChangedDelegate.AddDynamic(this, &YourClass::HealthChangedFunction);
```
?
4. **觸發(fā)委托**:在適當(dāng)?shù)臅r候,調(diào)用委托實例的觸發(fā)函數(shù),以執(zhí)行已綁定的函數(shù)。例如:
?
```cpp
OnHealthChangedDelegate.Broadcast(InstigatorActor, OwningComp, NewHealth, Delta);
```
?
在上述步驟中,`HealthChangedFunction` 是您要執(zhí)行的函數(shù),而 `InstigatorActor`、`OwningComp`、`NewHealth` 和 `Delta` 是函數(shù)的參數(shù)。
?
通過這些步驟,您可以使用聲明的委托類型來實現(xiàn)事件觸發(fā)、回調(diào)和監(jiān)聽機(jī)制,以響應(yīng)特定的事件情況。請注意,這只是一個基本的使用示例,具體的實現(xiàn)可能會根據(jù)您的需求而有所不同。
?
我們現(xiàn)在想給血量Text的UI做個動畫。
在設(shè)計界面可以方便地給某個Widget添加動畫的track,在track中可以選擇某個時間點,然后插入該時間點的Widget。例如在0時刻有Text的默認(rèn)形態(tài),我們在1時刻插入了Text的放大形態(tài)(修改Transform的Scale),然后動畫系統(tǒng)會進(jìn)行類似插值的操作,最終我們就會看到從0時刻到1時刻Text逐漸變大的動畫效果。


然后我們可以非常方便地在藍(lán)圖中用PlayAnimation調(diào)用這個動畫,
