斯坦福UE4C++課程P55-P58帶有C++和更多框架擴(kuò)展的UMG
這一節(jié)首先我們寫一個控件基類,給AI顯示出血條、傷害值(之前的damage控件),也能夠把interact的信息顯示在屏幕上(比如按F打開寶箱)。
新建C++類,繼承自UserWidget(任何控件藍(lán)圖都繼承自該類),命名為SWorldUserWidget。
我們從UserWidget類中找到NativeTick函數(shù),該函數(shù)每幀把世界坐標(biāo)投影到屏幕坐標(biāo)(使用Super調(diào)用的超類實(shí)現(xiàn))。
void USWorldUserWidget::NativeTick(const FGeometry& MyGeometry, float InDeltaTime)
{
Super::NativeTick(MyGeometry, InDeltaTime);
? ?FVector2D ScreenPosition;
if (UGameplayStatics::ProjectWorldToScreen(GetOwningPlayer(), AttachedActor->GetActorLocation(), ScreenPosition))
{
float Scale = UWidgetLayoutLibrary::GetViewportScale(this);
ScreenPosition /= Scale;
if (ParentSizeBox)
{
ParentSizeBox->SetRenderTranslation(ScreenPosition);
}
}
}
我們添加
UPROPERTY(meta = (BindWidget))
USizeBox* ParentSizeBox;
在藍(lán)圖中我們創(chuàng)建名為ParentSizeBox的sizebox時,上面的指針會指向它。
編譯后,我們創(chuàng)建藍(lán)圖類MinionHealth_Widget,繼承自SWorldUserWidgetC++類,此時會報(bào)錯,因?yàn)槲覀冞€沒有添加sizebox。把一個sizebox拖進(jìn)畫布,命名為ParentSizeBox,就不報(bào)錯了。右鍵點(diǎn)擊ParentSizeBox->Wrapped with,用Canvas Panel包裹它。再添加一個圖片

把圖片設(shè)置為之前我們做的M_HealthBar血條材質(zhì),現(xiàn)在改變圖片的尺寸,畫布中看出其尺寸沒有改變。點(diǎn)擊ParentSizeBox,勾選Size To Content,表示使用孩子的尺寸。此時改變圖片的尺寸,畫布中其尺寸就變了。
我們希望AI受到傷害時才顯示該控件。
所以進(jìn)入到SAICharacter類,找到OnHealthChanged函數(shù),這里是我們想要亮AI血條的時機(jī)。
// 首次受到傷害時才創(chuàng)建血條
if (ActiveHealthBar == nullptr)
{
ActiveHealthBar = CreateWidget<USWorldUserWidget>(GetWorld(), HealthBarWidgetClass);
if (ActiveHealthBar)
{
ActiveHealthBar->AttachedActor = this;
ActiveHealthBar->AddToViewport();
}
}
ActiveHealthBar是一個局部變量,存儲首次受傷害時創(chuàng)建的控件。
SWorldUserWidget.h:
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "Components/SizeBox.h"
#include "SWorldUserWidget.generated.h"
/**
*
*/
UCLASS()
class ACTIONROGUELIKE_API USWorldUserWidget : public UUserWidget
{
GENERATED_BODY()
protected:
UPROPERTY(meta = (BindWidget))
USizeBox* ParentSizeBox;
virtual void NativeTick(const FGeometry& MyGeometry, float InDeltaTime) override;
public:
// 加uproperty是為了1、和藍(lán)圖建立聯(lián)系2、放置游戲運(yùn)行過程中,如果對象被銷毀,我們可以在C++中立刻知道,就無需處理空指針的情況了。
UPROPERTY(BlueprintReadOnly, Category = "UI")
AActor* AttachedActor;
};
SWorldUserWidget.cpp:
// Fill out your copyright notice in the Description page of Project Settings.
#include "SWorldUserWidget.h"
#include "Blueprint/WidgetLayoutLibrary.h"
#include "Kismet/GameplayStatics.h"
void USWorldUserWidget::NativeTick(const FGeometry& MyGeometry, float InDeltaTime)
{
Super::NativeTick(MyGeometry, InDeltaTime);
// 當(dāng)AttachedActor為空時(比如繼承該類的AI血條類,AI在被玩家殺死后銷毀),直接結(jié)束函數(shù)
if (!IsValid(AttachedActor))
{
RemoveFromParent();
UE_LOG(LogTemp, Warning, TEXT("AttachedActor no longer valid, removing Health Widget."));
return;
}
? ?FVector2D ScreenPosition;
if (UGameplayStatics::ProjectWorldToScreen(GetOwningPlayer(), AttachedActor->GetActorLocation() + WorldOffset, ScreenPosition))
{
float Scale = UWidgetLayoutLibrary::GetViewportScale(this);
ScreenPosition /= Scale;
if (ParentSizeBox)
{
ParentSizeBox->SetRenderTranslation(ScreenPosition);
}
}
}
現(xiàn)在我們編譯,回到AI血條控件藍(lán)圖:

運(yùn)行,發(fā)現(xiàn)攻擊AI第一次時,血條是滿血,再次攻擊時才是正常的血量,這是因?yàn)?,我們顯示血條的時機(jī)在首次更新血量值之前。最簡單的解決辦法:

我們直接在上圖位置調(diào)用OnhealthChanged函數(shù),提前更新生命值。
另外,在后面判斷如果NewHealth小于等于0,就刪除AI血條控件。

