首页 > 编程语言 >斯坦福 UE4 C++ ActionRoguelike游戏实例教程 05.认识GameMode&自动生成AI角色

斯坦福 UE4 C++ ActionRoguelike游戏实例教程 05.认识GameMode&自动生成AI角色

时间:2023-03-15 20:58:55浏览次数:61  
标签:曲线 游戏 05 AI GameMode C++ 查询 函数

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

概述

本篇文章将会讲述UE中Gamemode的基本概念,并在C++中开发GameMode,为游戏设置一个简单的玩法:使用环境查询自动生成AI角色,并自定义一条难度曲线,随着时间增大游戏的难度。

最终实现效果,为AI小兵添加了属性组件,可以被我们打爆;编辑GameMode,每隔两秒钟在玩家生成一个AI小兵,有生成上限,上限随着游戏时间增大而增大:

目录

  1. 认识GameMode
  2. 创建GameMode
  3. 创建环境查询、难度曲线
  4. 使自定义GameMode生效

认识GameMode

在之前的所有课程中,我们制作了自己的角色,制作了敌对AI角色,制作了一系列场景物品,这些形形色色的Actor被我们拖动到场景中,组成了一个美好的展览馆。但是我们可以问一下自己,我们做的这些真的可以组成一个游戏吗?到目前为止,我们只是不停地制作一个物品,一个功能,一段交互逻辑,并将他们放置在我们的虚拟世界中,但是他们只是静静地站立在那里,不知道自己存在的意义,不知道自己要到哪里去。

作为这个世界的创世神,是时候为这个世界创造一个规则了,有了规则,才称得上是游戏。本篇文章将会介绍构成整个游戏逻辑的一个重要组件:GameMode。

img

图片来自知乎《InsideUE4》

GameMode类继承自AInfo,作为Actor大家族的一个成员,它就像Actor家族的领袖,指引Actor们如何出生和灭亡。

GameMode定义了一个游戏的玩法,游戏的规则由他指定,正如它的标识为一个旗子一样,你可以用它来规定游戏的玩法是抢夺一个旗子,又或是一个5v5的团队竞技,或者是一个开放世界抽卡游戏。只要它一声令下,就可以宣布游戏开始,如果它愿意,它也可以随时暂停和终止游戏。每一个游戏世界都需要一个GameMode类来管理游戏逻辑。

同样的,它可以指定玩家进入关卡时,默认使用的是哪一个Controller,控制的是哪一个Pawn,加载的是哪一个UI界面。总之,它贯彻了一个关卡的始终。它不依附于场景里的任一个Actor,只要游戏启动了,它就会一直履行它的职责。具体到代码里如何实现,让我们边做边说。

创建GameMode

还是老规矩,右键内容浏览器,创建一个GameModeBase的子类,这里我将它命名为SurGameModeBase。就这样,创世神的第一个得力助手诞生了。

image-20230315113133485

创建GameModeBase的子类

进入代码编辑器,让我们看看GameModeBase支持的操作有哪些。比较常用的方法有InitGameInitGameStateStartPlay等函数,这篇文章并不是API文档,先短暂看一下今天我们要实现什么目标:实现AI角色每隔一段时间在玩家角色周围自动生成,并实现一个难度曲线,使得AI的个数存在一个动态的上限。因此,今天的重点是重写GameModeBase::StartPlay函数,为这个游戏时间建立一个简单的初始法则。

在父类中,StartPlay负责通知所有Actor调用BeginPlay函数,也就是说,只有GameModeBase类一声令下,调用StartPlayer,场景里的Actor才能开始工作,才能拥有自己的心跳(Tick)。而作为子类,我们重写时需要记得调用Super::StartPlay,然后才在后面添加逻辑。

要想实现功能,我们需要为SurGameModeBase添加一系列成员:

  1. 指定生成的AI类型
  2. 生成AI所需要的环境查询
  3. 定义AI小兵生成数量难度曲线
  4. 生成AI的间隔时间
  5. 因为AI是定时生成的,因此需要一个定时器

注意,UE的回调函数都需要使用UFUNCTION宏进行标识。

以下是为了实现功能,对.h文件所进行的修改:

// ASurGameModeBase.h
class FPSPROJECT_API ASurGameModeBase : public AGameModeBase
{
   GENERATED_BODY()
protected:
   UPROPERTY(EditDefaultsOnly, Category = "AI")
   TSubclassOf<AActor> MinionClass;

   //要调用的环境查询
   UPROPERTY(EditDefaultsOnly, Category = "AI")
   UEnvQuery* SpawnBotQuery;

    //难度曲线
   UPROPERTY(EditDefaultsOnly, Category = "AI")
   UCurveFloat* DifficultyCurve;
   
   FTimerHandle TimerHandle_SpawnBots;

   //生成AI的间隔
   UPROPERTY(EditDefaultsOnly, Category = "AI")
   float SpawnTimerInterval;

   //定时器的回调函数
   UFUNCTION()
   void SpawnBotTimerElapsed();

   //查询结束后的回调函数
   UFUNCTION()
   void OnQueryCompleted(UEnvQueryInstanceBlueprintWrapper*  QueryInstance, EEnvQueryStatus::Type QueryStatus);
public:
   ASurGameModeBase();

   virtual void StartPlay() override;
};

由于环境查询非常消耗时间,一帧的时间不足以让其执行完毕,所以UE使用异步的方式执行环境查询。提到异步,就不得不创建一个回调函数传递给环境查询,当环境查询结束后,调用回调函数。这里定义了一个查询结束后的回调函数OnQueryCompleted,回调函数的函数签名可以查看UEnvQueryInstanceBlueprintWrappe的源码或官方文档得知。

由于大部分逻辑都是在查询结束后进行的,因此代码的逻辑部分重点集中在OnQueryCompleted函数中。为了实现目标,列出了以下主要步骤,从StartPlay开始:

  1. 在游戏开始后,开启一个循环执行的定时器,每隔SpawnTimerInterval时间执行一次SpawnBotTimerElapsed。注意SetTimer的最后一个参数为True。
  2. SpawnBotTimerElapsed函数中, 调用UEnvQueryManager::RunEQSQuery。该函数可以执行一次环境查询,并返回一个环境查询的实例即UEnvQueryInstanceBlueprintWrapper对象。由于环境查询可能需要花上好几帧的时间才能结束,因此需要自定义一个回调函数,即OnQueryCompleted,作为参数传进去,在查询结束后调用。
  3. 当环境查询执行完毕后,调用OnQueryCompleted。在该函数里,我们需要判断环境查询的结果,如果为Success则继续。
  4. 使用UE提供的Actor迭代器,遍历所有AICharacter,如果AICharacter拥有属性组件且存活,则计入计数中。
  5. 获取在UE编辑器里定义的难度曲线,以时间为X轴获取数据,即当前允许存在的Bot的最大值。
  6. 如果当前存活AI数小于最大值,则从环境查询的结果中选取一个点,这里默认为第0个坐标,生成一个AI角色。
ASurGameModeBase::ASurGameModeBase()
{
   SpawnTimerInterval = 2.f;
}

void ASurGameModeBase::StartPlay()
{
   Super::StartPlay();
   //循环调用定时器
   GetWorldTimerManager().SetTimer(TimerHandle_SpawnBots, this, &ASurGameModeBase::SpawnBotTimerElapsed, SpawnTimerInterval, true);
}

void ASurGameModeBase::SpawnBotTimerElapsed()
{
   UEnvQueryInstanceBlueprintWrapper* QueryInstance = UEnvQueryManager::RunEQSQuery(this, SpawnBotQuery, this, EEnvQueryRunMode::RandomBest25Pct, nullptr);
   if(ensure(QueryInstance))
   {
      QueryInstance->GetOnQueryFinishedEvent().AddDynamic(this, &ASurGameModeBase::OnQueryCompleted);
   }
}

