首页 > 编程语言 >斯坦福 UE4 C++ ActionRoguelike游戏实例教程 09.第二个游戏规则:玩家重生

斯坦福 UE4 C++ ActionRoguelike游戏实例教程 09.第二个游戏规则:玩家重生

时间:2023-04-09 22:34:41浏览次数:48  
标签:控制器 血条 OnActorKilled 角色 09 C++ 玩家 实例教程 重生

斯坦福课程 UE4 C++ ActionRoguelike游戏实例教程 0.绪论

概述

本文对应课程15章,60 - Refining Player Respawns。

在本篇文章中,将会为游戏新增加一个规则,即玩家可以自动产卵,呸,自动重生。

设定玩家重生

在之前的课程中,我们使用GameMode为游戏添加了第一个规则,即自动生成AI小兵。在本节课中,我们将为游戏添加第二个规则,让我们的角色在被打死后能够自动复活从而继续进行游戏。

要实现这个功能也非常简单,主要思路如下:当玩家受到伤害且血量归零时,调用我们在GameMode里定义的OnActorKilled函数,执行玩家死亡后的逻辑。我们想让玩家死亡后在一定时间后重生,因此我们需要设置一个重生时间和一个重生的定时器,并且在OnActorKilled函数中启用定时器,在重生时间结束后调用重生相关的函数,这里我将其定义为RespawnPlayerElapsed(AController* Controller)。函数声明如下:

//SurGameModeBase.h
protect:
   UFUNCTION()
   void RespawnPlayerElapsed(AController* Controller);

   UFUNCTION(BlueprintCallable)
   virtual void OnActorKilled(AActor* VictimActor, AActor* Killer);

   UPROPERTY(EditDefaultsOnly)
   float RespawnDelay;

public:
   ASurGameModeBase();

下面是OnActorKilled函数的定义,我们将在角色血量归零的时候调用这个函数。主要的工作就是启用了一个定时器,在重生时间结束后调用RespawnPlayerElapsed

值得一提的是,由于RespawnPlayerElapsed函数是带有参数的,所以我们不能像之前一样直接将函数名作为定时器的参数传进去,而是要定义一个Delegate,将函数名和参数绑定在一起。熟悉C++11的读者应该也见过类似的东西,没错,就是std::bind 函数.

void ASurGameModeBase::OnActorKilled(AActor* VictimActor, AActor* Killer)
{
   AMyCharacter* Player = Cast<AMyCharacter>(VictimActor);
   if(Player)
   {
      //没有必要持有Handle,且为了防止多人游戏中handle相互覆盖,这里做成局部变量
      FTimerHandle TimerHandle_RespawnDelay;
      //Delegate用于需要传参的情况,类比于C++11的Bind函数
      FTimerDelegate Delegate;
      Delegate.BindUFunction(this, "RespawnPlayerElapsed", Player->GetController());
      
      GetWorldTimerManager().SetTimer(TimerHandle_RespawnDelay, Delegate, RespawnDelay, false);
   }
   UE_LOG(LogTemp, Log, TEXT("OnActorKilled:Victim:%s, Killer: %s"), *GetNameSafe(VictimActor), *GetNameSafe(Killer));
}

接下来是RespawnPlayerElapsed函数。我们想让控制死亡角色的控制器重新获得一个新的角色,并且控制器的生命周期往往长于角色的生命周期,因此这里需要传入控制器的指针,释放它所控制的角色,并重新生成一个。

//之所以传入Controller,是因为我们不能保证玩家角色是否在计时结束后已经被销毁
void ASurGameModeBase::RespawnPlayerElapsed(AController* Controller)
{
   if(ensure(Controller))
   {
      //作用之一就是将pawn成员设为null
      Controller->UnPossess();
      //如果控制器拥有一个Pawn,则获取pawn的旋转作为控制器的新旋转
      //如果控制器不拥有,则选择一个出生点,新生成一个pawn
      RestartPlayer(Controller);
   }
}

至于如何调用OnActorKilled函数,下面给出了使用的案例,并且对USurAttributeComponent::ApplyHealthChange作出了较多的修改,供读者参考。

当角色的血量归零时,就会获取当前游戏的GameMode,并调用GameMode类定义的OnActorKilled函数,将攻击双方的指针作为参数传进去。

