UE UI学习之路
本文最后更新于208 天前,其中的信息可能已经过时,如有错误请联系作者

UE UI学习之路

投射物3DUI

  1. 创建新的UI蓝图,并调整Text文本的设置

  1. 创建新的Actor变量,勾选Instance Editable与Expose on Spawn,使变量暴露出来

  1. 为某材质与UI绑定蓝图,使其受到投射物攻击时UI生效

  1. 确保只有当我们攻击时,产生的伤害数字才会产生

  1. 为数字设置消亡时间

血量条

1. 创建血量属性

血量是角色的一种属性,要记录角色的血量数据,一种本能的做法是在SCharacter中定义一个Health变量。但这种方式的弊端显而易见:随着项目的扩展、游戏不断更新迭代新版本,角色的属性可能达到几十上百,若是加上角色的技能、特性、天赋或其他各种角色相关内容,SCharacter类的内容将膨胀到极难维护。

在UE中解决这个问题的办法也很简单,在之前开宝箱的文章中已经介绍过,可以使用UE中的ActorComponent类,让整个类以组件的形式被SCharacter拥有。因此,我们从ActorComponent创建角色的SAttributeComponent类,用于实现角色的各种属性。

随后,我们创建protected的浮点型Health,并提供public的set方法。如果有朋友对这种设计方法不理解,可以自行了解学习设计模式的相关知识,其中一个基础的原则就是尽量减少底层代码的暴露,即不要为了一时方便滥用public来修饰变量。

// SAttributeComponent.h
#pragma once

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "SAttributeComponent.generated.h"

DECLARE_DYNAMIC_MULTICAST_DELEGATE_FourParams(FOnHealthChanged,AActor*,InstigatorActor,USAttributeComponent*,OwningComp,float,NewHealth,float,Delta);


UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class ACTIONROGUELIKE_API USAttributeComponent : public UActorComponent
{
	GENERATED_BODY()

public:	
	// Sets default values for this component's properties
	USAttributeComponent();

protected:

	UPROPERTY(EditDefaultsOnly,BlueprintReadOnly,Category="Attributes")
	float Health;



public:
	
	UFUNCTION(BlueprintCallable)
	bool IsAlive() const;


	
	
	UPROPERTY(BlueprintAssignable)
	FOnHealthChanged OnHealthChanged;
	
	UFUNCTION(BlueprintCallable,Category="Attributes")
	bool ApplyHealthChange(float Delta);
	

};

//SAttributeComponent.cpp


#include "SAttributeComponent.h"

// Sets default values for this component's properties
USAttributeComponent::USAttributeComponent()
{
	// Set this component to be initialized when the game starts, and to be ticked every frame.  You can turn these features
	// off to improve performance if you don't need them.
	PrimaryComponentTick.bCanEverTick = true;

	// ... 
	Health = 100;
}

bool USAttributeComponent::IsAlive() const
{
	return Health>0.0f;
}


bool USAttributeComponent::ApplyHealthChange(float Delta)
{
	Health += Delta;

	OnHealthChanged.Broadcast(nullptr,this,Health,Delta);
	return true;
}

然后使用如下方法在SCharacter中声明组件,并在.cpp中创建相应实例:

UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
USAttributeComponent* AttributeComp;

2. 应用血量更改

为了测试血量组件能否正常运行,我们可以给魔法粒子附加伤害计算,并利用蓝图快速创建一个向玩家发射魔法粒子的敌人。

首先,在SMagicProjectile中为球体组件绑定一个OnComponentBeginOverlap事件,旨在当魔法粒子与物体重叠时触发。比起使用阻挡来触发,使用重叠可以通过判定重叠对象来更方便的忽略友军伤害,并且让魔法粒子直接穿过友军继续在场景中计算。

然后在函数中实现减少血量的代码

//SMagicProjectile.h
UFUNCTION()
	void OnActorOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
	UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);

//SMagicProjectile.cpp
SphereComp->OnComponentBeginOverlap.AddDynamic(this,&ASMagicProjectile::OnActorOverlap);

