TomLooman_ActionRoguelike_第十四章带有C++和更多框架扩展的UMG
时间:2023-08-19 03:03:16来源:哔哩哔哩

该专栏用于保存对TomLooman的ActionRoguelike项目的学习笔记,学习过程中的思考与记录不一定准确。

教程参考:/tomlooman/ActionRoguelike


(资料图片)

基于的项目实现:/CarolBaggins2023/TomLooman_ActionRoguelike_Tutorial

2023_08_13

带有C++和更多框架扩展的UMG:bot的血条,更多Heads-up Display(HUD),玩家角色的Spawn,控制台命令

之前我们在蓝图中实现了角色的血条显示,现在我们尝试结合C++中实现bot受击后,在bot身边显示血条的功能。

因为这类附着在某个对象上的UI,在很多类似的地方可以复用,比如可以用在宝箱或者血包上,所以我们对这种Widget创建一个基类,继承自UserWidget。UserWidget是一个可以被我们扩展的Widget类。

我们创建的继承自UserWidger的Widget类如下,

bot受击产生UI后,这个UI应该一直跟着bot移动,所以我们需要覆盖Tick函数,在其中确定UI的位置。在Widget中,承担Tick功能的是NativeTick函数。

因为Widget的Tick函数不只确定UI的位置,还要执行其它操作,所以要先调用父类函数。

我们知道UI在世界中的位置就是bot的Location,但是不知道世界中的位置与屏幕上的位置的转换。这时就要调用UGameplayStatics::ProjectWorldToScreen函数确定UI在屏幕上的位置,其函数原型是bool UGameplayStatics::ProjectWorldToScreen(APlayerController const* Player, const FVector& WorldPosition, FVector2D& ScreenPosition, bool bPlayerViewportRelative)需要我们传入玩家的Controller和UI在世界中的位置,并用一个二维向量的变量接收结果(UI在屏幕上的位置)。这里的AttachedActor是该类的成员变量,该变量在生成widget时被赋值。WorldOffset也是该类的成员变量,用来微调UI的位置。

但是ProjectWorldToScreen并不考虑UMG的DPI,所以这样可能会让最终生成位置产生偏移。所以我们要根据显示窗口的DPI对UI显示在屏幕上的位置进行调整。通过UWidgetLayoutLibrary::GetViewportScale可以获得目前的DPI。

目前我们只确定了UI的位置,还要实际地渲染UI,所以我们调用SetRenderTranslation并传入上面获得的位置。然而我们原本并没有一个实际地Widget来进行显示,所以我们创建了一个SizeBox类型的成员变量,这个Widget之后会作为实际UI的父组件。

注意,这里的说明符meta = (BindWidget)表示这个成员变量会在Widget的设计图中被绑定,绑定的对象是设计图中与它类型和名称相同的那个组件。

创建了上面的Widget基类后,我们可以在编辑器中创建它的蓝图子类,用来表示bot的血条。

因为我们在这个基类中有一个需要在Widget设计图中绑定的SizeBox成员变量,所以刚创建完蓝图类后会出现报错,提示SizeBox成员变量未绑定,此时我们就要创建一个SizeBox,并使它的名字与那个成员变量相同。

然后和玩家角色血条类似,我们用一个image表示血条,并使用之前用过的血条材料(该材料中的ProgressAlpha参数控制血条变化)。

现在bot受击时还没有真正地生成这个Widget,所以我们要对bot的OnHealthChanged成员函数进行补充。

我们不想重复生成UI,所以在bot类中声明了一个之前定义的Widget基类的成员变量。

在OnHealthChanged中,我们先判断是否已有UI。之后的逻辑和蓝图很像,用CreateWidget创建一个Widget,创建成功后AddToViewport。

注意在AddToViewport“之前“,我们要对Widget的AttachedActor进行赋值,因为AddToViewport会调用蓝图中的EventConstruct,而我们在EventConstruct的执行流中需要访问bot的属性组件成员,所以必须要知道这个Widget到底Attach在哪个bot上。

CreateWidget第一个参数要求传入OwnerT* OwningObject,这里我们选择最简单的GetWorld()(没弄明白)。第二参数要求传入生成的Widget的类别,类似于玩家角色射出的子弹的类别,这里我们将这个类别作为bot类的成员变量,并在编辑器中进行赋值。

现在我们攻击bot后已经会正确地在bot上显示血条UI了,但是有两个问题,(1)bot死亡被销毁后崩溃,(2)血条不会变化。下面依次解决。

bot死亡被销毁后崩溃的原因是,我们现在Widget的NativeTick中执行了AttachedActor->GetActorLocation(),但并没有检查AttachedActor是否为空,所以一旦bot死亡被销毁,AttachedActor就变成了空指针,再在它上面调用函数就会导致报错。为了解决这个问题,我们在NativeTick中,先对AttachedActor进行空指针判断,如果为空,则将Widget从父组件中移除,相当于删除Widget。