void ASurGameModeBase::OnQueryCompleted(UEnvQueryInstanceBlueprintWrapper* QueryInstance,
   EEnvQueryStatus::Type QueryStatus)
{
   if(QueryStatus != EEnvQueryStatus::Success)
   {
      UE_LOG(LogTemp, Warning, TEXT("Spawn bot EQS Query Failed!"));
   }
   //NrOf意思为Number Of 外文编程里奇妙的小缩写
   //当前存活的Bot数量
   int32 NrOfAliveBots = 0;

   //遍历所有AI角色,计算存活的Bot数量
   for(TActorIterator<ASurAiCharacter> It(GetWorld()); It; ++It)
   {
      ASurAiCharacter* Bot = *It;

      //判断Bot是否存活。要求Bot拥有属性组件
      USurAttributeComponent* AttributeComp = Cast<USurAttributeComponent>(Bot->GetComponentByClass(USurAttributeComponent::StaticClass()));
      if(AttributeComp && AttributeComp->IsAlive())
      {
         NrOfAliveBots++;
      }
   }

   float MaxBotCount = 10.f;
   if(DifficultyCurve)
   {
      DifficultyCurve->GetFloatValue(GetWorld()->TimeSeconds);
   }

   if(NrOfAliveBots >= MaxBotCount)
   {
      return;
   }
   //从结果中获取一个坐标生成Bot
   TArray<FVector> Locations = QueryInstance->GetResultsAsLocations();
   if(Locations.IsValidIndex(0))
   {
      GetWorld()->SpawnActor<AActor>(MinionClass, Locations[0], FRotator::ZeroRotator);
   }
}

以上就是我们制定的第一个游戏规则,以C++代码的方式记录在我们自定义的Gamemode类中。要想使其生效,我们还需要做一些简单的工作。

创建环境查询、难度曲线

首先为我们刚才创建的C++Gamemode类创建一个蓝图子类,我将其命名为BP_SurGameModeBase。进入蓝图,为SurGameMode里的成员赋值:

image-20230315163943467

设置成员

其中的FindBotSpawn环境查询和难度曲线会在下面简单讲解。

设置AI出生点

这次将AI的出生点简单设置为取所有玩家附近圆环的一点。对于创建环境查询我们已经轻车熟路了,这里就不展开叙述了。

image-20230315122403021

设置环境查询(Query_FindBotSpawn)

image-20230315122429900

设置环境查询内容(QueryContext_AllPlayers)

设置难度曲线

右键内容浏览器,选择其他->曲线即可找到曲线。

image-20230315201503029

创建曲线

进入曲线编辑器,使用alt+enter组合键可以快速创建关键帧,这里将难度曲线设置为如图所示,读者可以自行试验各种曲线的插值方式。其中,曲线的X轴就是我们传入的游戏时间,在Y轴就是Bot的最大数量。

image-20230315162452618

编辑曲线

为AICharacter添加AttributeComponent

方法同添加其他寻常组件一样,注意这里的属性组件AttributeComponent是我们自定义的,要想知道如何创建和使用AttributeComponent,可以参考https://www.bilibili.com/read/cv19014581?spm_id_from=333.999.0.0这篇文章。AttributeComponent类定义了血量属性和血量变化的委托,组装了该组件的Character只需要定义好回调函数,绑定到委托里即可。

回调函数我设置成了当血量降低到0及以下,就销毁这个AI小兵。

//SurAiCharacter.cpp
void ASurAiCharacter::OnHealthChanged(AActor* InstigatorActor, USurAttributeComponent* OwningComp, float NewHealth,
   float Delta)
{
   if(NewHealth <= 0.f && Delta < 0)
   {
      GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Yellow, TEXT("Killed an AI"));
      Destroy();
   }
}

PS:在测试的时候遇到一个非常诡异的BUG,有些在C++里创建的组件在使用时会变成空指针,对应的就是蓝图类组件的细节面板为空,导致程序运行出现错误乃至崩溃.解决方法竟然只是给组件改个名。令人费解。

使自定义GameMode生效

在UE编辑器的主界面中,右侧的细节面板旁边一般是有一个世界场景设置的。如果没有这个设置,可以在上方的窗口->世界场景设置中打开。找到游戏模式,将游戏模式重载修改为刚刚创建的蓝图类。这样游戏开始时就默认会实例化一个BP_SurGameModeBase,并开始执行我们刚才定义的逻辑。

下方的选中的游戏模式是GameMode预先登记的一些默认类,当场景没有默认的相关类的话,就会自动帮我们实例化,我们同样可以在蓝图或者C++代码里使用这些对象。这里可以根据自己写过的类随意设置一下,一般来说默认也行,不在本次课程的讨论范围里。

image-20230315164031533

世界场景设置中更换游戏模式重载

运行游戏,会发现自动生成的AI傻站着一动不动,原来是AI控制器没有运行。在默认的设置中,只有提前放置在场景中的AI角色才会被AI控制器控制,修改这个设置有两种办法,一种是在AICharacter里修改:

image-20230315150911261

在蓝图里修改“自动控制AI”