值得一提的是,所有的带有USurAttributeComponent的Actor都有可能执行OnActorKilled,但是并非所有Actor都是玩家角色,因此才需要在OnActorKilled函数里对Actor进行类型转换并进行判断,随手判空绝对是一个好习惯。

bool USurAttributeComponent::ApplyHealthChanges(AActor* InstigatorActor, float Delta)
{
   if(!GetOwner()->CanBeDamaged() && Delta < 0.f)
   {
      return false;
   }
   float OldHealth = Health;
   Health = FMath::Clamp(Health + Delta, 0.f, MaxHealth);
   float ActualDelta = Health - OldHealth;

   OnHealthChanged.Broadcast(InstigatorActor, this, Health, ActualDelta);

   if(ActualDelta < 0.f && Health == 0.f)
   {
      ASurGameModeBase* GM = Cast<ASurGameModeBase>(GetWorld()->GetAuthGameMode());
      if(GM)
      {
         GM->OnActorKilled(this->GetOwner(), InstigatorActor);
      }
      
   }
   return true;
}

UMG的BUG

在进入游戏测试时,发现玩家是可以顺利重生的,但是左上角的血条出现了BUG。

image-20230409175058669

玩家重生BUG(请忽略光照重建问题)

出现了以下几点问题:

  1. 血条自动回满,并且数字变成了文本的默认值(100),但是玩家角色的血量并不是100(这里我将玩家血量设置为3);
  2. 在玩家受到攻击掉血后,上方的血条(红条)不会产生任何变化;
  3. 由于玩家反复重生,就会反复创建UMG,因此我们看到的UMG是一层一层叠在一起的。

课程里讲解这个BUG省略了很多细节(可以说什么都没讲明白),在笔者反复Debug之后,大概有了些许眉目。现在让我们简单分析一下。

image-20230409180110162

血条的构造函数

回到血条的构造函数图表中去。逐步调试发现,在玩家进行重生时,构造函数图表的获取玩家Pawn返回了空值,这导致了后面所有逻辑全部不被执行,这也是导致了血条不发生变化的直接原因,毕竟没有绑定事件,玩家受到攻击后血条自然不会发生任何变化。

为什么获取玩家Pawn会返回空值呢?这涉及到程序执行顺序的问题。当上一个玩家角色死亡的时候,控制器会将当前控制的角色释放掉,然后新建一个玩家角色,最后才将这个玩家角色赋给自己。

这就出现了一个问题。玩家角色的构造函数设置了创建UMG,玩家角色被创建的时候,血条也就在这时候被创建了。血条创建的时候,想要通过控制器获取玩家的Pawn,但是这时候新的玩家角色此时并没有被赋值到控制器中,因此就会返回空值。

讲完啦,总结一下执行的顺序:

  1. 上一个玩家角色死亡
  2. 控制器释放玩家角色(将自己的pawn成员赋值为nullptr)
  3. 控制器创建新的玩家角色
  4. 角色在构造中创建UMG,创建血条
  5. 血条的构造函数中想通过控制器获取玩家Pawn,但此时控制器的Pawn为nullptr,返回空值,后面的逻辑全部不执行
  6. 血条构造完毕,玩家角色构造完毕
  7. 控制器将pawn成员赋值为新创建的玩家角色

以上就是这个BUG产生的根本原因,目前课程里并没有提到解决方法,但既然我们知道了BUG产生的根本原因,那么我们就可以以此指定修复bug的策略,例如修改血条获取玩家角色的方法,添加带参数的构造函数,或者是玩家角色主动获取UMG进行赋值等。方法交由读者思考,因为修改BUG涉及到要修改的地方比较多,为了尽量不偏离课程,这里就不自作聪明了。

至少,我们可以解决前面提到的第三个BUG。

新建玩家控制器类

由于玩家控制器类的生命周期通常都是远远长于玩家的(一些游戏中甚至是一直存在的),因此, 相较于让玩家角色创建,让用户UI由玩家控制器来创建UMG往往是一个更好的选择。这里,我新创建了一个PlayerController蓝图类,在事件开始运行时创建Main HUD空间,并删掉了玩家角色中创建控件的蓝图节点。这样,只要玩家控制器不发生改变,那么HUD控件自始至终都只会创建一次了。