血条不会变化是因为我们还没有对血条材料中的ProgressAlpha进行赋值,而我们要先思考在哪里修改ProgressAlpha。因为血条形态的改变应该在bot属性组件中的Health发生改变时,所以就离不开属性组件类中的委托。所以我们应该将血条材料中的某个函数与拥有该血条的bot的属性组件类中的委托绑定,当委托广播时执行该函数,修改ProgressAlpha。

绑定过程如下,我们通过自定义的Widget基类中的AttachActor获得拥有该血条的bot,并获得该bot的属性组件从,再接着完成自定义函数与委托的绑定。这里我们还在绑定后直接调用了委托,因为bot受伤这一事件发生在绑定之前,所以血条材料无法接受到bot的第一次委托广播,也就是说如果不在绑定后立即执行一次的话,bot第一次受击就不会引起血条变化。

与委托绑定的事件与玩家角色的血条类似,不同的是,当bot血量小于等于0,也就是死亡时,我们延迟1s删除血条UI。

我们玩家角色的血条、瞄准点等UI都是在蓝图中通过CreateWidget创建的,如果想这样一个个创建,当Widget很多时会冗杂。所以我们想要将这些Widget集成到一个Widget中。

我们创建一个Widget将玩家角色的UI都放进去。我们还新建了两个Text,一个表示玩家积分(现在还没实现),一个表示游戏时间(后面讲)。

在这些原本单独的UI放进一个大的Widget中时,我们要对它们进行一些修改,主要的修改是去除原本的CanvasPanel,可以看到现在血条UI的外面已经没有表示显示器区域的虚线方框了。另外此时UI的显示方式我们可以选择DesiredOnScreen,而不是默认的FullScreen,注意这只影响我们看到的,不影响UI实际放入大Widget后的样子。

在大Widget中,我们将血条和积分竖直排列,这里不要手动对齐,而是要用VerticalBox,也就是它们外层显示的虚线框。另外VerticalBox应设置为SizeToContent,这样它能随里面Widget的大小调整自己的大小,而里面小Widget的大小可能通过它们自己的设计图进行修改,大Widget中的引用也会相应修改。

关于每个UI在屏幕上的位置,我们还可以通过UI的锚点(Anchor)与支点(Pivot)进行对齐(表述不准确,直接看例子)。例如下面的例子,我们想把游戏时间放在右上角,所以我们将UI的锚点(辐射状的图表)改到右上角,PositionX和PositionY是UI的锚点与支点之间的偏移,图中表示支点在锚点的X轴向-50、Y轴向50,Alignment表示选择哪个点作为UI的支点,候选点就是UI周围方框上的八个小白点,左上是(0,0),右下是(1,1),上方中点是(,0)。

在创建完这样一个包含其他Widget的大Widget后,我们在玩家角色的蓝图中就不需要一个个地Create小Widget了,直接Create一个大Widgtet就可以了。

补充一下游戏时间UI的实现方法。

容易想到的是我们创建一个Text组件,然后将该Text与一个函数绑定。因为这个UI是显示游戏时间的,时刻在改变,而不像攻击伤害那样,受伤了才出现,所以可以不使用那种自定义事件的方法。

绑定的函数如下所示,之前我们也涉及过多次获取游戏时间,例如受击发光材料。我们可以通过UGameplayStatics::GetTimeSeconds获得游戏时间,但是这个游戏时间是玩家本地的。当涉及到多人游戏时,这里会发生错误,比如游戏已经开始了10分钟,但是你刚加入游戏,此时如果你用UGameplayStatics::GetTimeSeconds获取游戏时间,返回时间为0,因为游戏刚开始在玩家本地运行。但实际上我们希望获取的是游戏真正运行的时间,这时我们可以使用GetServerWorldTimeSeconds,来获取服务器上的游戏运行时间,而这个函数要从GameState中获得。

另外,我们在游戏运行过程中的任何地方都不应该使用FString,因为FString不会进行本地化,可能会出错,所以这里把FString转成了FText。

我们在最开始的时候通过把玩家角色和bot的蓝图子类拖拽到视口面板中,在世界中生成了玩家角色和bot。现在我们已经在自定义的GameMode中实现了bot的自动生成,所以视口面板中的bot可以删除。那我们的玩家角色删除后会怎么样?我们会进入自由移动的上帝视角,没有可以控制的角色。

我们可以在GameMode中设置游戏开始时玩家控制的Pawn的类型,

当我们在WorldSetting中用自定义的GameMode覆盖了默认的GameMode后,我们在游戏开始后就可以控制一个PlayerCharacter类的Pawn。

但是现在开始游戏后我们依然没有控制一个角色,因为角色不知道生成在哪里。这种情况下,我们可以在世界中放置一个PlayerStart,它将作为玩家角色的生成点。如果有多个PlayerStart,将会随机挑选一个点生成玩家角色。