void ASMagicProjectile::OnActorOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
	UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	if(OtherActor && OtherActor != GetInstigator())
	{
		USAttributeComponent* AttributeComp = Cast<USAttributeComponent>(OtherActor->GetComponentByClass(USAttributeComponent::StaticClass()));
        // 再次判空,可能碰到的是墙壁、箱子等没有血量的物体
		if(AttributeComp)
		{
            // 魔法粒子造成DamageAmount血量伤害
			AttributeComp->ApplyHealthChange(-DamageAmount);
			
            // 一旦造成伤害就销毁,避免穿过角色继续计算
			Destroy();
		}
	}
}

由于我们希望子弹可以忽略友军,设置了重叠时开始事件,因此对应的Projectile碰撞设置也要进行更改:

在Player中利用蓝图实现在屏幕上打印血量的字符串,以让我们实时了解角色的血量。这个蓝图实现很简单,唯一注意的是设置显示时长为0并关闭Print to Log,这样屏幕上只会出现一个数字(事件Tick会在每一帧都调用),且不会在Log中输出大量无用信息:

从Actor派生一个名为ProjectileDemo的蓝图类,为其添加Utility下的箭头组件并设置为根,并设置颜色(UE中用红绿蓝表示XYZ轴,箭头默认的红色与X轴相同容易混乱),去掉“渲染”属性中的“游戏中隐藏”。

然后,在事件图表中从“开始运行”节点添加循环计时器,并添加名为OnTimerElapsed的自定义事件,设置该事件为朝箭头方向发射魔法粒子;同时,在Tick节点后实现在每一帧将箭头指向玩家,方向向量由两个Actor的坐标相减得到

运行关卡,随着箭头每2秒对玩家进行攻击

3. 血量UI

现在才正式进入UMG的使用,我们将通过UMG制作角色的血量条来实时显示角色的血量。

首先在Content文件夹下创建UI文件夹,用于存放所有UI相关的资产。在UI文件夹中创建“用户界面” -> “控件蓝图”,命名为PlayerHealth_Widget。双击打开控件,就可以看到UMG的操作界面

在左侧“控制板”中的“通用”分别添加一个进度条和文本,在“层级”中选择任意组件,右键添加水平框,并将另外一个拖入其中,这样不需要手动调整就可以使这两个元素在水平保持对齐。

但此时UI的内容还是固定的,UI需要和数据进行绑定后,才能实时根据数据进行显示

选择文本块,点击 “内容” -> 文本-> 绑定 -> 创建绑定,UE会在蓝图中创建相应函数,我们将其重命名为GetHealthText。

可以看到数字能够实时显示血量

UMG-优化血量条

1. 优化执行效率

上一节虽然实现了UI的实时更新,但绑定函数会每帧刷新进而造成资源浪费,毕竟血量变化的速度和一帧比起来还是慢太多。类似于CPU控制的IO操作会导致CPU持续忙等(一直检查状态但没有实际作用),一种解决办法就是使用中断控制来解放CPU。类似的,在设计模式中有一种订阅-发布(观察者)模式就很适合这样的场景:UI组件不再自行每帧查询血量,而是让血量自己在发生变化的同时通知UI进行刷新。

在UE中可使用多播委托自定义事件来实现,它的作用主要是只需调用一次Broadcast函数就可使所有绑定的对象触发相应功能。在上一节转到OnComponentBeginOverlap的定义时,我们已经看到事件在UE中的代码结构,这里我们将使用类似的方法来实现一个自定义事件。

//SAttributeComponent.h

#pragma once

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "SAttributeComponent.generated.h"

DECLARE_DYNAMIC_MULTICAST_DELEGATE_FourParams(FOnHealthChanged,AActor*,InstigatorActor,USAttributeComponent*,OwningComp,float,NewHealth,float,Delta);


UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class ACTIONROGUELIKE_API USAttributeComponent : public UActorComponent
{
	GENERATED_BODY()

public:	
	// Sets default values for this component's properties
	USAttributeComponent();

protected:

	UPROPERTY(EditDefaultsOnly,BlueprintReadOnly,Category="Attributes")
	float Health;



public:
	
	UFUNCTION(BlueprintCallable)
	bool IsAlive() const;


	
	
	UPROPERTY(BlueprintAssignable)
	FOnHealthChanged OnHealthChanged;
	
	UFUNCTION(BlueprintCallable,Category="Attributes")
	bool ApplyHealthChange(float Delta);
	

};