image-20230409193954803

将玩家角色的创建空间功能移动到玩家控制器上

要启用我们自定义的玩家控制器类,只需要在GameMode中选定即可。

image-20230409194112134

修改GameMode

总结

本篇文章为游戏新增了玩家重生的功能,并且尝试理解和优化了玩家重生所带来的BUG。

值得一提的是,笔者在看教程的时候十分不满意讲师对BUG的讲解,因此自己花费了很多时间去阅读源码和逐步调试,最后定位了BUG产生的原因,在这个过程中也接触到了自己之前从未想过的知识,在此我也希望看到这里的读者能够积极思考,尝试自己解决问题和阅读源码,这将会是非常好的学习方式。

标签:控制器,血条,OnActorKilled,角色,09,C++,玩家,实例教程,重生
From: https://www.cnblogs.com/Qiu-Bai/p/17301308.html

相关文章

  • Python同Java及C++的不同之处
    Python同Java及C++的不同之处1.C++、Java对变量的定义很严格比如inta=0python则直接定义a=02.C++、Java代码结束时需要用;隔开比如inta=0;,而python则不用a=0直接换行即可3.C++、Java中的循环或者判断需要用{}括起来for(i=0;i<5;i++){},python使用:forii......
  • 4_09
    给定一个正整数n,你可以做如下操作:1.如果n是偶数,则用n/2替换n。2.如果n是奇数,则可以用n+1或n-1替换n。3.返回n变为1所需的最小替换次数。publicclassSolution4_03{publicintintegerReplacement(intn){vari=0;while(n!=1){......
  • 每日总结2023-04-09
    今天完成了密码找回界面代码:<?xmlversion="1.0"encoding="utf-8"?><RelativeLayoutxmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools=&qu......
  • 2023第14届蓝桥杯C/C++A组参赛记录+部分题解
    比赛记录早上起得还算早,没吃早餐,我吃早餐会瞌睡,也会变蠢。在门口还没来得及和队里其他同学聊几句就进场了......键盘还是一样的难用,软件有codeblocks和dev,很舒服。今年来参加蓝桥杯的人好多啊......女生也好多。听说今年蓝桥杯有统一的正经培训,不过和我这个被踢出蓝桥杯群的......
  • 【230409-1】记者要为5名志愿者和他们帮助的2位老人拍照,要求排成一排,2位老人相邻但不
    ......
  • c++ constexpr
    c++中,constexpr 让用户显式的声明函数或对象构造函数在编译期会成为常量表达式,这个关键字明确的告诉编译器应该去验证被声明的表达式在编译期就应该是一个常量表达式。声明为constexpr的变量一定是一个const变量,而且必须用常量表达式初始化:constexprintmf=20;//20是常量......
  • 230409 What is a Battery Management System
    WelcometotheStoffelSystemsInsightsvideoseries.I'mEricStoffel,PresidentofStoffelSystems.Inthisseries,we'lldiscussbatterymanagementsystemsasusedinlithium-ionbatterypacks.Let'sbeginwithanintroductiontowhatabat......
  • C++构造函数的调用
    对象特性---构造函数和析构函数构造函数---没有回值也不写void---函数值和类名相同---构造函数可以有参数,因此可以发生重载---程序在调用对象的时候自动调用构造,无须手动调用,只能调用一次析构函数语法---析构函数,没有返回值也不写void---函数名和类名也相同,在名称前加上符号~---......
  • c++Primer 14 重载运算符与类型转换
    除了重载的函数调用运算符operator()之外,其他重载运算符不能含有默认实参。      泛型算法中调用的几元谓词是看函数对象的调用运算符的参数个数。而不是构造函数的参数个数。    转换构造函数只能有一个参数,如果他有多个参数,就无法判断是将哪个参数转......
  • C++数组
    数组C++中的数组是一种特殊类型的变量,它可以存储一组具有相同数据类型的元素,这些元素在内存中是按照一定的顺序排列的。下面是C++数组的一些特点:数组的元素类型必须相同。例如,一个int类型的数组中只能存储int类型的元素。数组的大小是固定的。一旦数组被创建,它的大小就不能再......