Seize the Day
游戏AI入门指南(Part 2)
原文: The Total Beginner’s Guide to Game AI 作者: Ben Sizer 译者: Anthony Han
进阶决策
虽然简单的反应型系统已十分强大,但在很多情况下还是不够完善的。有时我们想根据代理当前正在做的事情做出不同的决定,并将其作为一个条件来使用,那么就会有些不太方便。有时因为条件太多,导致无法在决策树或脚本中表达清楚。有时在决定下一步行动之前,需要先思考再评估情况将如何变化。对于这些问题,我们需要更复杂的解决方案。
有限状态机
有限状态机(Finite State Machine,简称FSM)是一个花哨的专用术语,来描述这样的东西:举例来说,某个AI代理目前处于几种可能的状态中,它可以从一个状态转换到另一个状态。而这些状态的数量有限,因此得名。现实生活中的例子如一组交通信号灯,它会从红色变成黄色,再变成绿色,然后再变回红色。不同的地方有不同的亮灯顺序,但原理是相同的——每个状态代表某种事物(例如“停止”,“前进”,“尽可能停止”等),任何时候都仅处于一种状态,并且它会根据简单的规则从一个状态过渡到另一个状态。
这非常适用于游戏中的NPC。一个警卫可能具有以下状态:
- 巡逻
- 攻击
- 逃跑
当状态改变时,你可能会想到这些规则:
- 如果警卫看到敌人,就立即攻击
- 如果警卫正在攻击但无法再看到敌人,那么返回巡逻
- 如果警卫正在攻击但受了重伤,那么开始逃跑
这个规则很简单,你可以直接把它写成硬编码的if语句,用一个变量来保存警卫的状态,并进行各种检查:查看附近是否有敌人,警卫的健康状况如何等等。但如果我们要添加更多的状态:
- 空闲(巡逻期间)
- 搜寻(刚才发现的敌人躲起来时)
- 求助(发现敌人,但因为敌人太强而无法独自作战时)
通常在每个状态下可做出的选择是有限的——例如当警卫的健康状况不佳时,他们可能不想寻找敌人。
如果最终用一长串的“if (x and y but not z) then p”来表示,就显得有些笨拙了。如果以一种通用统一的方式来实现状态之间的转换,应该会有所帮助。为此我们要考虑所有状态,并且在每个状态下,列出到其它状态的所有转换和条件。我们还要指定一个初始状态来决定在条件适用之前从哪里开始。
…游戏AI入门指南(Part 1)
原文: The Total Beginner’s Guide to Game AI 作者: Ben Sizer 译者: Anthony Han
这是一篇非常好的综述类文章,对当今的游戏AI常用技术和实现方法做了简单介绍,参考了近年来的 Game AI Pro 系列。虽然不及论文那样严谨,但全文条理清晰,通俗易懂。确实是一篇入门指南佳作。花了点业余时间翻译出来,为了分享,也希望能温故知新。
引言
本文将介绍游戏人工智能(或简称“游戏AI”)的入门概念,让读者了解使用哪些方法来处理AI问题,它们如何协同工作以及如何使用相应的语言或引擎来实现。
我们假设你具备电子游戏的基本知识,并掌握几何,三角函数等数学概念。大多数代码示例为伪代码,因此不需要特定的编程语言知识。
什么是游戏AI?
游戏AI主要关注实体根据当前条件所采取的行动。这就是传统人工智能文献所指的控制“智能代理”,代理通常是游戏中的角色,但也可以是车辆,机器人。或者更抽象的东西,例如一组实体,甚至一个国家或文明。智能代理需要在各种情况下观察周围环境,依此做出决策,并采取行动。这就是所谓的“感知/思考/行动(Sense/Think/Act)”循环:
- 感知:代理侦测到或被告知环境中可能影响其行为的事物(例如:附近的威胁,要收集的物品,要调查的兴趣点)。
- 思考:代理决定采取的应对措施(例如:考虑是否足够安全来收集物品,或者决定应该先集中精力战斗还是躲藏)。
- 行动:代理将先前的决定付诸行动(例如:沿着通向敌人或物品等的路径移动)。由于代理做出了行动,形势已经改变,因此再次重复循环。
现实世界中的AI,特别是成为新闻热点的那些,通常主要关注循环中的“感知”部分。例如,自动驾驶汽车拍摄道路的图像,结合其他数据(例如雷达和光达),并分析所看到的状况。这个过程一般是通过机器学习来完成,机器学习尤其擅长这方面,获取大量现实世界中有噪声的数据(如汽车前方的道路照片或视频)并加以分析理解,提取诸如“前方20码处有一辆汽车”这类的语义信息。这些被称为“分类问题”。
…Debian Redmine 备份与恢复
几年前装了 Debian 作为服务器,顺便把以前的 Bitnami Redmine (Windows) 迁移了过来。好多年过去也没再去干预,系统一直稳定运行,每周定时备份。最近因为考虑迁移这些工具到Docker上,重新整理一遍备份和恢复流程,以备不时之需。
系统需求
- Redmine 3.3 installed with apt on Debian 9 (stretch).
- Database: PostgreSQL
备份
配置文件
配置文件包含路径:/etc/redmine/{instance}
例如,默认实例名称为default
,则配置文件路径为/etc/redmine/{default}
。包含以下配置文件:
- configuration.yml
- database.xml
- secret_key.txt
晶体管:献给赛博朋克的十四行诗
When I first saw you up on that stage back there. It was like… everyone loved you. Everyone except for them.
《晶体管》(Transistor) 是一款由《堡垒》(Bastion)开发商 Supergiant Games 所开发的科幻题材作品。游戏讲述了主角——云堤城 (Cloudbank) 歌星红伶 (Red),躲过卡梅拉塔组织 (The Camerata) 刺杀后,意外获得了一件名叫“晶体管” (Transistor) 的武器,并以此复仇的故事。
虽然《晶体管》被官方定义为 ARPG ,但确切地说,它更应该被称作为一款 TRPG (Tactical RPG)。类似 XCOM 或者 神界:原罪 (Divinity: Original Sin) 的战斗规则 但又有所不同。首先,在行动规划阶段没有移动的次序和限制,玩家可以移动多次,并且在发动技能前后都可以移动,行动规划如同施放“时间暂停”一般。其次,个别技能会影响敌人位置,如拉近/击飞等效果。而在行动规划期间,受到此类效果影响的敌人并不会实时改变其位置,因为是规划嘛,所以需要一定的预判,游戏中会给出相应的提示。
如何选择游戏AI架构
Conference: GDC 2010
Session Name: Deciding on an AI Architecture: Which Tool for the Job?
Speaker(s): Alex Champandard, Michael Dawe, Dave Mark, Steve Rabin, Charles Rich
Track / Format: AI Summit
Video: GDC Vault - Deciding on an AI Architecture: Which Tool for the Job?
“工欲善其事,必先利其器。”
—— 《论语·卫灵公》
AI 架构的选择是 AI 程序员需要解决的最重要问题之一。这个选择将为项目奠定基础,同时也决定了未来的方向。主要的 AI 架构都有自己的优缺点,很难确定哪一个架构最适合一个项目。
本次讨论将从独特的角度探讨这个问题。每一种主流架构都有一位代言人,他们将被提供假想的游戏例子,并被要求解释为什么他们支持的架构是最合适的选择,而其他的则不合适。通过不同架构的代言人就同一问题进行讨论和分析,可以更全面地评估每种架构的优劣,并对具体项目做出更合适的建议。
…行为树入门工具包
偶然看到一个2012年 Alex Champandard 的演讲1,本想看看有什么可以借鉴,看到一半后,发现就是 Game AI Pro 的文章 《The Behavior Tree Starter Kit2》的演示版。结合代码3,对照着看一遍,有助于理解。
Motivation
行为树广泛应用于各个系统:角色,策略,小队,动画,镜头……
使用案例:
- Rockstar Games: R.A.G.E.
- Guerrilla Games:Killzone 2 & 3
- Uncharted 2
- Halo 3
- NBA ‘09
- Metro 2033
- Crysis: Warhead
- League of Legends
The Basics
v1:原型
一个基本的行为树实现:
enum Status
/**
* Return values of and valid states for behaviors.
*/
{
BH_INVALID,
BH_SUCCESS,
BH_FAILURE,
BH_RUNNING,
BH_ABORTED,
};
class Behavior
/**
* Base class for actions, conditions and composites.
*/
{
public:
virtual Status update() = 0;
virtual void onInitialize() {}
virtual void onTerminate(Status) {}
Behavior()
: m_eStatus(BH_INVALID)
{
}
virtual ~Behavior()
{
}
Status tick()
{
if (m_eStatus != BH_RUNNING)
{
onInitialize();
}
m_eStatus = update();
if (m_eStatus != BH_RUNNING)
{
onTerminate(m_eStatus);
}
return m_eStatus;
}
void reset()
{
m_eStatus = BH_INVALID;
}
void abort()
{
onTerminate(BH_ABORTED);
m_eStatus = BH_ABORTED;
}
bool isTerminated() const
{
return m_eStatus == BH_SUCCESS || m_eStatus == BH_FAILURE;
}
bool isRunning() const
{
return m_eStatus == BH_RUNNING;
}
Status getStatus() const
{
return m_eStatus;
}
private:
Status m_eStatus;
};