//SAttributeComponent.cpp


#include "SAttributeComponent.h"

// Sets default values for this component's properties
USAttributeComponent::USAttributeComponent()
{
	// Set this component to be initialized when the game starts, and to be ticked every frame.  You can turn these features
	// off to improve performance if you don't need them.
	PrimaryComponentTick.bCanEverTick = true;

	// ... 
	Health = 100;
}

bool USAttributeComponent::IsAlive() const
{
	return Health>0.0f;
}


bool USAttributeComponent::ApplyHealthChange(float Delta)
{
	Health += Delta;

	OnHealthChanged.Broadcast(nullptr,this,Health,Delta);
	return true;
}

我们直接在PlayerHealth_Widget的事件图表中编写蓝图程序,在“事件构造”一开始就将将玩家Pawn的AttributeComp绑定到OnHealthChanged事件上。在每次事件触发时计算当前血量与玩家默认血量的百分比,并设置给进度条。

2. 简易脉冲动画

这部分通过在UI中添加血量减少时的脉冲动画,来了解UMG中动画的使用。

在左下角的“动画”中点击“添加动画”,命名为PulseHealthAnim,点击动画,并选择旁边的“时间轴”。选择显示血量数字的文本块,在细节中下滑找到“渲染变换”,将缩放添加为关键帧。

十字准星

1. 创建准星UI

结合之前文章关于UMG的内容,我们可以十分快速地创建一个之子准星的UI。

首先,我们需要调整一下摄像机的位置。如果我们现在运行关卡,会发现游戏角色位于镜头的正中间,这无疑会在操作中遮挡玩家的实现。回忆一下各种第三人称视角的游戏,人物通常位于画面的偏左或偏右的位置。

进入Player的蓝图编辑器,选择弹簧臂组件(SpringArmComp),调整其中的“摄像机”属性。通过设置“长度”可以变化摄像机与角色的距离,设置“插槽偏移”从而在改变相机位置时保持弹簧地碰撞检测的功能。这里大家可以根据自己的喜好自行调节视角,我在这个地方的设置如图

//2024.10.11
target arm length修改为 300
socket offset 为 0 90 0

接下来,在UI文件夹下创建Crosshair_Widget控件蓝图,并添加一个图像控件。通常,十字准星固定在屏幕的正中央。因此,我们设置图像的锚点为屏幕中间点,设置位置X、Y为0,并把X与Y的对齐均设置为0.5。适当调节尺寸X、Y属性,使准星的大小合适。

回到Player蓝图中,在之前添加血量条的位置再把准星加上,同样添加到视口。然后运行关卡,就可以看到屏幕正中间有一个简单的准星了。

2. 调整发射代码

在此前文章中,我在SCharacter中的PrimaryAttack使用了GetActorRotation函数来获取角色的旋转,从而使粒子以角色的正前方发射

在添加十字准星后,我们肯定需要粒子沿着准星发射,即沿着玩家的视角发射。所以,我们只用把PrimaryAttack_TimeElapsed函数中的GetActorRotation()换成GetControlRotation()即可。此外,在当前项目下,我使用UE_LOG发现GetControlRotation()和GetViewRotation()的值相等,所以后面就统一用前者。

但这样的更改可能会带来两个问题:一是反方向(正脸面向摄像头)发射时可能会打中角色自己,这取决于魔法粒子蓝图的设置;二是击中的位置与准星还是存在偏差,尤其在角色朝向左边的时候(角色右手发射):

第一个问题很容易理解,反方向发射的魔法粒子检测到了自己角色的actor,就触发了OnActorOverlap事件。我们已经使用了Instigator判断是不是玩家自己,所以第一个问题并没有遇到。

但此处还要注意,粒子虽然可以穿过角色,但仍会对角色自己造成伤害。我们控制粒子销毁的功能是在蓝图中实现的,而控制伤害的功能是在代码中实现的(有点混乱,但这是课程出于教学目的的设计)

