【翻译】UE4中的基础导航功能

一个开发者的点滴积累

简介

现今,网上有大量优秀的资源供游戏开发者学习如何使用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来提高到达区域的精度(特别是楼梯的地方)。


Cell Size 19.0

Cell Size 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()函数,创建下面这些UFUNCTIONUPROPERTY

注意:每当我们要使用虚幻引擎提供的类时,我们都必须#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, MoveToActorMoveToLocation

我们用最简单的形式——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控制的!

原文链接

Basic Navigation in UE4

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容