游戏AI

游戏AI


工具

Behaviac —— 来自腾讯的游戏 AI 开发框架

behaviac是游戏AI的开发框架组件,也是游戏原型的快速设计工具。

主要特性

  • 支持全平台,适用于客户端和服务器,助力游戏快速迭代开发
  • 编辑器可以运行在PC上,操作方便直观可靠,支持实时和离线调试
  • 编辑器可以导出xml,bson等多种格式,更可以导出C++,C#源码,提供最高效率
  • 运行时支持全平台,有C++和C#两个版本,原生支持Unity
  • 已被《天天炫斗》、《QQ飞车》、《全民突击》、《全民夺宝》、《九龙战》等游戏及其他更多预研项目使用

Serpent.AI - Game Agent Framework (Python)

Serpent.AI 中包含大量支持模块,在以游戏为开发环境时经常遇到的场景提供解决方案,同时也提供加速开发的 CLI 工具。支持 Linux、Windows 和 MacOS 。

SerpentAI 是一个 Game Agent 框架(ps:在人机对战中,为了区分玩家,通常称机器玩家为 agent ),简单而又强大。它能把任何一个游戏变成用 Python 编写的沙盒环境,供开发者在其中创造游戏 Game Agent 做实验,使用的都是开发者非常熟悉的 Python 代码。

1. 安装

安装 Anaconda 4.4.0 (Python 3.6)
1
2
3
4
5
6
7
8
# 为 Serpent.AI 创建一个 Conda 虚拟环境
conda create --name serpent python=3.6

# 为 Serpent.AI 项目创建一个目录
mkdir SerpentAI && cd SerpentAI

# 启动 Conda 虚拟环境
activate serpent
解决 ‘chcp’ 不被识别问题

看看你的PATH环境变量是否正确设置。任何系统应该在PATH上至少有以下内容:

1
;%SystemRoot%\system32;%SystemRoot%;%SystemRoot%\System32\Wbem;
第三方软件依赖
Redis

Redis 用来实现所捕获帧的内存内存储,同时也是分析事件的临时容器。

配置 WSL(可以在 Windows 10 上的 Ubuntu 的 Bash 命令提示符中运行 Redis 服务器)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 安装 Windows Subsystem for Linux
Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux

# 重启电脑

# 安装 Linux 发行版(以 Ubuntu 为例)
# 1. 打开 Microsoft Store 搜索 Ubuntu 并安装
# 2. 启动并创建 LINUX 用户名和密码

# 安装 Redis
sudo apt-get update
sudo apt-get install redis-server -y
sudo sed -i -e 's/127.0.0.1/0.0.0.0/g' /etc/redis/redis.conf
sudo service redis-server restart
Tesseract

Serpent.AI 内含有一些 OCR 功能,这样我们就可以从游戏帧中读取文字了,我们把这项重任交给 Tesseract 去完成

1
2
3
4
5
# 1. 访问 https://github.com/UB-Mannheim/tesseract/wiki
# 2. 下载版本为 3 的 .exe 可执行文件
# 3. 运行图形化安装器 (务必记录下安装路径!)
# 4. 在你的环境变量 %PATH% 中为 tesseract.exe 添加刚才你记录的路径
# 5. 打开 Anaconda 提示符,使用 tesseract --list-langs 可以测试 Tesseract 是否成功安装
开始安装 Serpent.AI

一旦上面的所有组件都被正确安装与配置,你就可以开始安装 Serpent.AI 框架了。

1
2
3
4
5
6
7
8
9
10
# 返回你原来为自己创建的 Serpent.AI 项目目录,确保你在 Conda 虚拟环境下
cd SerpentAI
activate serpent

# 安装msgpack,解决distributed 1.21.8 requires msgpack, which is not installed.问题
pip install msgpack
pip install SerpentAI

# 然后运行 serpent setup 来自动安装余下的依赖包
serpent setup

2. Hello World

游戏准备

启动路径:

1
J:/Words/trunck/proj.win32/Debug.win32/Words/Words.exe

运行模式选窗口模式,记下窗口的识别名称:游戏的窗口名。

创建一个游戏插件
1
2
3
4
5
6
7
8
9
cd SerpentAI
activate serpent

# 运行 serpent generate game
serpent generate game

# 依次填入关键词:
# Words(游戏名)
# executable(可执行)
配置游戏插件

编辑文件:

1
H:\SerpentAI\plugins\SerpentWordsGamePlugin\files\serpent_Words_game.py

