欢迎光临散文网 会员登陆 & 注册

斯坦福UE4C++课程P55-P58带有C++和更多框架扩展的UMG

2022-11-08 21:55 作者:_Hide-on-bush_  | 我要投稿

这一节首先我们写一个控件基类,给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控制台命令,我们就无敌了(不掉血不加血)。


斯坦福UE4C++课程P55-P58带有C++和更多框架扩展的UMG的评论 (共 条)

分享到微博请遵守国家法律