游戏开发中的人工智能(十一):规则式 AI
接上文 游戏开发中的人工智能(十):模糊逻辑
本文内容:技术上而言,有限状态机和模糊逻辑都落在基于规则的方法这个大伞之下。本章将谈这些方法,以及其他变化的方法。
规则式 AI
本章我们要研讨基于规则的 AI 系统。基于规则的 AI 系统可能是真实世界和游戏软件 AI 中最为广泛使用的 AI 系统了。规则系统最简单的形式由一连串的 if-then 规则组成,用来推论或行动决策。从形式上来说,在第九章的有限状态机中,已经看过规则系统的一种形式:我们用规则处理状态的转换问题。第十章谈到模糊逻辑时,也看过另一种规则系统(模糊规则)。
规则系统基础
规则系统有两个主要的部分,一个是工作记忆,另一个是规则记忆。
工作记忆储存已知的游戏世界信息,这部分是动态的。规则记忆储存设游戏设计师设计的的规则。当工作记忆符合规则记忆的某一条规则时,相应的行动就会被触发。或者,规则记忆中的规则也能修改工作记忆的内容。
为了说明规则系统,我们举个实时战略模拟游戏中科技树的例子。在实时战略模拟游戏中,玩家必须训练农民,建立设施以及收割农作物。与此同时计算机对手也会追踪玩家当前的科技状态进行评估并推论,更新自己的科技。玩家也可以以同样的方式评估计算机对手的科技状态。因此,玩家和计算机都必须排除侦察兵,收集信息,根据所收集到的信息做推论。(可以利用简单的规则系统达到这种效果)。图11-1 说明了科技树的构成。
例11-1 是实时策略游戏科技树的工作记忆内容。
//例11-1:工作记忆示例enum TMemoryValue(Yes,No,Maybe,Unknown);TMemoryValue peasants; //农民
TMemoryValue Woodcutter; //伐木工
TMemoryValue Stonemason; //石匠
TMemoryValue Blacksmith; //铁匠
TMemoryValue Barracks; //兵营
TMemoryValue Fletcher; //箭工
TMemoryValue WoodWalls; //木栅栏
TMemoryValue StoneWalls; //石墙
TMemoryValue Cavalry; //骑兵
TMemoryValue FootSoldier; //步兵
TMemoryValue Spearman; //矛兵
TMemoryValue Archer; //弓箭手
TMemoryValue Temple; //庙宇
TMemoryValue Priest; //僧侣
TMemoryValue Crossbowman; //十字弓箭手
TMemoryValue Longbowman; //长弓箭手
就此例而言,我们让工作记忆里的每个元素都以 TMemoryValue 类型声明,而且可以取下列四个值之一:Yes、No、Maybe 或 Unknown。主要目的是,让计算机对手知道当前玩家对手的科技状态。Yes 表示玩家有某种科技,No 表示没有。如果玩家满足所有获得某种科技的条件,但其状态尚未被侦察兵确认,则其值是 Maybe。如果计算机不知道玩家对某科技的能力,则取值 Unknown。
计算机可以收集玩家当前科技状态的事实,做法是派出侦察兵,并做观察。例如,如果计算机派出一名侦察兵,而侦察兵看见玩家建了庙宇,则 Temple 设为 Yes。不过在此之前,使用一组 if-then 规则,在侦测兵确认之前,计算机能根据既有事实推论玩家的科技状态。例如,看图11-1 ,如果玩家有伐木工和石匠,则有能力建庙宇,则 Temple 的值会是 Maybe。如例11-2 所示。
例11-2:庙宇规则示例if(Woodcutter==Yes && Stonemason==Yes && Temple==Unknown)Temple=Maybe;
推论也可以以反推的方式得到。例如,如果玩家被观察到有僧侣,则计算机可以推论,玩家一定有庙宇,因此,也一定有兵营、伐木工以及石匠。如例11-3 所示。
//例11-3:僧侣规则示例if(Priest==Yes)
{Temple=Yes;Barracks=Yes;Woodcutter=Yes;Stonemason=Yes;
}
根据图11-1 的科技树还可以写出许多规则,例11-4 是可以写出的其他规则。
//例11-4:其他规则示例if(Peasants==Yes && Woodcutter==Unknown)Woodcutter=Maybe;
if(Peasants==Yes && Stonemason==Unknown)Stonemason=Maybe;
…
如前所述,就此例而言能写的规则不止这些,你可以开发更多规则,包含如图11-1 所示的所有可能科技。思路是:你可以写这类规则,并在游戏中不断执行(GameCycle 时),以保持计算机对手看待玩家科技能力的最新图像,以决定如何部署攻防兵力。
此例让你大致了解规则系统的运作方式,实际上就是一组 if-then 规则。但是,注意,开发人员经常不用本节所用的 if 语句建构规则系统,因为直接把 if 语句写在程序里,会让某种推论难以达到。开发人员时常使用描述语言或 shell 语言,使他们能建立规则并予以修改,而不用修改源代码再重新编译。
对战游戏攻击预测
此例中,我们的目标是,在对战游戏中,预测人类对手的下一个招式。我们想让计算机对手,能够利用玩家最近出的招式以及玩家过去所出招式的某些模式,预测玩家下次要出什么招。如果计算机可以预测下一招,就能采取适当的反击、阻挡或闪躲动作,比如往侧边跳或往后退。这会让战斗模拟游戏有更强烈的真实感,给玩家新的挑战。
为了达到这种效果,我们要实现一个有学习能力的规则系统。让每条规则加权,强化某些规则,压抑另外一些规则,借此达到学习的效果。
为了让范例能在讨论的掌控范围内,我们做一些简化工作。假定玩家的招式可以分成挥拳、下踢、上踢。
工作记忆
例11-6 是工作记忆的操作方式。
//例11-6:工作记忆enum TStrikes(Punch,LowKick,HighKick,Unknown);struct TWorkingMemory
{TStrikes strikeA; //前前次攻击TStrikes strikeB; //前次攻击TStrikes strikeC; /预测的下次攻击//可以在这里加上其他元素,比如要怎么反击等
};TWorkingMemory WorkingMemory; //全局工作记忆变量
规则
例11-7 是此例的规则类。这里我们没有直接写出 if-then 规则,我们以 TRule 对象数组表示规则记忆。
//例11-7:规则类class TRule
{public:TRule();void SetRule(TStrikes A,TStrikes B,TStrikes C);TStrikes antecedentA; //前前次攻击TStrikes antecedentB; //前次攻击TStrikes consequentC; //预测的下次攻击bool matched; //工作记忆是否与规则记忆相匹配int weight; //权值因子
};
TRule 规则类只有两个方法:SetRule( ) 和构造方法。构造方法是把 matched 赋初值 false,weight 赋为 0。我们以 SetRule( ) 设定其他成员:antecedentA、antecedentB、consequentC,由此就可以定义出一条规则。SetRule( ) 方法如例11-8 所示。
//例11-8:SetRule()方法void TRule::SetRule(TStrikes A,TStrikes B,TStrikes C)
{antecedentA=A;antecedentB=B;consequentC=C;
}
此例需要几个全局变量,第一个是 WorkingMemory,如例11-6 所示。例11-9 是其他的全局变量。
//例11-9:全局变量TRule Rules[NUM_RULES]; //存储规则记忆 TRule对象的数组,此例指定为27
int PreviousRuleFired; //存储上一次游戏循环中启动的规则索引值TStrikes Prediction; //规则系统中所作的招式预测,技术上而言并不需要,因为预测招式都会存储在工作记忆中
TStrikes RandomPrediction; //存储随机产生的预测招式,用以比较随机和我们预测的成功率int N; //存储预测次数
int NSuccess; //成功预测次数
int NRandomSuccess; //随机猜测成的次数
初始化
游戏开始时,我们必须对所有规则和工作记忆做初始化。例11-10 的 Initialize( ) 函数会完成此任务。
//例11-10:Initialize()函数void TFom1::Initialize()
{Rules[0].SetRule(Punch,Punch,Punch);…Rules[26].SetRule(HighKick,HighKick,HighKick);WorkingMemory.strikeA=sUnknown;WorkingMemory.strikeB=sUnknown;WorkingMemory.strikeC=sUnknown;PreviousRuleFired= -1;N=0;NSuccess=0;NRandomSuccess=0;UpdateForm();
}
这里我们一共有27条规则,对应出拳、下踢、上踢这三招的所有可能组合模式。例如,第一条规则 Rules[0] 可以理解成这样:
if ( WorkingMemory.strikeA == Punch && WorkingMemory.strikeB == Punch)
then WorkingMemory.strikeC = Punch
检视这些规则可以发现,任何时刻都有一条以上的规则可以吻合工作记忆中的事实。例如,如果招式A 和 B 都是出拳,则前三条规则都吻合,预测的招式可以是出拳、下踢或者上踢。此时我们用加权因子,协助我们找出要启动哪条规则。我们只用权重最高的规则。如果有两条或两条以上的规则有相同的权重,那就用最前面那一条。
预测招式
当游戏开始运行,每次玩家出招之后,我们都必须做招式预测。我们用函数 ProcessMove( ) 处理玩家出的每一招,并预测其下一招。如例11-11 所示。
//例11-11:ProcessMove()TStrikes TForm1::ProcessMove(TStrikes move)
{int i;int RuleToFire= -1;//第一块:if(WorkingMemory.strikeA == sUnknown){WorkingMemory.strikeA=move;return sUnknown;}if(WorkingMemory.strikeB == sUnknown){WorkingMemory.strikeB=move;return sUnknown;}//第二块://先处理前次预测,记录并调整权重N++;if(move==Prediction){NSuccess++;if(PreviousRuleFired != -1)Rules[PreviousRuleFired].weight++;}else{if(PreviousRuleFired != -1)Rules[PreviousRuleFired].weight--;//增加应该启动规则的权重for(i=0;i<NUM_RULES;i++){if (Rules[i].matched && (Rules[i].consequentC == move) ){Rules[i].weight++;break;}}}if(move == RandomPrediction)NRandomSuccess++;//删除旧值WorkingMemory.strikeA=WorkingMemory.strikeB;WorkingMemory.strikeB=move;//第三块://开始做新预测for(i=0;i<NUM_RULES;i++){if(Rules[i].antecedentA == WorikingMemory.strikeA && Rules[i].antecedentB == WorikingMemory.strikeB)Rules[i].matched=true;elseRules[i].matched=false;}//选出权重最高的规则RuleToFire= -1;for(i=0;i<NUM_RULES;i++){if(Rules[i].matched){if(RuleToFire == -1)RuleToFire=i;else if(Rules[i].weight > Rules[RuleToFire].weight)RuleToFire=i;}}//启动规则if(RuleToFire != -1){WorikingMemory.strikeC=Rules[i].antecedentC;PreviousRuleFired=RuleToFire;}else{WorkingMemory.strikeC=sUnknown;PreviousRuleFired= -1;}return WorikingMemory.strikeC;
}
第一块
第一块是填写工作记忆。游戏开始时,工作记忆初始化之后,任何招式出击之前,工作记忆中只有 Unknown 值,这样使无法预测的,所以我们要在玩家开始出招后,从玩家那里搜集资料。
第一招存储在 WorkingMemory.strikeA 中,而 ProcessMove( ) 返回 Unknown。第二招打出后,ProcessMove( ) 再次被调用,第二招存储在 WorkingMemory.strikeB 中,ProcessMove( ) 依旧返回 Unknown。
第二块
ProcessMove( ) 的第一块是处理前次预测,也就是上一次调用 ProcessMove( ) 后所返回的预测招式。
第二块首先要确认前次预测时候有效。ProcessMove( ) 以 move 为参数。move 是玩家最近一次出的招。如果 move 等于存储在 Predicition 的前次预测招式,那么我们的预测就是成功的。我们递增 NSuccess,以更新成功率。然后我们我们强化上次启动的规则即增加该规则的权重。如果前次预测是错的,我们则要递减前次启动的规则权重。
接下来我们查看前次随机预测是否正确,正确就递增 NRandomSuccess。最后,我们更新工作记忆中的招式,以便做新预测,即 WoringMemory.strikeB 变成
WoringMemory.strikeA,而 move 变成 WoringMemory.strikeB 。
第三块
首先我们要找出符合工作记忆中事实的规则(第一个 for 循环)。配对步骤完成后,我们要从那些吻合的规则中挑选一条出来,即冲突解决(第二个 for 循环),循环工作完成之后,选定的规则的索引值会存储在 RuleToFire 中。要实际启动规则,只需要把 Rules[RuleToFire] 的 consequentC 赋值给 WorkingMemory.strikeC 即可。
ProcessMove( ) 把要启动的规则索引值 RuleToFire 存储在 PreviousRuleFired中,下次 ProcessMove( )被调用时,会在第二块使用。最后,ProcessMove( ) 返回预测的招式。
游戏开发中的人工智能(十一):规则式 AI相关推荐
- 游戏开发中的人工智能(一):游戏人工智能简介
本系列文章对<游戏开发中的人工智能>David M.Bourg / Glenn Seemann 一书进行解读. 这本书谈了很多游戏软件 AI 的主题,内容深度适合初涉开发人员.所以,无论你 ...
- 游戏开发中的人工智能(十三):不确定状态下的决策:贝叶斯技术
接上文 游戏开发中的人工智能(十二):概率概论 本文内容:贝叶斯技术是概率技术,本章解释如何运用,以便在游戏中做决策并适应游戏. 不确定状态下的决策:贝叶斯技术 本章要介绍贝叶斯推论和贝叶斯网络,教你 ...
- 游戏开发中的人工智能(五):以势函数实现移动
接上文: 游戏开发中的人工智能(四):群聚 本文内容:靠势能移动在游戏 AI 程序中还算相当新颖.这个方法的最优越的地方在于可以同时处理追逐.闪躲.成群结队和避免碰撞等行为.我们专门研究的这个势函数叫 ...
- 游戏开发中的人工智能(十四):神经网络
接上文 游戏开发中的人工智能(十三):不确定状态下的决策:贝叶斯技术 本文内容:"神经网络"技术让游戏具有学习和适应的能力.事实上,从决策判断到预测玩家的行为,都可以应用.我们会详 ...
- 游戏开发中的人工智能(六):基本路径寻找及航点应用
接上文:游戏开发中的人工智能(五):以势函数实现移动 本文内容:游戏开发人员使用很多技术在游戏环境中寻找路径.本章要谈几种方法,包括航点应用. 基本路径寻找及航点应用 寻找路径的问题有很多不同类型.没 ...
- 游戏开发中的人工智能(二):追逐和闪躲
接上文 游戏开发中的人工智能(一):游戏人工智能简介 本文内容:讨论基本的追逐和闪躲技术,以及进级的拦截技术.我们也谈及这些技术在砖块环境和连续环境中的变化. 追逐和闪躲 本章的焦点是追逐和闪躲,这是 ...
- 游戏开发中的人工智能
前言 今天非常开心,观看cocos官方直播居然在几千人中中奖,可以买彩票了. 言归正传,所谓的人工智能,也就是大家常说的AI(Artificial Intelligence).一说到AI可能就会让人觉 ...
- Unity3D游戏开发中的人工智能AI 简单实例
今天我想说的是游戏中的人工智能.人工智能这个东西在游戏中是非常重要的,人工智能说简单了就是根据随机的数字让敌人执行一些动作或逻辑,说难了TA需要一个非常复杂的算法,本文我主要说说Unity ...
- Unity3D研究院之游戏开发中的人工智能AI
很久没有写Unity3D相关的东西了,是因为这段时间我深陷一款IOS的软件开发中.不过以后我还是会回归Unity3D游戏开发的.什么语言都在用,生活与工作都挺给力的嚯嚯.今天还是打开了久违的U ...
最新文章
- 【转】使用Xcode中的iOS SDK给iphone开发出第一个App程序
- 用html修改游戏聊天字体,前端使用自定义字体方案
- import引入json文件_关于TypeScript中import JSON的正确姿势详解
- AI 玩微信跳一跳的正确姿势:跳一跳 Auto-Jump 算法详解
- Leetcode题库 144.二叉树的前序遍历(递归 C实现)
- word无法打开请去应用商店_word文档打不开的4种解决方法
- linux没有interface文件,Linux下interface文件修改
- JS 进阶知识点及常考面试题
- java图片去掉文字,Java 移除html,图片 链接转文字
- 新冠疫情,或加速银行数字化服务转型
- (Object detection)目标检测从入门到精通——第五部分YOLO 算法
- c语言switch的作用域,你真的懂switch吗?聊聊switch语句中的块级作用域
- 老年代的更新机制_魔兽世界:60年代五大“远古”机制,这根胡萝卜,可是当年的神器...
- ESET Smart Security 3.0667与WindowsXP Sp3冲突
- JAVAFX输入法的实现
- Excel 宏编程-使用excel宏编写第一个Hello World程序实例演示!
- 《Head First设计模式》中文版 读书笔记
- 【转载】win10环境下,利用自带虚拟机hyper-v安装centos7方法详解
- 卸载软件失败:“1628:完成基于脚本的安装失败”【已解决】
- Android7.0以上 安装Ca证书