使用 Words 替换掉 WINDOW_NAME
使用 J:/Words/trunck/proj.win32/Debug.win32/Words/Words.exe 替换掉 EXECUTABLE_PATH

启动游戏
1
2
3
serpent launch Words
# 游戏将会开始运行。如果你的游戏被移动到了屏幕左上角的话,这说明 Serpent.AI 可以定位到这个游戏窗口。
# 让游戏在那里运行着不要管它。
创建游戏代理插件

获取游戏画面帧并与之交互

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 运行 serpent generate game_agent
serpent generate game_agent

# 填入关键词:
# Words(游戏名)

# 编辑 H:\SerpentAI\plugins\SerpentWordsGameAgentPlugin\files\serpent_Words_game_agent.py
# 更改 handle_play 函数内容,使之看起来像这样:
def handle_play(self, game_frame):
print("Hello World!")

# 运行游戏代理
serpent play Words SerpentWordsGameAgent
# 一切顺利的话,你会在你的终端中开始看到一连串输出的 "Hello World!" 字样
更加可视化的方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 打开另一个终端,运行 serpent visual_debugger
serpent visual_debugger

# 现在把 handle_play 改为如下这样:
def handle_play(self, game_frame):
print("Hello World!")

for i, game_frame in enumerate(self.game_frame_buffer.frames):
self.visual_debugger.store_image_data(
game_frame.frame,
game_frame.frame.shape,
str(i)
)

# 运行游戏代理
serpent play SuperHexagon SerpentSuperHexagonGameAgent

# 这样你应该就已经能在可视化调试器中看到循环显示的逐帧画面了,差不多跟终端中显示新的 "Hello World!" 字段的节奏一致。

3. 训练一个上下文分类器

解构游戏

希望所创建的上下文分类器能识别四种不同的状态:

  • main_menu 主菜单
  • level_select 关卡选择
  • game 游戏中
  • game_over 游戏结束
启动游戏
1
2
3
cd SerpentAI
activate serpent
serpent launch Words
捕捉上下文游戏帧图像

假设我们需要从 main_menu 主菜单捕捉图像的话:

1
2
3
4
5
6
7
8
# 手动进入 main_menu 主菜单界面
# 启动一个帧捕捉器捕捉一些图像帧
serpent capture context Words 0.5 main_menu

# 在捕获器运行的时候,尽量在不离开该状态的前提下多进行一些操作,以产生尽可能多的视觉变化
# 等到至少有 200 帧被捕获的时候(越多越好!),单击终端,让框架将内容保存到磁盘,然后按 Ctrl-C 结束该捕捉器
# 如果你到 datasets/collect_frames_for_context/main_menu 目录下查看一番,你应该可以看到这些帧了
# 重复该操作,直到你捕获完所有的其他场景为止。(你至少需要两个场景样本来开始训练)
训练上下文分类器

接下来,你将执行的是你这辈子能执行的最简单的机器学习训练操作:

1
2
3
4
5
6
7
8
# 解决softmax() got an unexpected keyword argument 'axis'问题
pip install keras==2.1
serpent setup ml

# 训练模型
serpent train context 15
# 该命令会把你所收集的每个场景的图像帧拆分成训练集及验证集,并进行一次 15 个周期的模型训练。
# 如果你使用的是 GPU 加速版本的 Tensorflow,训练操作将只需要几分钟的时间。不然的话就去喝杯咖啡吧

4. 测试

1
2
3
4
5
6
7
cd SerpentAI
activate serpent
serpent launch YouMustBuildABoat
serpent play YouMustBuildABoat SerpentYouMustBuildABoatGameAgent

serpent launch AIsaac
serpent play AIsaac SerpentAIsaacGameAgent

基础概念

游戏人工智能开发之6种决策方法

0. 人工智能遵循着:感知->思考->行动

决策方法:

  • 有限状态机(Finite-State Machines)
  • 分层状态机(Hierarchical Finite-State Machines)
  • 行为树(Behavior Trees)
  • 效用系统(Utility Systems)
  • 目标导向型行动计划(Goal-Oriented Action Planners)
  • 分层任务网络(Hierarchical Task Networks)

1. 有限状态机

简单又快速,使用上强大、灵活、计算开销小。

状态机状态类:FSMTransition列表为将要转到的所有可能的状态。

1
2
3
4
5
6
7
class FSMState
{
virtual void onEnter();
virtual void onUpdate();
virtual void onExit();
list<FSMTransition> transitions;
};

每个状态还存储着FSMTransition的类,代表能从当前状态可以转到的状态

当转换条件满足时isValid()返回true,getNextState()返回将要转到的状态,onTransition()是状态之间转换的过渡