所以我们还需要再C++中添加忽略对玩家造成伤害的代码。代码如下所示,其实就是在之前的基础上在第一个if处添加了对Instigator的判断,和蓝图中的逻辑是一样的。

void ASMagicProjectile::OnActorOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
	UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	if(OtherActor && OtherActor != GetInstigator())
	{
		USAttributeComponent* AttributeComp = Cast<USAttributeComponent>(OtherActor->GetComponentByClass(USAttributeComponent::StaticClass()));
		if(AttributeComp)
		{
			AttributeComp->ApplyHealthChange(-DamageAmount);

			Destroy();
		}
	}
}

第二个问题就稍微复杂一些,我个人是这样理解的:粒子发射的方向是角色的相机方向,而粒子发射的位置是角色右手。也就是说,只要发射位置不在屏幕正中心(也就是相机的位置),最后粒子的落点一定存在偏移,且距离屏幕中间越远偏移越大。

一种最简单的解决办法是,直接设置魔法粒子的发射点在屏幕正中间,也就是相机位置。这个方法只修改需要一行代码,也就是把相机的Rotation和Location都赋给SpawnTM:

FTransform SpawnTM = FTransform(GetControlRotation(), CameraComp->GetComponentLocation());

毫无疑问实现了指哪打哪。如果要使用这个方法,也许在每次攻击时把角色旋转到朝前,然后要精调角色在镜头中的位置,以及魔法粒子沿着GetControlRotation这个方向的具体生成位置,使其攻击时手部刚好和发射粒子的位置重合

另一种很巧妙的方法,是先检测再发射。在发射魔法粒子前,先用射线检测的方法,检测一个从相机位置沿着其朝向的较大的射程范围内,有没有命中对象。如果检测到命中,返回命中的位置,并利用向量加法得到这次攻击的方向向量,然后再发射魔法粒子;如果不命中,最后就会落到沿着相机,距离为射程的那个点上。这种方法每次攻击的方向向量是不相同的,是判定完位置后再计算的。

实现方法和之前第7节打开箱子的射线检测很类似,这里使用形状检测来增加检测的空间,核心代码如下:

void ASCharacter::SpawnProjectile(TSubclassOf<AActor> ClassToSpawn)
{
	if(ensure(ClassToSpawn))
	{
		// 获取模型右手位置
		FVector RightHandLoc = GetMesh()->GetSocketLocation("Muzzle_01");
 	
		// 检测距离为 5000 cm = 50 m
		FVector TraceStart = CameraComp->GetComponentLocation();
		FVector TraceEnd = TraceStart + ( GetControlRotation().Vector() * 5000 );
 	
		// 检测半径
		FCollisionShape Shape;
		Shape.SetSphere(20.0f);
 	
		// 不要检测自己角色
		FCollisionQueryParams Params;
		Params.AddIgnoredActor(this);
 	
		// 碰撞设置
		FCollisionObjectQueryParams ObjParams;
		ObjParams.AddObjectTypesToQuery(ECC_WorldStatic);
		ObjParams.AddObjectTypesToQuery(ECC_WorldDynamic);
		ObjParams.AddObjectTypesToQuery(ECC_Pawn);
 	
		FHitResult Hit;
		if (GetWorld()->SweepSingleByObjectType(Hit, TraceStart, TraceEnd, FQuat::Identity, ObjParams, Shape, Params)) {
			TraceEnd = Hit.ImpactPoint;
		}
			
		// 尾向量 - 头向量 = 方向向量 eg:起点(0,0) 终点(1,1),方向向量为(1,1)
		FRotator ProjRotation = FRotationMatrix::MakeFromX(TraceEnd - RightHandLoc).Rotator();
		// 朝向检测到的落点方向,在角色的右手位置生成
		FTransform SpawnTM = FTransform(ProjRotation, RightHandLoc);
 	
		// 此处设置碰撞检测规则为:即使碰撞也总是生成
		FActorSpawnParameters SpawnParams;
		SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
		SpawnParams.Instigator = this;

		/*
		GetWorld()->SpawnActor<AActor>(ProjectileClass, SpawnTM, SpawnParams);
		*/

		GetWorld()->SpawnActor<AActor>(ClassToSpawn,SpawnTM,SpawnParams);
	}
	
}

