斯坦福课程 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. July 2023 (version 1.81)

    July 2023 (version 1.81) 更新后显示发行说明 Update 1.81.1: The update addresses these issues. Welcome to the ...

  2. Batch Normalization 批量归一化的运算过程

    Batch Normalization 批量归一化 具体的运算过程: 假设经过卷积计算过后得到的feature map的尺寸为 2*3*2*2. 其中2代表的是batch的大小,3代表的是通道chan ...

  3. 20.2 OpenSSL 非对称RSA加解密算法

    RSA算法是一种非对称加密算法,由三位数学家Rivest.Shamir和Adleman共同发明,以他们三人的名字首字母命名.RSA算法的安全性基于大数分解问题,即对于一个非常大的合数,将其分解为两个质 ...

  4. ACTF 2023 部分WP

    来自密码手的哀嚎: 玩不了一点,太难了. CRYPTO MDH Description Malin's Diffile-Hellman Key Exchange. task.sage from has ...

  5. JAVA架构师

    https://github.com/zq99299/note-architect https://zq99299.github.io/note-architect/hc/ https://zq992 ...

  6. EFCore 使用FluntApi配置 全局查询筛选器

    我们在类中通常会有一个属性为 IsDel来表示软删除或也称逻辑删除,这个属性会导致我们在进行查询操作时,每一次都要 .where(s=>s.IsDel==false) 非常的麻烦.在使用efCo ...

  7. 题解 CF1004C

    题意描述: 给定 \(n\) 个数,从前往后找,看看能和后面的数组成多少个不同的数对. 若两个数对仅仅是位置不同,我们也认为是两个不同的数对. 题目分析: 阅读题目,我们不难看出,若前面有一个数已经出 ...

  8. 递归与分治思想:治思想 && 折半查找法(迭代 && 递归)

    1 //分治思想:将大问题拆成小问题逐一解决 2 //折半查找法:不断缩小一半查找的范围,知道达到目的,效率较高. 详情见:https://fishc.com.cn/thread-27964-1-1. ...

  9. 【免费】小傅哥 DDD 开发小册

    作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 大家好,我是技术UP主小傅哥. 如果在面试的时候,面试官问你DDD是什么,你怎么解释?是不是感 ...

  10. 基于 Echarts实现可视化数据大屏展示?

    当涉及到使用Echarts实现可视化数据大屏展示时,以下是一份非常详细的介绍说明. 第一部分:介绍Echarts Echarts是一个由百度开源的基于JavaScript的可视化图表库.它支持多种图表 ...