斯坦福UE4C++课程P55-P58带有C++和更多框架扩展的UMG
这一节首先我们写一个控件基类,给AI显示出血条、伤害值(之前的damage控件),也能够把interact的信息显示在屏幕上(比如按F打开宝箱)。
新建C++类,继承自UserWidget(任何控件蓝图都继承自该类),命名为SWorldUserWidget。
我们从UserWidget类中找到NativeTick函数,该函数每帧把世界坐标投影到屏幕坐标(使用Super调用的超类实现)。
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;
在蓝图中我们创建名为ParentSizeBox的sizebox时,上面的指针会指向它。
编译后,我们创建蓝图类MinionHealth_Widget,继承自SWorldUserWidgetC++类,此时会报错,因为我们还没有添加sizebox。把一个sizebox拖进画布,命名为ParentSizeBox,就不报错了。右键点击ParentSizeBox->Wrapped with,用Canvas Panel包裹它。再添加一个图片

把图片设置为之前我们做的M_HealthBar血条材质,现在改变图片的尺寸,画布中看出其尺寸没有改变。点击ParentSizeBox,勾选Size To Content,表示使用孩子的尺寸。此时改变图片的尺寸,画布中其尺寸就变了。
我们希望AI受到伤害时才显示该控件。
所以进入到SAICharacter类,找到OnHealthChanged函数,这里是我们想要亮AI血条的时机。
// 首次受到伤害时才创建血条
if (ActiveHealthBar == nullptr)
{
ActiveHealthBar = CreateWidget<USWorldUserWidget>(GetWorld(), HealthBarWidgetClass);
if (ActiveHealthBar)
{
ActiveHealthBar->AttachedActor = this;
ActiveHealthBar->AddToViewport();
}
}
ActiveHealthBar是一个局部变量,存储首次受伤害时创建的控件。
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、和蓝图建立联系2、放置游戏运行过程中,如果对象被销毁,我们可以在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);
// 当AttachedActor为空时(比如继承该类的AI血条类,AI在被玩家杀死后销毁),直接结束函数
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);
}
}
}
现在我们编译,回到AI血条控件蓝图:

运行,发现攻击AI第一次时,血条是满血,再次攻击时才是正常的血量,这是因为,我们显示血条的时机在首次更新血量值之前。最简单的解决办法:

我们直接在上图位置调用OnhealthChanged函数,提前更新生命值。
另外,在后面判断如果NewHealth小于等于0,就删除AI血条控件。

最后我们在C++控件SWorldUserWidget中添加偏移量,让我们能够更改其在屏幕的位置
UPROPERTY(EditAnywhere, Category = "UI")
FVector2D WorldOffset;
cpp文件中,在投影到屏幕语句加上该偏移量。
if (UGameplayStatics::ProjectWorldToScreen(GetOwningPlayer(), AttachedActor->GetActorLocation() + WorldOffset, ScreenPosition))
这样,我们就可以让继承自该类的血条能够不恰好在AI中间,而是显示在头顶、脚下等位置

接下来我们添加包含健康、积分、游戏时间等等控件的主HUD
我们新建一个蓝图主HUD类,我们把之前做的一些小控件比如十字准星、玩家血条放在里面,还可以把要做的积分、游戏时间等要在屏幕上显示的信息都放在其中(可以在一个控件蓝图把其他控件蓝图拖进来)。
进入角色蓝图类,把创建显示十字准星和玩家血条的节点删掉,换成创建显示Main_HUD的节点。

现在和之前一样,但血条和准星的位置由Main_HUD决定。
我们新建积分、游戏时间控件蓝图,添加到主HUD控件蓝图。
调整到如下,添加vertical box约束血条和积分,游戏时间锚点设置右上,勾选size to content,在文本变动时自动控制大小:

其中,游戏时间控件的文本绑定函数:

获取服务器时间是为了在多人游戏时,每个人获取到的时间一致。(两个玩家先后进入游戏,显示的时间均为服务器时间,而非自己机器的Get Time Seconds获取到的从0开始的时间)。Time Seconds To String节点让时间以数字表的形式给出,更加美观。
现在运行游戏:

此P的作用是,避免我们每加一个控件都要到角色类create widget和add to viewport,直接创建显示主HUD,把一堆小控件拖到主HUD即可一劳永逸。

这一P我们设置适当的玩家再生
我们在GameModeBP中设置Default Pawn Class为角色类蓝图PlayerCharacter;把之前给角色的auto possess从player 0还原为disabled。拖入视口两个角色出生点Player Start,删掉之前拖进视口的第二个玩家,现在唯一的玩家将随机选择一个出生点开始游戏。
我们可以在项目设置里设置默认的游戏模式和默认pawn类,后续可以在world settings里override掉默认的游戏模式、默认pawn类等等。

最后我们添加调试命令
控制台命令可以写像杀死全部AI、无敌、+99条命这种上帝性的行为,对debug很有用(调游戏难度、开无敌闯关。。。)。
在角色C++类添加(确保在public下)
// .h
UFUNCTION(Exec)
void HealSelf(float Amount = 100);
// .cpp
void ASCharacter::HealSelf(float Amount /* = 100*/)
{
AttributeComp->ApplyHealthChange(this, Amount);
}
表示这是一个控制台命令,可更改角色生命值,默认值为100。
tip:在角色类、玩家控制器类、gamemode类和cheat manager类(查看该类,发现许多上帝行为的函数)可以写进去控制台命令。
在游戏运行时,按~键打开控制台,输入HealSelf,直接回车将回默认的100血,给第二个参数的话,表示更改的生命值。
下面在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函数是属性组件类新写的函数,给Instigator添加-HealthMax的生命值改变。
运行游戏,控制台输入KillAll,立即杀死所有AI。
下面我们找到cheat manager类的God()函数控制台命令,里边有CanBeDamaged函数,返回bCanBeDamaged变量(控制台可调)。如果我们控制台输入God,bCanBeDamaged就为true。
我们在属性组件类的ApplyHealthChange函数最开始添加
if (!GetOwner()->CanBeDamaged())
{
return false;
}
此时如果玩家开了God控制台命令,我们就无敌了(不掉血不加血)。