伤害数值

1. 创建UI

要显示数字,需要利用UMG来实现。创建UI的部分十分简单,在Ui文件夹下创建一个新的DamagePopup_Widget控件蓝图,只添加一个文本框即可。设置文本框的位置和默认值如下,需要注意设置文本框“Is Variable”便于后续访问。

伤害数值会显示在魔法粒子Overlap且有血量(即有AttributeComp)的对象上,因此我们需要一个变量来记录这个对象。在“图表”中创建一个变量AttachTo,变量类型设置为Actor,然后勾选其“可编辑实例”和“生成时公开”,从而暴露该变量方便我们在其他蓝图传递这个变量的值。随后创建如下蓝图:

实现了数字UI的显示,其中的关键点在于利用“将场景投射到屏幕(Project World To Screen)”节点实现3维的游戏内世界坐标对应2维屏幕坐标的转换,并通过“获取视口缩放”来得到屏幕的缩放比例,从而矫正UI显示的位置。

同时,我们希望数值UI在显示一定时间后自动消失:

2. 调用UI

回顾之前的内容,在SAttributeComponent类中声明了OnHealthChanged事件,即所有带有血量属性的对象都可以触发这个事件,而这些带有血量的对象刚好需要显示伤害数值。因此,我们可以利用这个事件来创建显示UI。

打开TargetDummyBP蓝图,进行控件的创建。相关方法已在UMG提过,故不再赘述。唯一需要注意到,之前将AttachTo暴露后,可以在此处传入Attach的对象。

此时,按照课程中的进度应该已经实现了显示数字的效果,但我在运行时却没有反应。

之前跟着课程实现的逻辑十分混乱:魔法粒子造成伤害在代码中实现,使用Overlap事件,且包含了Destroy语句;魔法粒子爆炸效果在蓝图中实现,使用了OnActorHit事件。这将导致魔法粒子在碰撞到Gideon角色后只扣血,而不能播放爆炸效果。因此我在MagicProjectile中添加(不是替换)了Overlap事件来触发,解决了一个历史遗留bug。

创建最大生命值(HealthMax)

在游戏后期我们如果需要频繁的修改血量的值,就需要去设置最大生命值,避免血量超出我们设置的值

//SAttributeComponent.h 
//新添加下列内容
protected:
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Attributes")
	float HealthMax;

public:
UFUNCTION(BlueprintCallable)
	bool IsFullHealth() const;

	UFUNCTION(BlueprintCallable)
	float GetHealthMax() const;

//SAttributeComponent.cpp
USAttributeComponent::USAttributeComponent()
{
	HealthMax = 120;
	Health = HealthMax;
}


bool USAttributeComponent::IsFullHealth() const
{
	return Health == HealthMax;
}

float USAttributeComponent::GetHealthMax() const
{
	return HealthMax;
}

bool USAttributeComponent::ApplyHealthChange(float Delta)
{

	float OldHealth = Health;

	Health = FMath::Clamp(Health+Delta,0.0f,HealthMax);

	float ActualDelta = Health - OldHealth;
	OnHealthChanged.Broadcast(nullptr,this,Health,ActualDelta);
	return ActualDelta != 0;
}

在PlayerHealth_Widget中如下修改

在PlayCharacter蓝图中修改

为Bots添加生命UI

创建新类SWorldUserWidget,基类是UserWidget

//SWorldUserWidget.h
// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "SWorldUserWidget.generated.h"

class USizeBox;
/**
 * 
 */
UCLASS()
class ACTIONROGUELIKE_API USWorldUserWidget : public UUserWidget
{
	GENERATED_BODY()

private:
	virtual void NativeTick(const FGeometry& MyGeometry, float InDeltaTime) override;


protected:

	UPROPERTY(meta = (BindWidget))
	USizeBox* ParentSizeBox;

public:
	UPROPERTY(BlueprintReadOnly,Category="UI")
	AActor* AttachedActor;
    
    UPROPERTY(BlueprintReadOnly,Category="UI")
	FVector WorldOffset;
};

//SWorldUserWidget.cpp