还有一种是在代码里进行修改,这样生成的AI就可以默认被AI控制器控制了:

//SurAiCharacter.cpp
void ASurAiCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
   Super::SetupPlayerInputComponent(PlayerInputComponent);
   AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned;
}

最终效果&总结

最后运行游戏,可以看到AI小兵不断地在角色身边生成直到上限,这些AI看到主角后会发起攻击,同样的,玩家也可以攻击AI将其摧毁。随着游戏的进展,数量上限越来越高,AI角色也越来越多,现在终于有点游戏的样子了?可喜可贺。

做个总结吧,本节课我们创建了第一个GameModeBase类,为这个游戏添加了第一个规则,有点像丧尸围城,会有源源不断的AI敌人生成并试图攻击玩家。读者可以发挥想象力,活用GameModeBase类,以及他的好兄弟GameState类,为这游戏创建更加复杂的规则,包括胜利条件。学习到这个阶段,相信大家对UE C++已经具备了感性的认识,这时候应该试着更进一步,理解UE4的架构以及各个组件之间的关系。这里推荐知乎文章《InsideUE4》,以风趣幽默的口吻讲述了不少UE4架构的相关知识。笔者本人也在不断的学习,不论是UE4的知识还是写博客的风格,希望看到这里的读者能够积极发表评论,共同进步。

参考链接

细节面板空白相关BUG https://zhuanlan.zhihu.com/p/267986596

《InsideUE4》Gamemode和GameState https://zhuanlan.zhihu.com/p/23707588

创建属性组件(虽然文章的标题不是这个)https://www.bilibili.com/read/cv19014581?spm_id_from=333.999.0.0

标签:曲线,游戏,05,AI,GameMode,C++,查询,函数
From: https://www.cnblogs.com/Qiu-Bai/p/17219956.html

相关文章

  • 斯坦福 UE4 C++ ActionRoguelike游戏实例教程 04.角色感知组件PawnSensingComponent
    斯坦福课程UE4C++ActionRoguelike游戏实例教程0.绪论概述本文章对应课程第十一章43、44节。本文讲述PawnSensingComponent中的视觉感知的使用,以及对AI角色平滑转身......
  • C++学习记录
    C++recordnotebook基础导论C++特性具有c访问硬件的能力和面向对象程序的属性,以及更具有泛型编程的功能(使用模板进行编程)。OOP(面向对象编程)其中的方法有:自顶向下和......
  • C++ 构造函数和析构函数
    构造函数和析构函数目录页面问题构造函数与析构函数初始化列表转换构造拷贝构造(这种都是浅拷贝,每一项成员依次拷贝过去)默认的赋值运算符小的总结页面构造/......
  • C++ 常用语法
    1.定义一个字符串常量staticconststd::stringversion("0.0.1");staticconststd::stringname("Car-"+version);2.定义size大小staticconstexpruint64_tsh......
  • C++风格 字符串操作
    获取字符串长度              str.size();或者str.length();连接字符串                     str=str+"world";删除字符串......
  • [计算机基础笔记] C/C++
    C语言面向过程,C++面向对象。面相过程的思维方式,它更加注重这个事情的每一个步骤以及顺序。他比较直接高效,需要做什么可以直接开始干。程序=算法+数据面向对象的思维方式......
  • ERROR 10516 --- [ restartedMain] o.s.b.d.LoggingFailureAnalysisReporter :
    在IDEA上运行程序时遇到如下问题:如果你跟我一样也遇到了这个问题,那么大概率是端口冲突造成的。可能是之前运行的程序没有完全关闭从而影响到了现在的程序运行,最根本的解......
  • C++学习笔记3
    18.虚析构问题提出:在继承关系中构造和析构什么时候被调用?假如当前有类CSon继承CFather构造:当newCSon的时候,就会调用CSon(),程序跳进CSon(),在CSon()里会先调用CFather(......
  • C++学习笔记4
    C++=C+面向对象+泛型编程+STL26.STL容器STL(标准模板库),它其中包含了:容器、迭代器、算法、空间配置器、配接器、仿函数六个部分,这里介绍一些容器以及几个简单算法......
  • 一些不常遇到的C++知识总结
    explicit防止隐式转换C++提供了关键字explicit,可以阻止不应该允许的经过转换构造函数进行的隐式转换的发生,声明为explicit的构造函数不能在隐式转换中使用。C++中,一个......