HUD UI

基础内容

Posted by AIaimuti on May 1, 2020

今天要做的是关于UI界面的一些功能,显示生命值、体力条、金币数量,并将这些UI与人物的属性连接起来,动态设置

UI设置–生命值、体力条、整体界面和金币数量

生命值

右键内容浏览器–>Edit Utilities–>Editor Widget创建UI界面–>名称为WB_Health
1)在panel面板中选取一个HorizontalBox水平框和一个ProgressBar进度条,水平框中物体都会水平摆放,竖直框中物体都会竖直摆放。
将竖直框放入水平框之下,在Slot的Size中Fill填充,使进度条填充水平框。

2)我们先将Progress中的percent调成0.2,方便我们添加主体颜色和背景颜色进行对比。
主体颜色:Appearance–>Fill Color–>红色(1,0,0)
背景颜色:Style–>Background–>Tint着色–>最后一行A设置0.2

绑定蓝图事件

Progress中的percent使用Bind,绑定蓝图事件
其中先选中进度条,并在编辑界面右上角设置为变量,这样就可以在蓝图里使用
Get Owning Player Pawn–>CastTo Man–>将Man提升为变量RefMan
因为Man变量的属性都在玩家控制的Pawn之中,因此将Pawn类型强制转换为Man
Get Percent 0–>IsValid?return value:CastTo Man?return value:return value(原始设置的值);
这里用了C++的问号表达式来描述过程:
获取百分比值时先判断值是否有效,如果有效,则返回百分比;若无效,则转化类型
转换类型若若成功则,返回百分比;若失败则返回默认值
return value的值为从Man中读取出的生命值/最大生命值,即百分比
return value(原始设置的值)是进度条的初始设定值

体力条

复制生命值蓝图,命名WB_Stamina,颜色设置为黄色(1,1,0)
蓝图事件与生命值基本一致,只需将其中生命值的获取改为体力条

金币数量

1)金币需要在panel面板中选取一个HorizontalBox水平框,其附件选取Text文字框以及一个SizeBox尺寸盒子并Fill填充,尺寸盒子可以保持长宽比
尺寸盒子附件添加一个图片框用并放一个金币图片(PS可以用魔术橡皮擦去除白色背景,但需到保存为png文件),Text文字框将text框居中
金币图片在Size Box–>Child Layout中Min Aspect Ratio和Max Aspect Ratio设置为1,锁定长宽比

绑定蓝图事件

生命值和体力条返回的是浮点类型,金币数量返回的是文本型,但整体逻辑是一样的 另外,将Text文字框提升为变量, 这里return value的值为从Man中读取出硬币数量
return value(原始设置的值)是Text文字框的初始设定值

整体界面

右键内容浏览器–>Edit Utilities–>Editor Widget创建UI界面–>名称为WB_HUD
1)在panel面板中选取一个竖直框和两个水平框,两个水平框为附件;
两个水平框同时Fill,分成了上下两部分,上方Size设置为0.15,上小下大,上面用于放生命值、体力条和金币数量状态显示的地方
2)在上面水平框内加入一个竖直框和两个水平框并Fill填充,第一个竖直框放生命值和体力条,最后一个放金币显示,第一个Size 0.4,最后一个0.3
在第一个水平框中加入两个竖直盒子Fill填充,padding 设置空隙10,从Uesr Created中拖拽出生命值和体力条放入

程序设置

UI显示设置

1)这样设置完并不能直接在UI中显示,需要在Bulid.cs中PublicDependencyModuleNames加入UMG模块
2)另外需要创建一个继承玩家控制器的自定义控制器类MainPlayerController,这样我们能够制定我们自定义的HUD用户控件。

public:
//UUserWidget子类的一个类型声明,对象为用户控件	
UPROPERTY(EditAnywhere, BlueprintReadWrite)
	TSubclassOf<UUserWidget> HUDAsset;
//抬头显示器,一种用户控件
UUserWidget *HUD;

3)函数实现
添加头文件:#include “Blueprint/UserWidget.h” 用于用户控制

void AMainPlayerController::BeginPlay()
{
        Super::BeginPlay();

	if (HUDAsset)
	{
		//用HUDAsset这个类去生成一个实例HUD
		HUD = CreateWidget<UUserWidget>(this, HUDAsset);
	}
	if (HUD)
	{	
		//将实例显示到屏幕上
		HUD->AddToViewport();
	}
}

这样在游戏开始阶段,使用该控制器就会将我们选取的用户控制器实例,投射到屏幕上。
本文中的例子就是创建蓝图BP_PlayerController继承于MainPlayerController,在HUDAsset中选取WB_HUD,在世界选项中选BP_PlayerController作为控制器

生命值等与变量关联实时变化

人物相关属性初始化

生命值等属性于任务属性,因为写在人物的父类Man中 添加生命值、体力条、金币数量和施加伤害、死亡和拾取函数

//生命值和最大生命值
UPROPERTY(EditAnywhere, BlueprintReadWrite)
    float Health;

UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
    float MaxHealth;
//体力条和最大体力条
UPROPERTY(EditAnywhere, BlueprintReadWrite)
    float Stamina;

UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
    float MaxStamina;
//金币数量
UPROPERTY(EditAnywhere, BlueprintReadWrite)
    int32 Coins;

//施加伤害函数
void SufferDamage(float Damage);
//死亡事件
void Die();
//拾取函数
void Pickup(EPickupType Type, uint32 Count);

另外需要枚举定义金币类型(目前只有一种),给Pickup函数使用

enum EPickupType
{
	PT_Coin,
};

施加伤害以及拾取函数的实现

传入参数demage,每次调用SufferDamage,生命值减少demage

void AMan::SufferDamage(float Damage)
{
	float Tmp = Health - Damage;
	//试算一下伤害后的生命值,并约束到0到MaxHealth之间
	Tmp = FMath::Clamp(Tmp, 0.f, MaxHealth);
	Health = Tmp;
	if (Health <= 0)
	{
		Die();
	}
}

void AMan::Die()
{
}

传入参数EPickupType,每次调用根据金币的类型,设置Count,增加UI金币显示数量

void AMan::Pickup(EPickupType Type, uint32 Count)
{
	//目前只有PT_Coin一种类型
	switch (Type)
	{
	case PT_Coin:
		Coins += Count;
		break;
	default:
		break;
	}
}

Explosive Item实现爆炸施加伤害

爆炸物是我们自定义Item的一个子类,在爆炸物Explosive.h中加入构造函数和伤害值

public:
	AExplosive();

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
		float Damage;

Explosive.cpp 加入头文件#include “Man.h” 构造函数中设置伤害为10, 碰撞函数中,获取触发对象转换为Man类型,调用SufferDamage(Damage)对实例造成伤害

AExplosive::AExplosive()
{
	Damage = 10;
}

void AExplosive::OnOverlapBegin_Item(UPrimitiveComponent * OverlappedComponent, AActor * OtherActor, UPrimitiveComponent * OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult & SweepResult)
{
	Super::OnOverlapBegin_Item(OverlappedComponent, OtherActor, OtherComp, OtherBodyIndex, bFromSweep, SweepResult);
	UE_LOG(LogTemp, Warning, TEXT("%s"), *FString(__FUNCTION__));

	if (OtherActor)
	{	
		//拿到伤害对象这个实例
		AMan * Man = Cast<AMan>(OtherActor);
		if (Man)
		{	
			//对实例造成伤害
			Man->SufferDamage(Damage);
		}
	}
}

Pickup Item拾取金币

添加头文件#include “Man.h”
碰撞函数中,获取触发对象转换为Man类型,碰到PT_Coin,则调用Pickup函数增加数量

void APickup::OnOverlapBegin_Item(UPrimitiveComponent * OverlappedComponent, AActor * OtherActor, UPrimitiveComponent * OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult & SweepResult)
{
	Super::OnOverlapBegin_Item(OverlappedComponent, OtherActor, OtherComp, OtherBodyIndex, bFromSweep, SweepResult);
	UE_LOG(LogTemp, Warning, TEXT("%s"), *FString(__FUNCTION__));

	if (OtherActor)
	{
		AMan * Man = Cast<AMan>(OtherActor);
		if (Man)
		{
			Man->Pickup(PT_Coin, 1);
		}
	}
}

体力条变化与状态机修改

体力条变化与动画相关,这里单独拿出来写

相关变量初始化

体力条相关变量初始化,
状态变量、设置状态函数:用于设置状态的变化
普通奔跑速度和冲刺速度:根据不同的状态变换速度
冲刺状态开始和结束的函数
IsExhausted判断体力是否耗尽 另外,在轴映射处将Shift绑定Sprint

public:
//定义当前状态变量
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
	EStatus CurrentStatus;
//设置当前状态
void SetStatus(EStatus Status);
//奔跑速度
UPROPERTY(EditAnywhere, BlueprintReadOnly)
	float RunSpeed;
//冲刺速度
UPROPERTY(EditAnywhere, BlueprintReadOnly)
	float SprintSpeed;
//开始冲刺
UFUNCTION()
	void BeginSprint();
//结束冲刺
UFUNCTION()
	void EndSprint();
UFUNCTION()
	void StaminaChange(float DeltaTime);
//表示是否体力耗尽
UPROPERTY(EditAnywhere, BlueprintReadOnly)
	bool IsExhausted;
//体力变化速度
UPROPERTY(EditAnywhere, BlueprintReadOnly)
	float StaminaDrainRate;

枚举定义运动状态,根据状态关联体力条

UENUM(BlueprintType)
enum class EStatus :uint8
{
	//定义两种状态,一种普通状态,一种冲刺状态
	ES_Normal UMETA(DisplayName = "Normal"),
	ES_Sprint UMETA(DisplayName = "Sprint"),
};

函数实现

变量设置初值
当前状态为ES_Normal,设置默认速度为600;
设置普通奔跑速度600和冲刺速度1000;
IsExhausted初始为false;
体力变化速度为30(正常来讲体力增加和减少应该是两个速度);

CurrentStatus = EStatus::ES_Normal;
GetCharacterMovement()->MaxWalkSpeed = 600;
RunSpeed = 600;
SprintSpeed = 1000;
IsExhausted = false;
StaminaDrainRate = 30;

状态设置函数,普通奔跑设置速度为RunSpeed 冲刺时设置速度为SprintSpeed

void AMan::SetStatus(EStatus Status)
{
	CurrentStatus = Status;
	switch (CurrentStatus)
	{
	case EStatus::ES_Normal:
		GetCharacterMovement()->MaxWalkSpeed = RunSpeed;
		break;
	case EStatus::ES_Sprint:
		GetCharacterMovement()->MaxWalkSpeed = SprintSpeed;
		break;
	default:
		break;
	}
}

体力条变化,平常状态下体力增加,小于最大体力值时,显示当前体力;大于最大体力值时,显示最大体力值
冲刺状态下体力减少,大于最小体力值时,显示当前体力;体力小于0时,IsExhausted为true,设置为普通状态恢复体力
case后面带个{},不然可能会出现错误;该函数放入tick中,每帧调用计算

void AMan::StaminaChange(float DeltaTime)
{
	float Delta = StaminaDrainRate * DeltaTime;
	switch (CurrentStatus)
	{
		//如果是平常体力
	case EStatus::ES_Normal:
	{
		float Tmp = Stamina + Delta;
		if (Tmp < MaxStamina)
		{
			Stamina = Tmp;
		}
		else
		{
			Stamina = MaxStamina;
			IsExhausted = false;
		}
		break;
	}
	case EStatus::ES_Sprint:
	{
		float Tmp = Stamina - Delta;
		if (Tmp > 0)
		{
			Stamina = Tmp;
		}
		else
		{
			Stamina = 0;
			IsExhausted = true;
			SetStatus(EStatus::ES_Normal);
		}
		break;
	}

	default:
		break;
	}
}

// Called every frame
void AMan::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
	StaminaChange(DeltaTime);
}

SetupPlayerInputComponent绑定BeginSprint()和EndSprint()
如果体力没耗尽,按下shift变为冲刺状态,松开变为普通

void AMan::BeginSprint()
{
	if (!IsExhausted)
	{
		SetStatus(EStatus::ES_Sprint);
	}
}

void AMan::EndSprint()
{
	if (!IsExhausted)
	{
		SetStatus(EStatus::ES_Normal);
	}
}

// Called to bind functionality to input
void AMan::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);
	//开始结束冲刺
	PlayerInputComponent->BindAction("Sprint", IE_Pressed, this, &AMan::BeginSprint);
	PlayerInputComponent->BindAction("Sprint", IE_Released, this, &AMan::EndSprint);
}

状态机修改

因为涉及修改动画,并且我们需要Man的变量来作为动画转换的变量 因此,在我们创建的动画蓝图类MainAnimInstance.h中需要加入Man的引用,

//引入Man变量,用于与动画状态关联
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Movement)
	class AMan *Man;

MainAnimInstance.cpp中添加头文件#include “Man.h” 并将获取的pawn转换为Man,以使用Man中变量

void UMainAnimInstance::NativeInitializeAnimation()
{
	//获取pawn的拥有者
	if (!Pawn)
	{
		Pawn = TryGetPawnOwner();
	}
	Man = Cast<AMan>(Pawn);
}

我们将之前做的混合动画1D的范围改到1000,分格改成5格
之前我们做的混合动画
0的地方放站立动画,600的地方放普通速度移动,1000的地方放快速运动,这样就初步设置了与Man中状态变量相关的动画
但仅仅这样设置是不够的,仅这样设置,当按下shift的时候没有动画。
因此,将动画蓝图中状态机里加入一个冲刺状态
idle/walk/run状态 Man–>Get Status == Sprint?进入冲刺状态,播放冲刺动画:idle/walk/run状态
Sprint状态 Man–>Get Status == Normal?进入idle/walk/run状态,播放动画:Sprint状态
这样,当按下shift的时候就是Sprint状态,就可以播放相关动画

体力耗尽,体力条颜色修改

Apperence中的Fill Color and Opacity使用Bind,绑定蓝图事件;
整体逻辑与之前一样;
Get Fill Color and Opacity 0–>IsValid?return value:CastTo Man?return value:return value(原始设置的值);
return value的值使用Select Actor,以Man中的IsExhausted为依据,若为false则为黄色(1,1,0),若为true,则为灰色(0.5,0.5,0.5);
return value(原始设置的值)是颜色的初始设定值