#include "SWorldUserWidget.h"

#include "Blueprint/WidgetLayoutLibrary.h"
#include "Components/SizeBox.h"
#include "Kismet/GameplayStatics.h"

void USWorldUserWidget::NativeTick(const FGeometry& MyGeometry, float InDeltaTime)
{
	Super::NativeTick(MyGeometry, InDeltaTime);

    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);
		}
	}
}


//SAICharacter.cpp
void ASAICharacter::OnHealthChanged(AActor* InstigatorActor, USAttributeComponent* OwningComp, float NewHealth,
	float Delta)
{
	if(Delta <0.0f)
	{
		if(InstigatorActor != this)
		{
			SetTargetActor(InstigatorActor);
		}
		
        //新
		if(ActiveHealthBar == nullptr)
		{
			ActiveHealthBar = CreateWidget<USWorldUserWidget>(GetWorld(),HealthBarWidgetClass);
			if(ActiveHealthBar)
			{
                ActiveHealthBar->AttachedActor = this;
				ActiveHealthBar->AddToViewport();
			}
		}
		
		GetMesh()->SetScalarParameterValueOnMaterials(TimeToHitParamName, GetWorld()->TimeSeconds);

		if(NewHealth <= 0.0f)
		{
			//stop BT
			AAIController* AIC =  Cast<AAIController>(GetController());
			if(AIC )
			{
				AIC->GetBrainComponent()->StopLogic("Killed");
			}

			//ragdoll
			GetMesh()->SetAllBodiesSimulatePhysics(true);
			GetMesh()->SetCollisionProfileName("Ragdoll");
			

			//set lifespan
			SetLifeSpan(10.0f);
		}
	}
}



//SAICharacter.h
protected:
	UPROPERTY(EditDefaultsOnly,Category="UI")
	TSubclassOf<UUserWidget> HealthBarWidgetClass;

以SWorldUserWidget为模板创建新的UI

弹出警告,ParentSizeBox未被发现

添加SizeBox并改名为ParentSizeBox

如图下一样去修改

在MinionHealth_Widget中去修改

修改MinionRangedBP中的UI,这时候我们bots时会产生血条,但是这时的血条不会判断是否为空

所以当bot在死亡一段时间消失后,就会产生崩溃,因为ue的垃圾内存回收系统,每隔60秒回收一次

if(!IsValid(AttachedActor))
	{
		RemoveFromParent();
		
		return;
	}
添加上面代码后解决

调整对齐度

修改蓝图

合并HUD Widgets

创建新的Widget蓝图

替换掉原来PlayerCharacter的Class

点数UI与时间计数UI

创建Credits_Widget蓝图

创建GameModeBaseInfo_Widget蓝图

为TEXT Binding新的函数,GetGameTimeText

在MainHUD中添加我们新创建的Credits和GameModeBaseInfo

主菜单及暂停菜单

在新的文件夹中创建新的蓝图

添加按钮和文本

复制一个一样的button

为button专门创建一个蓝图

引入到主菜单中

为我们的按钮蓝图创建变量

设置相关蓝图

建立相当于 C++中的BroadCast

在我们的蓝图中创建点击button的事件

设置退出游戏

在我们的菜单按钮中设置所有的按钮

添加textbox

设置蓝图

创建新的蓝图

设置蓝图

我们创建新的地图,设置这个世界的gamemode,现在我们的菜单就搞定了

其实有一点没有完成,那就是

设置蓝图

为我们的暂停菜单创建蓝图

创建新的类

//SPlayerController.h
// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "SPlayerController.generated.h"

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnPawnChanged, APawn*, NewPawn);

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnPlayerStateChanged, APlayerState*, NewPlayerState);
/**
 * 
 */
UCLASS()
class ACTIONROGUELIKE_API ASPlayerController : public APlayerController
{
	GENERATED_BODY()


protected:
	UPROPERTY(EditDefaultsOnly, Category = "UI")
	TSubclassOf<UUserWidget> PauseMenuClass;

	UPROPERTY()
	UUserWidget* PauseMenuInstance;

	UPROPERTY(BlueprintAssignable)
	FOnPawnChanged OnPawnChanged;

