斯坦福 UE4 C++ ActionRoguelike游戏实例教程 05.认识GameMode&自动生成AI角色
斯坦福课程 UE4 C++ ActionRoguelike游戏实例教程 0.绪论
概述
本篇文章将会讲述UE中Gamemode的基本概念,并在C++中开发GameMode,为游戏设置一个简单的玩法:使用环境查询自动生成AI角色,并自定义一条难度曲线,随着时间增大游戏的难度。
最终实现效果,为AI小兵添加了属性组件,可以被我们打爆;编辑GameMode,每隔两秒钟在玩家生成一个AI小兵,有生成上限,上限随着游戏时间增大而增大:
目录
- 认识GameMode
- 创建GameMode
- 创建环境查询、难度曲线
- 使自定义GameMode生效
认识GameMode
在之前的所有课程中,我们制作了自己的角色,制作了敌对AI角色,制作了一系列场景物品,这些形形色色的Actor被我们拖动到场景中,组成了一个美好的展览馆。但是我们可以问一下自己,我们做的这些真的可以组成一个游戏吗?到目前为止,我们只是不停地制作一个物品,一个功能,一段交互逻辑,并将他们放置在我们的虚拟世界中,但是他们只是静静地站立在那里,不知道自己存在的意义,不知道自己要到哪里去。
作为这个世界的创世神,是时候为这个世界创造一个规则了,有了规则,才称得上是游戏。本篇文章将会介绍构成整个游戏逻辑的一个重要组件:GameMode。
GameMode类继承自AInfo,作为Actor大家族的一个成员,它就像Actor家族的领袖,指引Actor们如何出生和灭亡。
GameMode定义了一个游戏的玩法,游戏的规则由他指定,正如它的标识为一个旗子一样,你可以用它来规定游戏的玩法是抢夺一个旗子,又或是一个5v5的团队竞技,或者是一个开放世界抽卡游戏。只要它一声令下,就可以宣布游戏开始,如果它愿意,它也可以随时暂停和终止游戏。每一个游戏世界都需要一个GameMode类来管理游戏逻辑。
同样的,它可以指定玩家进入关卡时,默认使用的是哪一个Controller,控制的是哪一个Pawn,加载的是哪一个UI界面。总之,它贯彻了一个关卡的始终。它不依附于场景里的任一个Actor,只要游戏启动了,它就会一直履行它的职责。具体到代码里如何实现,让我们边做边说。
创建GameMode
还是老规矩,右键内容浏览器,创建一个GameModeBase的子类,这里我将它命名为SurGameModeBase
。就这样,创世神的第一个得力助手诞生了。
进入代码编辑器,让我们看看GameModeBase支持的操作有哪些。比较常用的方法有InitGame
、InitGameState
、StartPlay
等函数,这篇文章并不是API文档,先短暂看一下今天我们要实现什么目标:实现AI角色每隔一段时间在玩家角色周围自动生成,并实现一个难度曲线,使得AI的个数存在一个动态的上限。因此,今天的重点是重写GameModeBase::StartPlay
函数,为这个游戏时间建立一个简单的初始法则。
在父类中,StartPlay
负责通知所有Actor调用BeginPlay函数,也就是说,只有GameModeBase类一声令下,调用StartPlayer,场景里的Actor才能开始工作,才能拥有自己的心跳(Tick)。而作为子类,我们重写时需要记得调用Super::StartPlay
,然后才在后面添加逻辑。
要想实现功能,我们需要为SurGameModeBase添加一系列成员:
- 指定生成的AI类型
- 生成AI所需要的环境查询
- 定义AI小兵生成数量难度曲线
- 生成AI的间隔时间
- 因为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
开始:
- 在游戏开始后,开启一个循环执行的定时器,每隔
SpawnTimerInterval
时间执行一次SpawnBotTimerElapsed
。注意SetTimer
的最后一个参数为True。 - 在
SpawnBotTimerElapsed
函数中, 调用UEnvQueryManager::RunEQSQuery。该函数可以执行一次环境查询,并返回一个环境查询的实例即UEnvQueryInstanceBlueprintWrapper
对象。由于环境查询可能需要花上好几帧的时间才能结束,因此需要自定义一个回调函数,即OnQueryCompleted
,作为参数传进去,在查询结束后调用。 - 当环境查询执行完毕后,调用
OnQueryCompleted
。在该函数里,我们需要判断环境查询的结果,如果为Success则继续。 - 使用UE提供的Actor迭代器,遍历所有AICharacter,如果AICharacter拥有属性组件且存活,则计入计数中。
- 获取在UE编辑器里定义的难度曲线,以时间为X轴获取数据,即当前允许存在的Bot的最大值。
- 如果当前存活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的出生点简单设置为取所有玩家附近圆环的一点。对于创建环境查询我们已经轻车熟路了,这里就不展开叙述了。
设置难度曲线
右键内容浏览器,选择其他->曲线
即可找到曲线。
进入曲线编辑器,使用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控制器控制了:
//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角色的更多相关文章
- 《Genesis-3D开源游戏引擎完整实例教程-跑酷游戏篇01:道路的自动生成》
1.道路的自动生成 道路自动生成概述: 3D跑酷游戏的核心就是跑,在跑这一过程中增加趣味性使得游戏具有更多的可玩性.道路的自动生成和自由拼接,为游戏增设了更多的不可预见性.这种不可预见性使得玩家在游戏 ...
- 《Genesis-3D开源游戏引擎--横版格斗游戏制作教程05:技能读表》
5.技能读表 技能读表概述: 技能读表,作为实现技能系统更为快捷的一种方式,被广泛应用到游戏开发中.技能配表,作为桥梁连接着游戏策划者和开发者在技能实现上的关系.在游戏技能开发中,开发者只需要根据策划 ...
- Effective C++ .05 一些不自动生成copy assigment操作的情况
主要讲了 1. 一般情况下编译器会为类创建默认的构造函数,拷贝构造函数和copy assignment函数 2. 执行默认的拷贝构造/copy assignment函数时,如果成员有自己的拷贝构造/c ...
- Cocos2d-x3.0游戏实例《不要救我》第十篇(结束)——使用Json配置数据类型的怪物
如今我们有2种类型的怪物,并且创建的时候是写死在代码里的,这是要作死的节奏~ 所以.必须可配置.不然会累死人的. ; i < size; ++i) { int id = root[i][&quo ...
- Cocos2d-x3.0游戏实例之《别救我》第八篇——TiledMap实现关卡编辑器
版权声明:本文为博主原创文章,未经博主同意不得转载. https://blog.csdn.net/musicvs/article/details/25368273 好吧.我真心全然搞不懂.我如今仅仅只 ...
- 《Genesis-3D开源游戏引擎完整实例教程-跑酷游戏篇:简介及目录》(附上完整工程文件)
跑酷游戏制作 游戏类型: 此游戏Demo,为跑酷类游戏. 框架简介: 游戏通常由程序代码和资源组成.如果说模型.贴图.声音之类的可以给游戏环境提供一个物理描述和设置,那么脚本和代码块会给游戏赋予生命, ...
- 《Genesis-3D开源游戏引擎完整实例教程-2D射击游戏篇02:滚屏》
2.滚屏 滚屏概述: 打飞机游戏场景背景设计通常很简单,因为角色敌人道具等都不与背景发生交互事件.开发者只需要根据设定的游戏类型,为游戏制作背景,模拟一个大环境即可. 滚屏原理: 材质UV动画,实现背 ...
- 《Genesis-3D开源游戏引擎完整实例教程-2D射击游戏篇:简介及目录》(附上完整工程文件)
G-3D引擎2D射击类游戏制作教程 游戏类型: 打飞机游戏属于射击类游戏中的一种,可以划分为卷轴射击类游戏. 视觉表现类型为:2D 框架简介: Genesis-3D引擎不仅为开发者提供一个3D游戏制作 ...
- 《Genesis-3D开源游戏引擎完整实例教程-2D射击游戏篇01:播放序列动画》
1.播放序列动画 系列动画播放概述 2D游戏中的动画系统,不同于3D游戏.3D游戏中,角色美术资源不仅包含角色模型的,还包括角色的贴图和动作等,模型本身自带角色的动作动画效果.2D游戏中,角色美术资源 ...
- Python导出Excel为Lua/Json/Xml实例教程(一):初识Python
Python导出Excel为Lua/Json/Xml实例教程(一):初识Python 相关链接: Python导出Excel为Lua/Json/Xml实例教程(一):初识Python Python导出 ...
随机推荐
- Go结构体深度探索:从基础到应用
在Go语言中,结构体是核心的数据组织工具,提供了灵活的手段来处理复杂数据.本文深入探讨了结构体的定义.类型.字面量表示和使用方法,旨在为读者呈现Go结构体的全面视角.通过结构体,开发者可以实现更加模块 ...
- Unicode 字符集与 UTF-8 编码系统
Unicode 字符集与 UTF-8 编码系统 Synopsis: Unicode 只是包含了所有语言符号.图形符号等的统一字符集(character set,每个字符都有唯一的 Unicode co ...
- 记一次Redis Cluster Pipeline导致的死锁问题
作者:vivo 互联网服务器团队- Li Gang 本文介绍了一次排查Dubbo线程池耗尽问题的过程.通过查看Dubbo线程状态.分析Jedis连接池获取连接的源码.排查死锁条件等方面,最终确认是因为 ...
- CSP初赛知识点
初赛知识点 计算机基础知识 1946年,世界上第一台计算机 ENIAC(埃尼阿克)在美国宾夕法尼亚大学诞生. 冯·诺依曼:计算机之父,提出了计算机体系结构(冯·诺依曼架构) 运算器 控制器 存储器:存 ...
- go使用snmp库查询mib数据
转载请注明出处: OID(Object Identifier)是一种用于标识和唯一命名管理信息库中的对象的标准方式.给定一个OID,可以确定特定的管理信息库对象,并对其进行操作. go语言使用snmp ...
- Ubuntu上解决快捷键与idea快捷键冲突
Ubuntu上解决快捷键与idea快捷键冲突 一.ubuntu 本身系统导致,需要修改 ubuntu 快捷键 解决方案: 设置 按钮→系统设置→硬件选项区域中的"键盘"→切换到&q ...
- K8S 组合命令
强制删除namespace kubectl get namespace [namespace-name] -o json | tr -d "\n" | sed "s/\& ...
- QPixmap、QIcon和QImage
QPixmap依赖于硬件,QImage不依赖于硬件.QPixmap主要是用于绘图,针对屏幕显示而最佳化设计,QImage主要是为图像I/O.图片访问和像素修改而设计的. 当图片小的情况下,直接用QPi ...
- 二叉树、平衡二叉树、红黑树、B树、B+树
几种树的主要区别: 红黑树为二叉自平衡搜索树,深度大,多用于内存排序: B树为多路(多叉)搜索树,深度低,搜索数据时磁盘IO较少,多用于索引外存数据,只支持随机访问,不支持顺序访问: B+树是对B树的 ...
- 一篇文章带你掌握测试基础语言——Python
一篇文章带你掌握测试基础语言--Python 本篇文章针对将Python作为第二语言的用户观看(已有Java或C基础的用户) 因为之前学习过Java语言,所以本篇文章主要针对Python的特征和一些基 ...