1
2
3
4
5
6
7
class FiniteStateMachine
{
void update();
list<FSMState> states;
FSMState* initialState;
FSMState* activeState;
}

最后是有限状态机类FiniteStateMachine

包含所有状态的列表states,initialState为初始状态,activeState为当前状态

1
2
3
4
5
6
7
class FiniteStateMachine
{
void update();
list<FSMState> states;
FSMState* initialState;
FSMState* activeState;
}

伪代码如下:

1
2
3
4
5
6
在activeState.transtitions中循环调用isValid(),检测是否符合达到下一状态的条件
如果符合转换条件
  调用activeState.onExit(),退出当前状态
  设置activeState 为 validTransition.getNextState(),把当前状态赋值为下一状态
  调用activeState.onEnter(),下一状态的开始
如果不符合转换条件,调用activeState.onUpdate(),让NPC执行当前状态需要做的事

2. 分层有限状态机

把多个状态机归为一层

分层有限状态机增加了一个滞后,当我们第一次进入嵌套状态->看守建筑时,历史状态H表示为初始状态,之后历史状态H表示为最近处在的一个状态

3. 行为树

行为树是树型结构的,每个节点都代表了一个行为,每个行为都可以有子行为。

所有行为都有一个先决条件,就是产生的这些行为的条件。

整个算法先从树的根部开始,然后开始检查每一个先决条件。树的每一层只可以执行一个行为,所以当一个行为正在执行,它的兄弟节点都不会被检查,但是它们的子节点还是要检查的。

伪代码:

1
2
3
4
5
6
7
8
9
使根节点为当前节点
当存在当前节点
  判断当前节点的先决条件
  如果先决条件返回true
    把节点加到执行清单
    使子节点为当前节点
  否则
    使兄弟节点为当前节点
执行执行清单上的所有行为

缺点:

  • 决策树做的选择并不一定是最优的,结果也不一定是我们想要的。
  • 而且决策每次都要从根部往下判断选择行为节点,比状态机要耗费时间。
  • 例如:一个农民要收割作物,敌人出现了,农民逃跑,逃出了距离敌人的一定范围之后,又回去收割作物,走到敌人的范围又逃出,这样来回往复,是一个弊端

4. 效用系统

当需要选择新的行为时,我们通过分数(上面说的各种程度)来选择相对最优的选择,或者加上一个随机值再选择,使得接近优选的几个选择都有一定几率(几率可根据所加随机值决定)被选中。

5. 目标导向型行动计划

GOAP是一个反向链接搜索,从要实现的目标开始,找到什么动作能实现目标,在寻找刚才动作的先决条件,一直往前推,知道达到你的当前(初始)状态。这种反向链接搜索替代了启发式的前向链接搜索。

伪代码:

1
2
3
4
5
6
7
8
9
把目标加到未解决事件列表
对于每个未解决事件(for)
  移除这个未解决事件
  找到达成事件的动作
  如果动作的先决条件已经满足
    增加动作到计划中
    往回推需要达到先决条件的动作到计划中
  否则
    添加该先决条件到未解决事件中

6. 分层任务网络

HTN也是寻找一个计划来让AI执行,原理是最高级的任务分解成更小的任务再继续分解直到我们解决问题。HTN与GOAP相反,HTN是前向链接搜索,是从当前状态一直推到目标状态,向前推直到问题解决。

我们有两种任务:原始任务和复合任务。

原始任务是可以只解决问题的任务,也就是可以直接达到目标的任务。

复合任务是高级别的任务,可以看作方法。一个方法是一组任务可以完成复合任务,这一组任务是由先决条件决定的。复合任务让HTN推断出世界并且决定该做什么动作。

伪代码:

1
2
3
4
5
6
7
8
9
10
增加根复合任务到分解列表中
对于每个在我们分解列表中的任务(for)
  移除任务
  如果任务是复合任务
    找到满足当前条件状态并且能处理该复合任务的方法
    如果该方法找到了,增加方法的任务到分解列表中
    如果没找到,恢复到之前分解任务的状态中
  如果任务是原始任务
    在当前状态下执行任务
    增加任务到最终计划列表

行为树

1. 什么是行为树

行为树,英文是Behavior Tree,简称BT,是由行为节点组成的树状结构

在BT中,节点是有层次(Hierarchical)的,子节点由其父节点来控制。

每个节点的执行都有一个结果(成功Success,失败Failure或运行Running),该节点的执行结果都由其父节点来管理,从而决定接下来做什么,父节点的类型决定了不同的控制类型。

