简介
现今,网上有大量优秀的资源供游戏开发者学习如何使用UE4内置的AI功能。不幸的是,这些资料中,有许多已经过时了,还有的要么是教授特定的功能点,要么是大型系列教程中的一部分,甚至还有假设你拥有一些你根本不知道的知识的。
本博文系列旨在给读者提供一个可以一步步实践的示例,用的是UE4(4.17版)的AI系统。文章面向的读者群体是向中级开发者过度中的初级开发者。本文将会用蓝图和C++两种方式来介绍如何使用AI功能,每一个功能都会先给出一个蓝图的使用示例,然后用C++来实现相同的功能。
本文假设读者已经具备了基础的使用引擎和编辑器的技能,至少需要实践过蓝图快速入门指南和编程快速入门这两篇文章中的内容。
目标
在接下来的几篇博文中,我们会做一个只使用AI的demo项目,项目的名称是"Game of Tag"。
我们要一步步的完成这个项目,使用的初始工程是一个几乎完全空白的项目。然后,用虚幻游戏性框架逐步完成我们的项目。
这篇文章里,我们就来看看如何使用AI驱动角色。当然,这也少不了虚幻4的导航系统(Navigation System)的帮助。
建立工程
下载初始模板。
这个简单的工程里只包括了一个人体模型(从UE4模板中弄出来的)以及一个基本的角色蓝图(BP_TagCharacter)。BP_TagCharacter中设置好了网格(Mesh)和动画蓝图(Animation Blueprint),并且相关的组件也已经调整到合适状态。最后,工程还包含了一个基本的几何关卡,用来测试我们的AI。
另外,如果你在用git,你也可以从我的Bitbucket仓库中获取工程。最初的下载提示里有template标签。代码库的状态(每篇博文的最后)也会根据文章名来标记(比如basic-navigation)。
在做什么大改动之前,我们先做一个小功能——用UE4的导航系统让AI Character可以四处走动。
UE4导航系统提供了Nav Mesh(Navigation Mesh,导航网格)来确定关卡中哪些地方是可以走动的。要构建一个导航网格,我们直接添加一个NavMeshBoundsVolume就行了,注意要把它放大到包含所有可以到达的地方!
大小调整好之后,按下P键就可以看到Nav Mesh了。
当你把Volume添加到关卡中时,你一定注意到了有一个名为RecastNavMesh-Default的角色也被加进去了。我们可以修改这个角色的设置来看到NavMesh的不同元素。当然,我们也可以通过它来调整网格的生成方式。
选中RecastNavMesh-Default,在细节面板中调整Agent Radius和Agent Height到44.0和192.0来适应我们的角色。这个操作会迫使NavMesh根据我们角色的尺寸来确定所有可以到达的区域。
设置好这两个参数之后,我们要把Cell Size的值从19.0下调到5.0来提高到达区域的精度(特别是楼梯的地方)。
生成NavMesh可能要花点时间,所以,如果游戏不需要这么高的精度,就不要去调整。当然,我们这个工程是需要的。
注意:马上测试一下你的NavMesh。它可能不会出现,或者当你模拟游戏的时候它会消失。如果出现这种情况,关闭UE4编辑器然后重新启动,然后移动一下NavMesh的位置再复原,强制编辑器重建网格。这种情况似乎是因为基于BSP/几何体的关卡会和NavMesh产生奇怪交互导致的。确保你的导航网格和上图中的一致,这样,当你按下Alt+S模拟游戏的时候,网格不会消失。
四处走动
下一步,我们要放一个简单的AI Character到游戏中让它能四处走动。比较合理的做法是,创建一个AI Controller类,这个类会随机选择一个路标,然后把角色移动到那,等待一段时间后,重复之前的行为。
先来设置路标。这个操作非常简单,在几何体面板中找到“Target Point”,拖几个到地图里就行了:
接下来,我们要创建一个类,继承自AI Controller类,用来控制我们的AI。我会给出蓝图和C++两种实现方式,选一种你喜欢的方式尝试吧!
提示:AIController通常被用作Pawn或Character的头脑,与Player Character十分类似。它主要关心的是角色要做什么,而不是他能不能做到(也就是说,Controller应该拥有输入和智能,Pawn应该拥有游戏逻辑)。
【蓝图】AI Controller
创建一个新蓝图类,当编辑器询问继承什么父类时,展开所有类,选择AI Controller,确定,将我们的类命名为:BP_TagController。
TagController类首先要实现的,是找到关卡中的所有路标点,然后保存起来。这一步,我们要用到Get All Actors Of Class节点:
我们也要能随机获得一个路标点,所以,我们需要一个新函数:GetRandomWaypoint。(注意:我把这个函数标记为Pure,因为它不需要改变任何状态。):
最后,创建一个GoToRandomWaypoint函数并且在BeginPlay中调用它:
但是要怎么实现GoToRandomWaypoint函数呢?这里就要用到导航系统了。可以选择的节点有四种,取决于我们想移动到一个Actor还是一个Location,也取决于我们想使用简单的版本还是想要使用复杂的版本:
在这里,我们用最简单的版本:Simple Move to Actor,只需提供一个随机的路标点参数:
这样,我们的Controller就完成了!
【C++】AI Character
创建一个C++类,继承自AIController,命名为TagController。同样,在创建的时候需要展开所有类。
提示:不管什么时候,只要我引用虚幻引擎的API,我都会给出一个到线上文档的链接。点击AIController,查看我们在重写的都是些什么函数。
我们的AI非常简单,只需要重写BeginPlay()函数,创建下面这些UFUNCTION和UPROPERTY。
注意:每当我们要使用虚幻引擎提供的类时,我们都必须#include它,通常,他们是不会被默认包含的。所以,别忘了在#include"*.generated.h"之前加上#include "Runtime/Enigne/Classes/Engine/TargetPoint.h"。
public:
void BeginPlay() override;
private:
UPROPERTY()
TArray<AActor*> Waypoints;
UFUNCTION()
ATargetPoint* GetRandomWaypoint();
UFUNCTION()
void GoToRandomWaypoint();
现在我们来实现这些函数。
首先,Begin Play需要保存所有的路标点。为了实现这个功能,我们要使用UGameplayStatics::GetAllActorsOfClass函数,参考官方论坛提问的第二个答案。接着,调用GoToRandomWaypoint函数,稍后我们会实现这个函数。
同样,我们要包含一个头文件,这次的代码是#include "Runtime/Engine/Classes/Kismet/GameplayStatics.h"
void ATagController::BeginPlay()
{
Super::BeginPlay();
UGameplayStatics::GetAllActorsOfClass(GetWorld(), ATargetPoint::StaticClass(), Waypoints);
GoToRandomWaypoint();
}
别忘了调用Super::BeginPlay()。如果不调用,你可能要花费好几个小时来找为什么无法成功的原因。
在我们去到随机路标点之前,我们需要获取一个随机路标点。使用UE4的FMath函数就能轻松做到。同时,我们也要把获取的值转换成正确的类型,使用的函数是Cast:
ATargetPoint* ATagController::GetRandomWaypoint()
{
auto index = FMath::RandRange(0, Waypoints.Num() - 1);
return Cast<ATargetPoint>(Waypoints[index]);
}
最后,我们来实现GoToRandomWaypoint函数。同样,我们有几种选择:MoveTo, MoveToActor和MoveToLocation。
我们用最简单的形式——MoveToActor,并且全部使用默认参数。
void ATagController::GoToRandomWaypoint()
{
MoveToActor(GetRandomWaypoint());
}
完成!
使用Controller
既然我们已经造好了“大脑”,是时候把它放到“身体”里去了。
打开BP_TagCharacter,找到AIController变量,把它设置成TagController:
编译,保存,拖一个Character的实例到关卡中。运行,你就可以看到AI随机跑到一个路标点。多停止-运行几次,确保所有的路标点都没有问题!
译者注:
如果运行没有效果,可能是项目没有配置好。打开项目设置(Setting -> Project Settings),找到Agents,添加一个元素:
这样,导航网格就有用了。
添加更多移动
到现在为止,我们已经做了让AI四处走动的所有工作,但是看上去还是有点傻。我们来做点改进,让它在到达一个路标点之后休息一段时间,然后前往下一个随机路标点。
同样,我会给出蓝图和C++两种实现方式。
蓝图:ReceiveMoveCompleted
首先,我们要知道AI什么时候完成移动了。我们可以绑定一个自定义事件到ReceiveMoveCompleted上。我们要在BeginPlay的时候就绑定事件(这样,任何移动事件完成都会被调用),所以,我们的代码就像这样:
无论何时,只要导航移动完成,UE4就会调用我们的自定义事件,只要绑定事件是在之前调用的。
注意:为了达到百分之百的正确性,你要在调用GoToRandomWaypoint之前绑定事件。万一移动失败了(比如忘记放Target Point了),Result结构体还会提供一些调试信息。
移动完成后,我们要等待一段时间,然后调用GoToRandomWaypoint:
C++:OnMoveCompleted
用C++来实现移动结束之后做一件事比蓝图实现还要简单,这要感谢AIController::OnMoveCompleted虚函数!
我们要做的只有在头文件中重写这个函数:
public:
void OnMoveCompleted(FAIRequestID RequestID, const FPathFollowingResult& Result) override;
如果我们不想在移动结束后等待一会,函数就更加简单:
void ATagController::OnMoveCompleted(FAIRequestID RequestID, const FPathFollowingResult & Result)
{
Super::OnMoveCompleted(RequestID, Result);
GoToRandomWaypoint();
}
但是,添加一个延迟一秒的节点就变得有点棘手了,我们需要使用FTimerHandle。
在你的头文件中添加一个FTimerHanlde变量:
private:
FTimerHandle TimerHandle;
去掉直接调用GoToRandomWaypoint的代码,调用SetTimer函数等待一秒,然后再调用GoToRandomWaypoint函数:
void ATagController::OnMoveCompleted(FAIRequestID RequestID, const FPathFollowingResult & Result)
{
Super::OnMoveCompleted(RequestID, Result);
GetWorldTimerManager().SetTimer(TimerHandle, this, &ATagController::GoToRandomWaypoint, 1.0f, false);
}
注意:GetWorldTimerManager()函数可能会给你一个警告“不完整的类型是不允许的(imcomplete type is not allowed)”,不过不用管,还是可以编译通过。
总结
现在,我们拥有了一个可以在关卡里乱跑的Pawn了,而且还是AI控制的!