特稿 >

行业洞察 >

手把手搭建游戏AI—如何使用深度学习搞定《流放之路》

手把手搭建游戏AI—如何使用深度学习搞定《流放之路》

Xtecher原创 丨 行业洞察

14607
2110

2017-09-10

           

翻译 | 彭硕,姜沂,reason_W

编校 | reason_W


DeepMind开源《星际2》AI平台,OpenAI人工智能系统打败Dota2游戏顶级玩家......越来越多的科技巨头开始进入到游戏AI的领域,并相继开放了他们的接口和数据集。复杂的训练数据,即时多变的对战环境,对多智能体协作能力的要求等等使得《星际争霸》这样的游戏被称为通用智能的关键,预示着AI将在越来越真实的混乱环境里向人类的心智靠近。


那么小白玩家该如何入坑游戏AI呢?游戏AI到底是如何和游戏进行接口交互,判断角色状态,执行动作,规划策略的呢?


本期推送AI科技大本营(微信ID:rgznai100)精选了国外一位博主游戏AI系列的文章,来,手把手开始搭建一个游戏AI。


快到碗里来吧~


基于深入学习和其他机器学习技术,我们将为“流亡之路”(以下简称PoE)打造一个游戏AI。本篇教程总共分为5个阶段,下面是原贴列表。


  1. 第一部分:概述

  1. 第二部分:为《流放之路》标定投影矩阵

  1. 第三部分:移动和导航

  1. 第四部分:实时屏幕捕捉及底层命令

  2. 第五部分:基于TensorFlow的CNN实时障碍物和敌对目标检测


    (由于我们把五部分整合成了一篇文章,因此本文共分5部分,20小节,8300+字,阅读花费约17分钟。)


    我们这个项目的目标是打造一个基于视觉输入的游戏AI,它可以成功地在游戏地图中进行自主巡航和自主防御。当然,你也可以在这个过程中一边玩游戏,一边学习打造游戏AI的乐趣。



    第一部分:概述


    本部分共2节,原post链接


    A Deep Learning Based AI for Path of Exile: A Series

    https://nicholastsmith.wordpress.com/2017/07/08/a-deep-learning-based-ai-for-path-of-exile-a-series/


    PoE是一款和暗黑破坏神、泰坦之旅等的暗黑型RPG类似的动作类游戏,游戏截图如下图1所示:


    2.jpg

    图1:游戏截图


    玩家主要是通过鼠标进行人物移动、打怪、开箱等和游戏的交互。也可以设置键盘热键来进行特殊攻击、加蓝和其他菜单快捷键。


    1.顶层设计


    我们打造这个AI的想法是利用卷积神经网络(CNN)对游戏中的图像进行分类,从而建立游戏世界的内部表征。通过这个内部表征来引导游戏世界内的角色。下面这个流程图表示出了游戏AI的基本设计思路:

    3.png

    图2:人工智能逻辑的流程图


    AI程序的主循环会不停滴从游戏中获取一个静态的图像,并将它传递给一个CNN。CNN会预测这幅静态的图像中正在发生什么。这些预测随后被传递给世界内部地图,并根据最新的预测更新世界内部地图。接下来,游戏AI会根据当前世界内部地图的状态,进行一系列的动作。最后,这些动作被转换成鼠标和键盘输入,然后发送给鼠标和键盘。这个循环是在不停重复的。是不是听起来很容易?没错。


    我们选择Python(3.6)作为这次的编程语言。主要使用的库是


    • scikit-learn

    • TensorFlow

    • PyUserInput

    • win32gui

    • scikit-image


    接下来几部分,我们将研究如何进一步分解上述任务并一步步实现它们。


    2.免责声明


    PoE的logo、美工均属于游戏开放商Grinding Gear Games(GGG)的财产,作者与GGG没有任何关系,作者的思想和观点并不代表GGG的观点。本文的目的是探索人工智能和深度学习,没有侵犯版权或服务条款的行为。



    第二部分:为《流放之路》标定投影矩阵


    本部分共5节,原post链接:


    Calibrating a Projection Matrix for Path of Exile

    https://nicholastsmith.wordpress.com/2017/07/09/calibrating-a-projection-matrix-for-path-of-exile/


    在这一部分中,我们将探索如何利用游戏静态画面,更新其世界的内部表征。


    1.视觉输入的挑战


    让游戏AI与视觉输入进行交互的一个难点在于图像数据是2D的,但是(游戏)世界是3D的。所以最可能的是,游戏引擎在3D环境中使用它自己的世界内部表征,然后使用投影技术将游戏渲染为2D并显示在屏幕上。通过逆向工程,从游戏世界的表征中获得数据非常有用,但是因为我们的最终目的是要打造游戏AI,所以暂时不对这个逆向工程进行深入的探索。


    为了更精确地模拟世界,游戏的投影矩阵要和世界尽可能地相似。这个矩阵被用来确定与屏幕上的2D坐标相对应的3D坐标,(再进行一些假设)反之亦然。图3说明了投影映射的基本概念。左矩形表示屏幕,而右坐标轴代表世界坐标。灰线(投影映射)将蓝点从世界坐标映射到屏幕上的位置。


    4.jpg

    图3:投影影射


    给定2D图像来近似投影矩阵的过程被称为相机标定。


    2.相机标定


    相机标定是通过一幅包含一个(已知三维空间尺寸的)物体的图像来完成的。从三维坐标到二维坐标的映射,构造了一种求解变换矩阵的优化问题。这个思想可以表示为在方程1。


    4.5.png

    方程式1:投影转换


    在上面的方程中,A是投影矩阵,w是世界点(3D)坐标矩阵,而p是投影点(2D)坐标矩阵。


    为了标定PoE的相机,也就是确定上面等式中的 A,我们会使用几个固定大小的箱子。标定相机的过程如下面的截图所示。


    5.jpg

    图4:相机标定


    请注意,这些箱子的大致顶角是用一个点指定的,并将其标记为对应的世界点。这个过程比较麻烦,而且需要手动进行。我们把画面中间那个箱子的右下角指定成坐标原点(0,0,0)(小编注:当然这个原点可以随意指定,这里是为了方便),并且假设这个箱子是一个单元立方体。箱子之间的间距也是有单位长度的。另外一个注意的点是,这个投影是用于分辨率为800*600的屏幕的,其他屏幕分辨率的话,像素大小会发生变化,需要重新标定。


    其数据点集合(缩写)如表1所示:世界点坐标&投影点坐标


    6.png

    表1:数据映射


    接下来,我们构建一个转换矩阵A,将3D点投射到2D的点上。


    3.执行拟合


    我们通过TensorFlow构建一个非线性拟合,注意:将标定问题看成一个齐次最小二乘问题的方法是比较常见的;Adam 方法看起来可以为这个特殊的图像提供更好的结果。


    7.jpg

    (点击查看大图)


    运行上述代码所产生的投影矩阵如等式2所示。由于初始化的差异,最终的值可能会稍有不同。

    8.png

    方程式2:得到的投影矩阵


    用等式3给的公式,可以在世界坐标中恢复出相机上的位置C。注意,Q是一个3x3矩阵,m是一个3x1矩阵。


    9.png

    方程式3:相机位置的恢复


    下面的代码对投影矩阵进行拟合,恢复出来的相机位置用变量CP表示。

    10.png


    4.结果


    恢复出来的的相机位置是 (5.322,-4.899,10.526)。现在再回头看看一开始的截图,这个值和我们直觉上感受的方向是一致的。世界空间坐标分别以一个箱子的高度、宽度和深度作为单位长度。因此,相机位置大概是在x轴正方向上5个箱子长度,y轴负方向4个箱子长度,z轴正方向上10个箱子长度处。 利用这个投影矩阵,我们就可以把点投影到原始图像上了。下图展示了怎样把一个xy平面表示的网格点投影到原始图像上。


    11.jpg

    图5:原始图像和将投影的XY平面


    上面的投影看起来还蛮合理的。在投影矩阵标定好的情况下,可以使用下面的函数把一个三维点(每一行为1点)矩阵投影到平面上。


    12.png

    (点击查看大图)


    5.假设和平移


    如果假定角色仅在xy平面上移动,那么角色的3D位置就可以通过角色的像素坐标恢复。我们假设z=0,然后在投影方程中解出x和y,就可以给出这个角色的像素坐标。完成此任务的代码如下。


    13.png

    (点击查看大图)


    在上述两个函数中,投影矩阵的转置计算是影响效率的主要因素。有了以上两个函数之后,我们就可以用下面的代码计算在800*600屏幕上xy平面的网格点。下面这个函数将是后面跟踪玩家在一级平面上位置的关键。


    14.png

    (点击查看大图)


    在PoE中,当玩家移动时,相机也会移动(相机角度固定)。为了跟踪移动的相机和玩家,世界点在被投影之前会被转平移回原始位置。在实际中,这是通过将投影矩阵乘以一个平移矩阵得到最终的投影矩阵来实现的。方程4中显示了一个平移矩阵,它可以用向量(x,y,z)来表示一组点的平移。


    15.png

    方程式4:一个平移矩阵


    我们可以利用matplotlib(https://matplotlib.org/)来构造一个XY平面的动画,它模拟了世界中角色的运动。在下面的动画中,相机通过几个随机产生的点进行线性移动。

    16.gif

    图6:相机平移运动


    有了上述代码,屏幕上的距离就可以更精确了。为了简单起见,我们假设玩家总是在XY平面上移动。然而,在某些高度上,这并不是一个可靠的假设。考虑到AI的性能,这一部分可能需要重新考虑。



    第三部分:移动和导航


    本部分共4节,本节是文章的第三部分:PoE AI Part 3: Movement and Navigation


    原post链接:

    https://nicholastsmith.wordpress.com/2017/07/18/poe-ai-part-3-movement-and-navigation/


    这一部分我们主要探索在同一高度的平面上移动游戏角色的技巧。


    1.移动地图类


    在PoE中,玩家移动角色一般会通过单击某个位置来实现,接着角色就会移动到鼠标点击的位置。图7展示了通过点击鼠标移动角色的一个例子。


    17.gif

    图7:角色移动


    现在,为了完成导航,AI会维持一个代表世界地图的数据结构。赋予坐标和位置类型之间一种对应关系,就可以实现这一点。例如,在给定的时间,在其内部地图中,AI可能就具有表2所示的数据。


    世界点坐标 & 类型


    18.png

    表2:内部地图


    地图会记录已访问的位置及其类型。这个类型标记的是,玩家能否移动到该位置。位置类型分别为“开放”类型或“障碍”类型。有了这样的地图,就可以使用广度优先遍历找到从一个位置到另一个位置的最短路径。


    2.维度之间的映射


    现在,我们假设玩家在位置(0,0,0),并且要移动到(1,1,0)。应该怎么用鼠标在屏幕上进行操作呢?想一下前几部分的内容,一个标定好的投影矩阵,能让我们在3D坐标中更准确地逼近玩家的位置。因此,利用投影矩阵来变换该点(1,1,0)就可以确定其在屏幕上的位置。这就是鼠标要点击的位置。


    在实际中,我发现,在玩家为角色指定移动的目标点时,位移技能其实很不准确。特别是当我们在障碍物上单击时。在这种情况下,角色通常会移动到单击位置的附近。下面这幅图就是一个这样的例子。


    19.gif

    图8:向障碍物移动


    这幅图显示了在障碍物上点击鼠标的结果。请注意,玩家虽然会向鼠标点击的地方移动,但到了障碍物面前就会停下来。


    3.闪电传送


    不幸的是,像图8所示的情况可能会导致AI的内部地图与现实不一致。为了解决这个问题,我决定用闪电传送来移动。图9展示了3次传送的效果。

    20.gif

    图9:闪电传送


    在角色移动方面,闪电传送的优点是在运动的结果只有两项,易于确定; 即玩家移动到了指定位置或者玩家没有移动到指定位置。这有助于将AI的位置保持在其内部地图中,并且和玩家的实际位置保持同步。因此,为了移动到位置x,AI首先将点x投影到屏幕上,然后将鼠标移动到该位置,并触发适当的键执行闪电传送。


    4.运动检测器


    现在,我们剩下的唯一一个挑战就是检测传送是否成功执行。如果在障碍物上方单击,角色就不会执行传送。为了准确地预测这一点,我们构建了一个二值分类器,它将屏幕画面的一部分作为输入,并预测当前是否发生传送。程序首先从画面中将角色周围70×70的矩形提取出来,作为模型的输入。


    为了构建模型,我们用游戏静态图像来手动构造数据集。图10显示了从数据集中取出的样本。


    21.jpg

    图10:闪电传送分类器数据


    执行预测的代码如下。以下代码假定文件 LWTrain.csv 有多行这样的格式:文件名,Y / N。在每行中,filename是上述图像文件的路径,Y表示图像显示正在执行传送,而N表示相反,表示没有传送。


    22.jpg

    (点击查看大图)


    因此,在触发适当的键之后,AI会(重复地)调用 DetectLW 函数来检查移动是否成功。成功后,角色在地图上的位置就会更新。如果在一定时间内没有检测到传送,则假定移动失败,玩家在地图上的位置也就不会改变。



    第四部分:实时屏幕捕捉和底层命令


    本节是文章的第四部分:PoE AI Part 4: Real-Time Screen Capture and Plumbing


    原post链接

    https://nicholastsmith.wordpress.com/2017/08/10/poe-ai-part-4-real-time-screen-capture-and-plumbing/


    正如我们在本系列第一部分中说的那样,AI程序会获取游戏的屏幕截图,并使用它来进行预测,以更新其内部状态。这一部分中,我们将探索捕捉游戏截图的方法。


    23.png

    图11:AI逻辑流程图


    1.可用的库


    有很多Python库都可以用来捕捉游戏截图,比如 pyscreenshot(https://pypi.python.org/pypi/pyscreenshot)和 ImageGrab from PIL(http://pillow.readthedocs.io/en/3.1.x/reference/ImageGrab.html)。这里有一个简单的程序可以来测试图像捕捉的效果。代码如下:


    24.png

    25.png

    (点击查看大图)


    不幸的是,测试的效果看起来实在差强人意。要是没有其他处理的话,顶多只能盼着这程序每秒处理5到6帧的画面。此外,在上面的代码中,屏幕捕捉要在主线程上运行。所以整个程序要等待获取到图像,在此期间不会和游戏发生处理或交互。另一个问题是,捕捉到的应该只有游戏画面(在窗口模式下),而不应该包括电脑桌面上其余的部分。


    2.使用Windows API 


    我们可以通过调用几个Windows API来减轻这些问题,并提高程序性能。


    26.jpg

    (点击查看大图)


    在上面的GetHWND函数中,我们使用了win32gui.FindWindow(None,wname)来获取游戏窗口的句柄。在这种情况下,wname应该是“Path of Exile”或win32gui.FindWindow(None,“Path of Exile”)。


    游戏窗口的句柄,win32gui.GetWindowRect(self.hwnd) 给出了游戏窗口在屏幕上的位置。这些值对于将游戏窗口(大小800×600)中鼠标的移动转换为屏幕上的绝对值(通常类似于1920×1080)是很必要的。


    上面的GetScreenImg函数是用来实际捕获游戏画面图像的,并将其存储在numpy矩阵中的代码。上述代码的有3个主要注意事项。首先,游戏窗口有一个对AI程序没有用的边框,可以丢弃。变量self.bl,self.br,self.bt和self.bb分别存储窗口的左,右,顶部和底部的边框。第二,图像的边缘需要丢弃一些像素,使得图像的高度和宽度分别为7和9的倍数。其原因我们将在本系列的下一部分中进行介绍。第三,来自Windows API的位图数据分别被组织为4个8位整数BGRA,分别代表蓝色,绿色,红色和阿尔法通道。大多数python图像库都需要像RGB这样的3个通道来显示图像。 GetScreenImg中的最后一行会反转通道的顺序并丢弃Alpha通道,这里没有使用。


    3.使用并行


    由于游戏需要不停地捕捉画面图像,所以我们会把捕捉程序单独写进一个线程,并以异步和线程安全的方式为其他线程提供一个读取图像的接口。这样一来画面图像就总是可以即时获取。这一操作可以通过线程库中的线程和锁定对象来实现。


    27.png

    (点击查看大图)


    4.结果


    为了对新的代码进行计时,应该在ScreenUpdateT函数中进行测量。这里有一个快速但不考虑后果的方法,最终计时程序如下:


    28.png

    29.png

    (点击查看大图)


    时间变快了一个数量级。 现在,AI处理速度的理论最大值大约为64 FPS。 主AI程序使用与以下代码相似的ScreenViewer类型的数据成员访问画面图像。


    30.png

    (点击查看大图)


    本系列的最后一部分将介绍如何使用卷积神经网络(CNN)来处理画面的图像以更新AI的状态。(小编注:终于快完了...)



    第五部分:基于TensorFlow的CNN实时障碍物和敌对目标检测


    本节是文章的第五小节:AI Plays Path of Exile Part 5: Real-Time Obstacle and Enemy Detection using CNNs in TensorFlow


    原post链接

    https://nicholastsmith.wordpress.com/2017/08/25/poe-ai-part-5-real-time-obstacle-and-enemy-detection-using-cnns-in-tensorflow


    正如我们在本系列第一部分中说的那样,AI程序会获取游戏的屏幕截图,并使用它来进行预测,以更新其内部状态。这部分中,我们将讨论从游戏画面获取视觉输入,并对信息进行分类和识别的方法。我已经把源码放到了我的github(https://github.com/nicholastoddsmith/poeai)上,开心O(∩_∩)O~~


    1.分类系统架构图


    31.png

    图12:AL逻辑流程图


    回忆一下第三部分的文章,移动地图维持了一个从3D点到标签的字典。例如,在给定时间内,机器人在内部地图上可能具有表3所示的数据。


    世界点坐标 & 投影点

    32.png

    表3:内部地图


    回忆一下第二部分的内容,投影地图类允许画面上的任何像素映射到3D坐标(假设玩家总是在xy平面上,然后该3D坐标会被量化为某个任意精度,让AI的世界地图变成均匀间隔网格的点)。


    因此,我们需要的是能够识别屏幕上的给定像素到底是障碍物的一部分、敌人还是物品等的方法。这个任务本质上是目标检测。而实时目标检测其实是一个困难且计算复杂度很高的问题。 这里我们会介绍一种简化的方案,可以在性能和精度之间实现很好的平衡。


    为了简化目标检测任务,游戏画面被划分成相等大小的矩形区域。对于800×600分辨率的画面,我们选择由m = 7行和n = 9列组成的网格。从画面的底部,左侧和右侧边缘分别移除十二个,四个和四个像素,使得所得到的尺寸(792和588)能够分别被9和7整除。因此,屏幕网格中的每个矩形的宽度和高度分别为88和84像素。图2展示出了使用上述方案分割的游戏画面图像。


    33.jpg

    图13:游戏画面分块


    判断画面单元格内是否包含障碍物或开放的分类任务使用了一个卷积神经网络(CNN)。障碍物意味着有一些占用单元格的东西,使得玩家不能站在那里(例如巨石)。开放和闭合单元格的实例如图3所示。

    34.png

    图14:图像单元格标签


    识别物品和敌人的任务第二次使用了CNN。给定画面上的单元格,CNN将单元格分类为包含敌人,物品还是什么也不包含。


    为了只瞄准活着的敌人,判断是否发生移动的二进制分类器第三次使用了CNN。 给定画面上的单元格,第三个CNN确定单元格中是否发生移动。只有包含移动的单元格才能传入第二个CNN。这个CNN然后预测这些单元格是否包含物品或敌人。通过在连续画面截图中切换物品的突出显示来检测物品标签的移动。


    用于移动检测的图像数据是通过快速连续地捕获画面的2帧图像并且仅保留图像中显著不同的区域得到的。这是使用numpy.where函数实现的(16是任意选择的阈值)。


    35.png


    总而言之,从游戏画面中捕获的截图将输入到3个CNN中的每一个之中。第一个CNN检测画面单元格中的障碍物。然后在运动图中相应地标记画面上每个单元格内的3D网格点。内部地图保留每个单元格的预测结果,并在查询单元格时报告预测最频繁的类别。第二和第三个CNN需要联合使用以检测敌人和物品。


    2.数据集


    使用ScreenViewer类获取的画面截图,来手动构建训练数据集。目前,该数据集仅包含游戏行为4中的“Dried Lake”级数据。数据集由11个文件夹中的14,000多个文件组成,大小为164MB。 数据集的截图如图4所示。


    36.jpg

    37.jpg38.jpg

    39.jpg

    图15:训练数据集


    在数据集中,Closed文件夹中的图像是包含障碍物的单元格。 第一个CNN使用文件夹Closed,Open和Enemy。第二个CNN使用文件夹Open,Enemy和Item。 第三个CNN使用文件夹Move和NoMove。


    3.训练


    AI采用了稍微适度的CNN架构,卷积和池化层的两个序列之后是3个全连接层。该架构如下图5所示。


    40.png


    图16:CNN架构

    match

    整个数据集大约20到30次epochs交叉验证的准确率在从中升高到90%之间。 通过从训练数据中随机抽取大小为32的batch来执行epochs,直到绘制出适当数量的样本。 NVIDIA GTX 970的培训大概需要5到10分钟。训练在NVIDIA GTX 970上大概花费5到10分钟。


    4.使用并行以获得更好的表现


    为了提高AI的性能,CNN检测要并行执行。这个程序允许加速,因为numpy和TensorFlow代码避免了普通Python代码的全局解释器锁定问题。针对敌方分类线程的启动代码如下。


    41.jpg

    (点击查看大图)


    42.png

    图17:线程逻辑组织


    因此,并行执行分类,并且使用互斥锁以线程安全的方式将包含预测的数据成员提供给主线程。图6说明了线程和互斥锁的逻辑组织。在图中,ecp和pct分别是包含敌方单元格位置和预测单元格类型的Bot类的数据成员。


    5.结果


    下面这个6分钟多的视频对该项目进行了总结,并且其中有长达四分钟的时间展示了AI如何玩流放之路(PoE)。


    43.jpg

    图18:PoE  AI素材


    视频地址(需翻墙):https://youtu.be/UrrZOswJaow


    更多最新游戏AI的视频可以到作者的Youtube主页观看。

    https://www.youtube.com/channel/UCdkASWTlm-9EuAdZmbhkxgQ 


    看完作者这么用心的教程,你心动了吗?还不赶紧clone一份~



    作者介绍:Nicholas T Smith,从事AI和机器学习软件开发,加利福尼亚州立大学计算机硕士毕业生。


    原文链接:

    https://nicholastsmith.wordpress.com/2017/08/25/poe-ai-part-5-real-time-obstacle-and-enemy-detection-using-cnns-in-tensorflow/

    Github地址:https://github.com/nicholastoddsmith/poeai


    打开微信“扫一扫”,打开网页后点击屏幕右上角分享按钮

    账号登录

    重置密码

    还没有账号?立即注册>

    账号注册

    已有账号?立即登录>注册企业会员

    重置密码

    返回

    绑定手机