节点不需要维护向其他节点的转换,节点的模块性(Modularity)被大大增强了。

主要优势之一就是其更好的封装性和模块性,让游戏逻辑更直观,开发者不会被那些复杂的连线绕晕。

2. 例子

image

3号Sequence节点有3个子节点,分别是:

  • 4号Condition节点
  • 5号Action节点
  • 6号Wait节点

而3号节点的父节点是2号的Loop节点。

先补充下各节点类型的执行逻辑:

  • 序列(Sequence)节点:顺序执行所有子节点返回成功,如果某个子节点失败返回失败。
  • 循环(Loop)节点:循环执行子节点到指定次数后返回成功,如果循环次数为-1,则无限循环。
  • 条件(Condition)节点:根据条件的比较结果,返回成功或失败。
  • 动作(Action)节点:根据动作结果返回成功,失败,或运行。
  • 等待(Wait)节点:当指定的时间过去后返回成功。

执行说明:

1
2
3
4
如果4号条件节点的执行结果是成功,其父节点3号节点则继续执行5号节点(对于持续运行一段时间的Fire动作,其执行结果持续返回"运行",结束的时候返回"成功")
  如果5号动作节点返回成功,则执行6号等待节点(对于持续运行一段时间的Wait动作,其执行结果持续返回"运行",当等待时间到达的时候返回"成功")
    如果6号节点返回成功,则3号节点全部执行完毕且会返回成功,那么2号节点继续下个迭代。
如果4号条件节点的执行结果是失败,其父节点3号节点则返回失败不再继续执行子节点,并且2号节点继续下个迭代。

当节点持续返回”运行”的时候,BT树的内部”知道”该节点是在持续”运行”的,从而在后续的执行过程中”直接”继续执行该节点,而不需要从头开始执行,直到该运行状态的节点返回”成功”或”失败”,从而继续后续的节点。从外面看,就像”阻塞”在了那个”运行”的节点上,其父节点就像不再管理,要一直等运行的子节点结束的时候,其父节点才再次接管

持续返回”运行”状态的节点固然优化了执行,但其结果就像”阻塞”了BT的执行一样,如果发生了其他”重要”的事情需要处理怎么办?

使用前置

可以添加前置附件,并且”执行时机”设为Update或Both,则在每次执行之前都会先执行前置里配置的条件

使用Parallel节点

条件,一边执行动作”,该条件作为该动作的”Guard”条件。当该条件失败的时候来结束该处于持续运行状态的动作节点

使用SelectorMonitor节点

SelectorMonitor是一个动态的选择节点,和Selector相同的是,它选择第一个success的节点,但不同的是,它不是只选择一次,而是每次执行的时候都对其子节点进行选择。

使用Event子树

任何一个BT都可以作为事件子树,作为event附加到任何的一个节点上(用鼠标拖动BT到节点)。当运行该BT的时候,如果发生了某个事件,可以通过Agent::FireEvent来触发该事件,则处于running状态的节点,从下到上都有机会检查是否需要响应该事件,如果有该事件配置,则相应的事件子树就会被触发。

3. 总结

行为树的基本概念:

  • 执行每个节点都会有一个结果(成功,失败或运行)
  • 子节点的执行结果由其父节点控制和管理
  • 返回运行结果的节点被视作处于运行状态,处于运行状态的节点将被持续执行一直到其返回结束(成功或失败)。在其结束前,其父节点不会把控制转移到后续节点。

游戏开源项目

著名游戏相关的开源项目

1. 商业游戏

  • id Software · GitHub:历来各个游戏的完整代码

  • Unreal Tournament:用开源方式开发

2. 游戏引擎/框架

  • Unreal Engine 4官网

  • cocos2d/cocos2d-x · GitHub(@王哲)

  • cloudwu/skynet · GitHub(云风)

3. 渲染引擎

  • KlayGE游戏引擎(@龚敏敏)

  • OGRE – Open Source 3D Graphics Engine

4. 物理引擎

  • Box2D | A 2D Physics Engine for Games:Blizzard 的程序员的个人项目

  • bulletphysics/bullet3 · GitHub:作者曾是Havok、Sony、AMD和现在Google的程序员。

  • PhysX Source on GitHub

5. 其他

所有项目使用的服务器框架:cloudwu/skynet · GitHub

2D项目使用的渲染引擎:ejoy/ejoy2d · GitHub

一个protobuf的替代品:cloudwu/sproto · GitHub

简悦科技的开源项目:ejoy/projectlist · GitHub

Wikipedia上的一些游戏开源项目列表: