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

概述

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

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

目录

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

认识GameMode

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

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

图片来自知乎《InsideUE4》

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

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

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

创建GameMode

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

创建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)
{
MaxBotCount = DifficultyCurve->GetFloatValue(GetWorld()->TimeSeconds);
} if(NrOfAliveBots >= FMath::RoundToInt(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里的成员赋值:

设置成员

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

设置AI出生点

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

设置环境查询(Query_FindBotSpawn)

设置环境查询内容(QueryContext_AllPlayers)

设置难度曲线

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

创建曲线

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

编辑曲线

为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++代码里使用这些对象。这里可以根据自己写过的类随意设置一下,一般来说默认也行,不在本次课程的讨论范围里。

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

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

在蓝图里修改“自动控制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

斯坦福 UE4 C++ ActionRoguelike游戏实例教程 05.认识GameMode&自动生成AI角色的更多相关文章

  1. 《Genesis-3D开源游戏引擎完整实例教程-跑酷游戏篇01:道路的自动生成》

    1.道路的自动生成 道路自动生成概述: 3D跑酷游戏的核心就是跑,在跑这一过程中增加趣味性使得游戏具有更多的可玩性.道路的自动生成和自由拼接,为游戏增设了更多的不可预见性.这种不可预见性使得玩家在游戏 ...

  2. 《Genesis-3D开源游戏引擎--横版格斗游戏制作教程05:技能读表》

    5.技能读表 技能读表概述: 技能读表,作为实现技能系统更为快捷的一种方式,被广泛应用到游戏开发中.技能配表,作为桥梁连接着游戏策划者和开发者在技能实现上的关系.在游戏技能开发中,开发者只需要根据策划 ...

  3. Effective C++ .05 一些不自动生成copy assigment操作的情况

    主要讲了 1. 一般情况下编译器会为类创建默认的构造函数,拷贝构造函数和copy assignment函数 2. 执行默认的拷贝构造/copy assignment函数时,如果成员有自己的拷贝构造/c ...

  4. Cocos2d-x3.0游戏实例《不要救我》第十篇(结束)——使用Json配置数据类型的怪物

    如今我们有2种类型的怪物,并且创建的时候是写死在代码里的,这是要作死的节奏~ 所以.必须可配置.不然会累死人的. ; i < size; ++i) { int id = root[i][&quo ...

  5. Cocos2d-x3.0游戏实例之《别救我》第八篇——TiledMap实现关卡编辑器

    版权声明:本文为博主原创文章,未经博主同意不得转载. https://blog.csdn.net/musicvs/article/details/25368273 好吧.我真心全然搞不懂.我如今仅仅只 ...

  6. 《Genesis-3D开源游戏引擎完整实例教程-跑酷游戏篇:简介及目录》(附上完整工程文件)

    跑酷游戏制作 游戏类型: 此游戏Demo,为跑酷类游戏. 框架简介: 游戏通常由程序代码和资源组成.如果说模型.贴图.声音之类的可以给游戏环境提供一个物理描述和设置,那么脚本和代码块会给游戏赋予生命, ...

  7. 《Genesis-3D开源游戏引擎完整实例教程-2D射击游戏篇02:滚屏》

    2.滚屏 滚屏概述: 打飞机游戏场景背景设计通常很简单,因为角色敌人道具等都不与背景发生交互事件.开发者只需要根据设定的游戏类型,为游戏制作背景,模拟一个大环境即可. 滚屏原理: 材质UV动画,实现背 ...

  8. 《Genesis-3D开源游戏引擎完整实例教程-2D射击游戏篇:简介及目录》(附上完整工程文件)

    G-3D引擎2D射击类游戏制作教程 游戏类型: 打飞机游戏属于射击类游戏中的一种,可以划分为卷轴射击类游戏. 视觉表现类型为:2D 框架简介: Genesis-3D引擎不仅为开发者提供一个3D游戏制作 ...

  9. 《Genesis-3D开源游戏引擎完整实例教程-2D射击游戏篇01:播放序列动画》

    1.播放序列动画 系列动画播放概述 2D游戏中的动画系统,不同于3D游戏.3D游戏中,角色美术资源不仅包含角色模型的,还包括角色的贴图和动作等,模型本身自带角色的动作动画效果.2D游戏中,角色美术资源 ...

  10. Python导出Excel为Lua/Json/Xml实例教程(一):初识Python

    Python导出Excel为Lua/Json/Xml实例教程(一):初识Python 相关链接: Python导出Excel为Lua/Json/Xml实例教程(一):初识Python Python导出 ...

随机推荐

  1. 甲骨文宣布: 也做 PostgreSQL!

    PostgreSQL 在开源数据库世界中一直是一个标志性的存在.经过35年的严格开发,它以其可靠性.强大的功能和性能而自豪.DB-engines 的突出显示,其市场份额的增长证明了其适应性强的数据模型 ...

  2. Kubeflow基础知识

    kubeflow 基础知识 kubeflow 简介 kubeflow是谷歌开源的MLOps开源平台,其中包含的不同组件代表了机器学习生命周期的不同阶段. 下图是kubeflow组织ML工作流程: ku ...

  3. centos7 oracle11gR2安装

    CentOS7安装Oracle 11gR2 图文详解 摘自: http://www.linuxidc.com/Linux/2016-04/130559.htm 最近要运维一个项目,准备在家办公,公司无 ...

  4. 15. 从零开始编写一个类nginx工具, 如果将nginx.conf转成yaml,toml,json会怎么样

    wmproxy wmproxy将用Rust实现http/https代理, socks5代理, 反向代理, 静态文件服务器,后续将实现websocket代理, 内外网穿透等, 会将实现过程分享出来, 感 ...

  5. 当我们输入 kubectl run 时都发生了什么?

    为了确保整体的简单性和易上手,Kubernetes 通过一些简单的抽象隐去操作背后的复杂逻辑,但作为一名有梦想的工程师,掌握其背后的真正思路是十分有必要的.本文以 Kubectl 创建 Pod 为例, ...

  6. 小白也能看懂的 AUC 曲线详解

    小白也能看懂的 AUC 曲线详解 简介 上篇文章 小白也能看懂的 ROC 曲线详解 介绍了 ROC 曲线.本文介绍 AUC.AUC 的全名为Area Under the ROC Curve,即 ROC ...

  7. NewsCenter

    打开界面有一个搜索框 抓包查看是post形式提交的数据包 这时候试试sql注入,万能密码直接全都显示,那就说明存在sql注入漏洞 这里试试用sqlmap自动注入试试(POST类型的sql注入第一次尝试 ...

  8. 递归与分治思想:汉诺塔(递归 && 分治思想)

    1 //64个盘子 2 //划分成小问题:1.将上面的63个盘子从x借助z移动到y上 3 2.将第64个盘子从x移动到z上 4 3.将y上的63个盘子借助x移动到z上 5 详解:https://www ...

  9. go并发 - goroutine

    概述 Go并发模型独树一帜,简洁.高效.Go语言最小执行单位称为协程(goroutine),运行时可以创建成千万上个协程,这在Java.C等线程模型中是不可想象的,并发模型是Go的招牌能力之一.很多文 ...

  10. 在ASP.NET Core 中使用 .NET Aspire 消息传递组件

    前言 云原生应用程序通常需要可扩展的消息传递解决方案,以提供消息队列.主题和订阅等功能..NET Aspire 组件简化了连接到各种消息传递提供程序(例如 Azure 服务总线)的过程.在本教程中,小 ...