终于等够了三个月,杂志的约定已经到期,可以把这篇文章发表到网络跟大家分享。本文原发表于《游戏创造》杂志www.chinagcn.com,如蒙转载,请保留原文和本声明完整,并注明转载自恋花蝶的博客:http://blog.csdn.net/lanphaday
用遗传算法加强足球游戏的人工智能<o:p></o:p>
广州网易互动娱乐 赖勇浩<o:p></o:p>
<o:p></o:p>
项目背景<o:p></o:p>
一直都想用遗传算法(Genetic Algorithms)实现足球游戏的人工智能,但因为实现一个足球游戏的对战平台太过于繁琐而没有动手。直到在《Programming Game AI by Example》一书中看到一个SimpleSoccer的demo(以下简称demo),实现了一个red-blue两队进行机器与机器对抗的简单足球游戏。在读过它的源码之后,我决定在demo上进行二次开发——为它加入遗传算法,实验遗传算法在实时战略游戏(RTS)性质的体育游戏中的威力。<o:p></o:p>
demo的架构非常好,采用了状态机来实现游戏流程,并分开计算游戏决策。因此加入遗传算法非常容易,只要在原来的状态机中增加一两个状态即可。red-blue两个队伍相互对抗,每队有五位球员,其中一位是守门员。这个demo的足球规则是简化的,除了只有五个球员外,没有手球也没有越位等规则,甚至连边界球都没有——球碰到边界就反弹回球场。简化的规则有利于我们简化实验的过程,不必把很多精力花费在过于复杂的规则上。<v:shapetype o:spt="75" coordsize="21600,21600" filled="f" stroked="f" id="_x0000_t75" path="m@4@5l@4@11@9@11@9@5xe" o:preferrelative="t"> </v:shapetype>
<v:shapetype o:spt="75" coordsize="21600,21600" filled="f" stroked="f" path="m@4@5l@4@11@9@11@9@5xe" o:preferrelative="t"></v:shapetype>
<v:shapetype o:spt="75" coordsize="21600,21600" filled="f" stroked="f" path="m@4@5l@4@11@9@11@9@5xe" o:preferrelative="t"><v:stroke joinstyle="miter"></v:stroke><v:formulas><v:f eqn="if lineDrawn pixelLineWidth 0"></v:f><v:f eqn="sum @0 1 0"></v:f><v:f eqn="sum 0 0 @1"></v:f><v:f eqn="prod @2 1 2"></v:f><v:f eqn="prod @3 21600 pixelWidth"></v:f><v:f eqn="prod @3 21600 pixelHeight"></v:f><v:f eqn="sum @0 0 1"></v:f><v:f eqn="prod @6 1 2"></v:f><v:f eqn="prod @7 21600 pixelWidth"></v:f><v:f eqn="sum @8 21600 0"></v:f><v:f eqn="prod @7 21600 pixelHeight"></v:f><v:f eqn="sum @10 21600 0"></v:f></v:formulas><v:path o:extrusionok="f" o:connecttype="rect" gradientshapeok="t"></v:path><o:lock v:ext="edit" aspectratio="t"></o:lock></v:shapetype><v:shape id="_x0000_i1025" type="#_x0000_t75" style="WIDTH: 415.5pt; HEIGHT: 237pt"><v:imagedata src="https://p-blog.csdn.net/images/p_blog_csdn_net/lanphaday/209780/t_blog_ss_board.jpg" o:title="sc"></v:imagedata></v:shape><o:p></o:p>
图一<o:p></o:p>
在demo的实现中,球场被分割为18块大小相等的区域(见图一)。每一个球员都一个属于自己的区域(称为HomeRegion),如图一中blue队的10号在自己的HomeRegion(Region5)中处于Wait状态(球员的状态之一)。当一个球员不处于进攻状态(Attacking)、助攻(SupportAttacker)、逐球(ChaseBall)、运球(Dribble)、踢球(KickBall)及返回(ReturnToHomeRegion)时,他就进入Wait状态——等待球队发出的下一个行动指令。显然,就像人类进行足球比赛时需要排兵布阵一样,demo中球员站在哪个位置也相当重要,能否组织起有效的进攻或者防守,决定因素之一就是在合适的位置有没有球员可以快速有效地执行命令。在书中自带的demo中,球员的站位都是固定的,因此难以组织有效的进攻和防守,在某一时间段内容易形成一边倒的局势。使用遗传算法来对球员的站位进行决策分析,可以找出对当前局势就有利的位置编排方案。从而使得球队与球队之间的对抗趋于激烈、策略更加有效、攻守都更精彩。<o:p></o:p>
<o:p></o:p>
遗传算法概述<o:p></o:p>
遗传算法因为它在解决许多生产、生活中的问题上的卓越性能而经久不衰。随着计算机的计算能力日益增强和玩家对游戏中的人工智能的强烈需求,目前在单机游戏中已经开始应用遗传算法、人工神经网络等现代优化计算方法来增强游戏中的人工智能,并且形成了趋势。可见以后为加强机器的对抗性能,遗传算法、人工神经网络等都会越来越多地应用到游戏中。<o:p></o:p>
遗传算法是模拟自然界中的生物对自然界的适应而不断进化这一客观事实的算法。为了解决某一个问题,在遗传算法中,我们虚拟一个物种(即解的表现形式或者称为解的编码),并将其放到“自然环境”中天下繁殖、进化,根据优胜劣汰、适者生存的自然法则,繁衍若干代之后,种群中的佼佼者将非常适应“自然环境”,这个佼佼者就是我们求得的解了。关于生物学与遗传算法之间的概念的对应关系可以用表一的形式来表示:<o:p></o:p>
生物遗传概念<o:p></o:p> | 遗传算法中的作用<o:p></o:p> |
适者生存<o:p></o:p> | 在算法停止时,最优目标值的解有最大的可能性被留住<o:p></o:p> |
个体<o:p></o:p> | 解<o:p></o:p> |
染色体<o:p></o:p> | 解的编码(二进制形式或者十进制形式的串即向量,或者字符串)<o:p></o:p> |
基因<o:p></o:p> | 解中每一分量的特征(如各分量的值)<o:p></o:p> |
适应性<o:p></o:p> | 适应函数的返回值<o:p></o:p> |
群体<o:p></o:p> | 选定的一组解(其中解的个数为群体的规模)<o:p></o:p> |
种群<o:p></o:p> | 根据适应函数选取的一组解<o:p></o:p> |
交配<o:p></o:p> | 通过交配原则产生一组新解的过程<o:p></o:p> |
变异<o:p></o:p> | 编码的某一分量发生变化的过程<o:p></o:p> |
表一<o:p></o:p>
遗传算法的流程图如图二所示:
<o:p></o:p>
<v:shape id="_x0000_i1026" type="#_x0000_t75" o:ole="" style="WIDTH: 254.25pt; HEIGHT: 336.75pt"><v:imagedata src="https://p-blog.csdn.net/images/p_blog_csdn_net/lanphaday/209780/t_blog_ss_lc.jpg" o:title=""></v:imagedata></v:shape>
图二<o:p></o:p>
遗传算法运行时,先生成初始群体(通常是随机产生一定数量的个体,这个数量就是群体的大小);然后让群体繁殖下一代,繁殖的方式有交叉、复制和变异;经过繁殖后群体的数量增加,然后使用评估模块对每一个个体进行评估;如果群体中最佳个体已经足够优秀,那就跳出循环,返回最佳个体;否则判断是否已经繁殖了预定的代数,如果是就返回最佳个体,如果不是则淘汰一部分劣质个体并进入下一轮繁殖循环直至结束。<o:p></o:p>
在遗传算法的实现中,最重要的主要有三点:一是染色体的编码,即一个新物种怎么样来表示它,通常染色体是问题的一个可能解的特定格式的表示,通常以二进制或者十进制的方式编码;二是为染色体实现交叉、复制、变异等算子;三是估值模块的编写。下面以这三点为中心,谈谈demo中的遗传算法实现。<o:p></o:p>
<o:p></o:p>
染色体编码<o:p></o:p>
染色体编码的方式有很多种,常见的是二进制方法和十制字方式,也有字符串方式的。如著名的旅行商问题(TSP)里,假设有20个城市以[0…19]编码,那么[7,6,9,8…19,3,0,4]这个包含20个元素的序列A就可以看作是一个染色体,每一个元素Aj(0<=j<20)就是染色体的一个基因。这个染色体可以解码为从编号为7的城市出发,到达城市6、城市9等等,最后到达城市4完成20个城市的遍历。显然,这个序列是TSP的一个可能解,因此染色体就是问题的可能解的表示方式。回到我们的足球游戏中来,我们期望获得某一队的球员的合适站位,那么如果我们把四个球员以[0…3]编号(因为守门员不应离开禁区所以不必考虑他的位置),那序列B[14,11,12,6]就是一个站位方案,表示0号球员站在ID为14的Region中,1号球员站在ID为11的Region中,等。序列B叫做一个可能解,序列B的编码方式即是我们染色体的编码方式——十进制编码方式的一个序列。依照这个规则,我们编写代码如下:<o:p></o:p>
typedef unsigned int Genetype;<o:p></o:p>
class Chromosome{<o:p></o:p>
private:<o:p></o:p>
std::vector<Genetype> m_Geneme;<o:p></o:p>
int m_iScore;<o:p></o:p>
public:<o:p></o:p>
Chromosome();<o:p></o:p>
~Chromosome(){};<o:p></o:p>
const std::vector<Genetype>& GetGeneme()const{return m_Geneme;}<o:p></o:p>
void SetScore(int iScore){m_iScore = iScore;}<o:p></o:p>
friend void Intercross(const Chromosome& p1, const Chromosome& p2,<o:p></o:p>
Chromosome& c1, Chromosome& c2);<o:p></o:p>
friend void Agamogenesis(const Chromosome& p, Chromosome& c);<o:p></o:p>
friend void Mutant(const Chromosome& p, Chromosome& c);<o:p></o:p>
friend class GT;<o:p></o:p>
};<o:p></o:p>
class GT{<o:p></o:p>
public:<o:p></o:p>
inline bool operator()(const Chromosome* c1, const Chromosome* c2)const{<o:p></o:p>
return c1->m_iScore > c2->m_iScore;<o:p></o:p>
}<o:p></o:p>
};<o:p></o:p>
类Chromosome是染色体封装,它的成员变量m_Geneme是一个基因序列,用std::vector容器来保存;成员变量m_iScore是这个染色体对“自然环境”的适应值,由评估模块评定。友元类GT实现了两个Chromosome的大小比较;还定义了三个友元函数分别实现交叉、复制及变异三个遗传算子,详见下节。<o:p></o:p>
<o:p></o:p>
遗传算子<o:p></o:p>
交叉、复制和变异三个遗传算子是遗传算法能够找到最优解的途径。这三个遗传算子模拟了自然界的物种交配和生殖的方式,为产生新的可行解提供了有效手段(见表一)。遗传算子声明为染色体类Chromosome的友元函数是为了方便操作它的私有变量,实现如下:<o:p></o:p>
void Intercross(const Chromosome& p1, const Chromosome& p2,<o:p></o:p>
Chromosome& c1, Chromosome& c2){<o:p></o:p>
unsigned int IntercrossPoint = RangeRandom<unsigned int>(0, GeneLen);<o:p></o:p>
unsigned int i = 0;<o:p></o:p>
for(; i < IntercrossPoint; ++i){<o:p></o:p>
c1.m_Geneme[i] = p1.m_Geneme[i];<o:p></o:p>
c2.m_Geneme[i] = p2.m_Geneme[i];<o:p></o:p>
}<o:p></o:p>
for(; i < GeneLen; ++i){<o:p></o:p>
c1.m_Geneme[i] = p2.m_Geneme[i];<o:p></o:p>
c2.m_Geneme[i] = p1.m_Geneme[i];<o:p></o:p>
}<o:p></o:p>
}<o:p></o:p>
void Agamogenesis(const Chromosome& p, Chromosome& c){<o:p></o:p>
c.m_Geneme = p.m_Geneme;<o:p></o:p>
}<o:p></o:p>
void Mutant(const Chromosome& p, Chromosome& c){<o:p></o:p>
unsigned int MutantPoint = RangeRandom<unsigned int>(0, GeneLen);<o:p></o:p>
Genetype NewGene = RangeRandom<Genetype>(0,18);<o:p></o:p>
c.m_Geneme = p.m_Geneme;<o:p></o:p>