最后我們在C++控件SWorldUserWidget中添加偏移量,讓我們能夠更改其在屏幕的位置
UPROPERTY(EditAnywhere, Category = "UI")
FVector2D WorldOffset;
cpp文件中,在投影到屏幕語句加上該偏移量。
if (UGameplayStatics::ProjectWorldToScreen(GetOwningPlayer(), AttachedActor->GetActorLocation() + WorldOffset, ScreenPosition))
這樣,我們就可以讓繼承自該類的血條能夠不恰好在AI中間,而是顯示在頭頂、腳下等位置

接下來我們添加包含健康、積分、游戲時間等等控件的主HUD
我們新建一個藍(lán)圖主HUD類,我們把之前做的一些小控件比如十字準(zhǔn)星、玩家血條放在里面,還可以把要做的積分、游戲時間等要在屏幕上顯示的信息都放在其中(可以在一個控件藍(lán)圖把其他控件藍(lán)圖拖進(jìn)來)。
進(jìn)入角色藍(lán)圖類,把創(chuàng)建顯示十字準(zhǔn)星和玩家血條的節(jié)點(diǎn)刪掉,換成創(chuàng)建顯示Main_HUD的節(jié)點(diǎn)。

現(xiàn)在和之前一樣,但血條和準(zhǔn)星的位置由Main_HUD決定。
我們新建積分、游戲時間控件藍(lán)圖,添加到主HUD控件藍(lán)圖。
調(diào)整到如下,添加vertical box約束血條和積分,游戲時間錨點(diǎn)設(shè)置右上,勾選size to content,在文本變動時自動控制大?。?/p>
其中,游戲時間控件的文本綁定函數(shù):

獲取服務(wù)器時間是為了在多人游戲時,每個人獲取到的時間一致。(兩個玩家先后進(jìn)入游戲,顯示的時間均為服務(wù)器時間,而非自己機(jī)器的Get Time Seconds獲取到的從0開始的時間)。Time Seconds To String節(jié)點(diǎn)讓時間以數(shù)字表的形式給出,更加美觀。
現(xiàn)在運(yùn)行游戲:

此P的作用是,避免我們每加一個控件都要到角色類create widget和add to viewport,直接創(chuàng)建顯示主HUD,把一堆小控件拖到主HUD即可一勞永逸。

這一P我們設(shè)置適當(dāng)?shù)耐婕以偕?/p>
我們在GameModeBP中設(shè)置Default Pawn Class為角色類藍(lán)圖PlayerCharacter;把之前給角色的auto possess從player 0還原為disabled。拖入視口兩個角色出生點(diǎn)Player Start,刪掉之前拖進(jìn)視口的第二個玩家,現(xiàn)在唯一的玩家將隨機(jī)選擇一個出生點(diǎn)開始游戲。
我們可以在項(xiàng)目設(shè)置里設(shè)置默認(rèn)的游戲模式和默認(rèn)pawn類,后續(xù)可以在world settings里override掉默認(rèn)的游戲模式、默認(rèn)pawn類等等。

最后我們添加調(diào)試命令
控制臺命令可以寫像殺死全部AI、無敵、+99條命這種上帝性的行為,對debug很有用(調(diào)游戲難度、開無敵闖關(guān)。。。)。
在角色C++類添加(確保在public下)
// .h
UFUNCTION(Exec)
void HealSelf(float Amount = 100);
// .cpp
void ASCharacter::HealSelf(float Amount /* = 100*/)
{
AttributeComp->ApplyHealthChange(this, Amount);
}
表示這是一個控制臺命令,可更改角色生命值,默認(rèn)值為100。
tip:在角色類、玩家控制器類、gamemode類和cheat manager類(查看該類,發(fā)現(xiàn)許多上帝行為的函數(shù))可以寫進(jìn)去控制臺命令。
在游戲運(yùn)行時,按~鍵打開控制臺,輸入HealSelf,直接回車將回默認(rèn)的100血,給第二個參數(shù)的話,表示更改的生命值。
下面在SGameModeBase添加殺死所有AI的控制臺命令。
// .h
UFUNCTION(Exec)
void KillAll();
// .cpp
void ASGameModeBase::KillAll()
{
? ?// 遍歷所有AI角色
for (TActorIterator<ASAICharacter> It(GetWorld()); It; ++It)
{
ASAICharacter* Bot = *It;
USAttributeComponent* AttributeComp = USAttributeComponent::GetAttributes(Bot);
if (ensure(AttributeComp) && AttributeComp->IsAlive())
{
AttributeComp->kill(this); // @fixme: pass in player? for kill credit
}
}
}
其中kill函數(shù)是屬性組件類新寫的函數(shù),給Instigator添加-HealthMax的生命值改變。
運(yùn)行游戲,控制臺輸入KillAll,立即殺死所有AI。
下面我們找到cheat manager類的God()函數(shù)控制臺命令,里邊有CanBeDamaged函數(shù),返回bCanBeDamaged變量(控制臺可調(diào))。如果我們控制臺輸入God,bCanBeDamaged就為true。
我們在屬性組件類的ApplyHealthChange函數(shù)最開始添加
if (!GetOwner()->CanBeDamaged())
{
return false;
}
此時如果玩家開了God控制臺命令,我們就無敵了(不掉血不加血)。