在游戏中,按下波浪号键可以呼出控制台,我们可以调用控制台中已有的一些函数,也可以自定义控制台命令。我们下面自定义两个控制台命令,玩家回血和杀死所有bot。

我们在ASCharacter中声明并实现玩家回血函数,函数的实现很好理解,重点在于函数宏中的说明符Exec,这表示我们可以在控制台中调用该函数。

另外,这里我们第一次用了函数形参的默认值,UE中的函数形参默认值和C++规则相同。

我们在GameMode中实现杀死所有bot函数,同样要使用说明符Exec。这里我们在属性组件类中添加了Kill函数,其实就相当于受到满生命值的伤害。

我们可以直接在游戏中呼出控制台然后调用这些函数。

UE的CheatManager自带一些控制台函数,例如void UCheatManager::God()

能将Pawn的CanBeDamaged设为false,也就是不受伤害(UE的Character自带一些类似游戏中常见的属性)。

但是调用这个函数并不会使我们的玩家角色不受伤害,因为我们的血量来自于我们自定义的属性组件类,UE不知道。所以为了实现类似的效果,我们在属性组件类的OnHealthChanged中增加一个功能,当Pawn的CanBeDamaged为false时,不继续修改血量成员变量。

最后要注意,说明符Exec能让类的成员函数在控制台中调用,但仅限于以下的类:(1)PlayerController(2)玩家控制的Character(3)GameMode(4)CheatManager这种已经有控制台命令的类。

标签:

最新
  • TomLooman_ActionRoguelike_第十四章带有C++和更多框架扩展的UMG

    该专栏用于保存对TomLooman的ActionRoguelike项目的学习笔记,学习过程

  • 江西“300位特警下周将夜爬武功山拉练”?官方回复:是真的! 具体是怎么回事?

    【江西“300位特警下周将夜爬武功山拉练”?官方回复:是真的!】下面

  • 涉嫌组织卖淫被取保了还有事吗?

    涉嫌组织卖淫罪被抓,取保候审后一定不要掉以轻心,不要以为万事大吉了

  • 一觉醒来,中国金花2连败!1姐+法网新后出局,世界第4退赛!

    辛辛那提赛中国军团表现不佳,男单全军覆没无人入围正赛,女单只有郑钦

  • 减肥食谱一周瘦10斤科学减肥_土豆吃了会胖吗

    1、1、来说一下和我们现在很多女生都很感兴趣有关的问题吧,和减肥有关

  • 这次没问题了吧?天空:齐耶赫已完成加盟加拉塔萨雷的体检

    据天空体育的消息,齐耶赫已经完成了加盟加拉塔萨雷的体检。报道称,齐

  • 大朗镇为企业高质量发展保驾护航

    连日来,大朗镇领导率队到镇内部分企业走访调研,了解企业发展情况,倾

  • 劝学不能十步的前一句与后一句

    作为一个勉励别人学习的百科达人,我认为学习应该贯穿生命的始终,并且

  • 杜甫被称为诗圣吗 杜甫是不是被称为诗圣

    杜甫的诗具有丰富的社会内容、强烈的时代色彩和鲜明的政治倾向,真实深

  • 美亨实业(01897)8月14日斥资25.8万港元回购60万股

    智通财经APP讯,美亨实业(01897)发布公告,于2023年8月14日斥资25 8万

  • 西方石油(OXY.N)将以11亿美元现金收购CARBON ENGINEERING公司。

    西方石油(OXY N)将以11亿美元现金收购CARBONENGINEERING公司

  • 申请加拿大研究生留学需要具备哪些条件呢

    《申请加拿大研究生留学需要具备哪些条件呢》由研究生留学网发布,主要

  • 河南你早丨河南省政府新任免一批干部;郑州小学入学开始审核,线下报名请注意

    河南你早丨河南省政府新任免一批干部;郑州小学入学开始审核,线下报名

  • 突发!时隔25天俄罗斯央行紧急加息,为何?

    突发!时隔25天俄罗斯央行紧急加息,为何?面对剧烈振荡的卢布,距

  • 8月15日基金净值:华商智能生活灵活配置混合A最新净值2.161,跌1.41%

    8月15日,华商智能生活灵活配置混合A最新单位净值为2 161元,累计净值

  • 点亮“北方戏窝子” 河北省首届职工戏迷票友大赛开赛

    全省首届职工戏迷票友大赛开赛河北日报讯(记者方素菊)从省总工会获悉

  • 旅游
    • 为人民群众生命安全筑防线——天津防汛泄洪现场直击

    • 山东曹县:非遗复兴令“朽木可雕”

    • 社会科学期刊是c刊吗 社会科学期刊

    • 农业农村部强化技术指导保障秋粮生产