	// Listen for incoming player state (for clients this may be nullptr when initially joining a game, 
	// afterwards player state will not change again as PlayerControllers maintain the same player state throughout the level)
	UPROPERTY(BlueprintAssignable)
	FOnPlayerStateChanged OnPlayerStateReceived;

	UFUNCTION(BlueprintCallable)
	void TogglePauseMenu();

	void SetupInputComponent() override;

	virtual void SetPawn(APawn* InPawn) override;

	virtual void BeginPlayingState() override;

	UFUNCTION(BlueprintImplementableEvent)
	void BlueprintBeginPlayingState();

	void OnRep_PlayerState() override;
	
};


//SPlayerController.cpp
// Fill out your copyright notice in the Description page of Project Settings.


#include "SPlayerController.h"

#include "Blueprint/UserWidget.h"

void ASPlayerController::TogglePauseMenu()
{
	if (PauseMenuInstance && PauseMenuInstance->IsInViewport())
	{
		PauseMenuInstance->RemoveFromParent();
		PauseMenuInstance = nullptr;

		bShowMouseCursor = false;
		SetInputMode(FInputModeGameOnly());
		return;
	}

	PauseMenuInstance = CreateWidget<UUserWidget>(this, PauseMenuClass);
	if (PauseMenuInstance)
	{
		PauseMenuInstance->AddToViewport(100);

		bShowMouseCursor = true;
		SetInputMode(FInputModeUIOnly());
	}
}


void ASPlayerController::SetupInputComponent()
{
	Super::SetupInputComponent();

	InputComponent->BindAction("PauseMenu", IE_Pressed, this, &ASPlayerController::TogglePauseMenu);
}

void ASPlayerController::SetPawn(APawn* InPawn)
{
	Super::SetPawn(InPawn);
}

void ASPlayerController::BeginPlayingState()
{
	BlueprintBeginPlayingState();
}


void ASPlayerController::OnRep_PlayerState()
{
	Super::OnRep_PlayerState();

	OnPlayerStateReceived.Broadcast(PlayerState);
}

设置暂停菜单键

修改我们PlayControllerBP的父类

现在我们可以实现我们按下esc弹出暂停菜单了

效果UI

创建我们的效果UI

将效果UI合在一起

将效果UI和我们的血量条绑定在一起

//SActionComponent.h
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnActionStateChanged, USActionComponent*, OwningComp, USAction*, Action);

public:
	UPROPERTY(BlueprintAssignable)
	FOnActionStateChanged OnActionStarted;

	UPROPERTY(BlueprintAssignable)
	FOnActionStateChanged OnActionStopped;

protected:
	UPROPERTY(BlueprintReadOnly)
	TArray<USAction*> Actions;


//SAction.h
protected:
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI")
	UTexture2D* Icon;

public:
	UPROPERTY()
	float TimeStarted;

//SAction.cpp
void USAction::StartAction_Implementation(AActor* Instigator)
{
	if (GetOwningComponent()->GetOwnerRole() == ROLE_Authority)
	{
		TimeStarted = GetWorld()->TimeSeconds;
	}

	GetOwningComponent()->OnActionStarted.Broadcast(GetOwningComponent(), this);
	
}

void USAction::StopAction_Implementation(AActor* Instigator)
{
	GetOwningComponent()->OnActionStopped.Broadcast(GetOwningComponent(), this);
}


//SActionEffect.h
public:
	UFUNCTION(BlueprintCallable, Category = "Action")
	float GetTimeRemaining() const;

//SActionEffect.cpp

float USActionEffect::GetTimeRemaining() const
{
	AGameStateBase* GS = GetWorld()->GetGameState<AGameStateBase>();
	if (GS)
	{
		float EndTime = TimeStarted + Duration;
		return EndTime - GS->GetServerWorldTimeSeconds();
	}

	return Duration;
}

为我们的燃烧效果分配一个UI

保证我们effect是有效的

使得buff效果消失后图标也消失(我这里没有消失)

为我们的Icon设置一个材质

修改我们的Slot

根据持续时间,修改buff图标显示,但坏消息是由于之前的问题,我们的图标还是不会消失

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