Game AI Best Practice (Part I)

来源:互联网 发布:python 逐行读取txt 编辑:程序博客网 时间:2024/05/29 14:33

前言:  最近读了Mat Buckland的Programming Game AI By Example, ⑤ 作为书中的配套例子,作者写了Simple Soccer和Raven两个教学游戏.  通过这两个游戏, 作者展示了

实际游戏开发中用到的大部分AI技术. Simple Soccer仅仅通过使用Steering Behaviors和State Machine就完成了一个非常精巧的足球游戏.

Raven掠夺者游戏则复杂得多,覆盖的AI技术主要有: Sensory Memory,  Messaging, Goal Driven Behavior, Time Slice Path Planning, Script, Trigger System, Steering Behavior,Target Selection, Fuzz Logic, Search等等, 就论AI技术复杂程度远远超过很多泡菜游戏和页游.(这2个项目的源码可以在此下载:http://www.jblearning.com/Catalog/9781556220784/student/   Just click in Sample Materials --> Source Code)

这篇文章主要是对Simple Soccer通过改进SteeringBehavior和增加和修改一些状态来改进AI作为一个Best Practice. 如果可能的话后续将写成一个系列文章, 对于上面提到的各个AI技术做相应的实现及总结。

一:   首先简单介绍一下Simple Soccer这款游戏, 顾名思义,就是一款简单的足球游戏: 一个足球场地, 二个球门,一个足球, 两支球队, 每队五名球员, 一名守门员, 四名场上球员.游戏规则: 没有越位, 没有出界(球碰到边界自动弹回), 足球越过球门线算得分. 

 

 

二:   程序架构(UML)  : Let's look into a big picture.

        由于Simple Soccer的程序架构有一定的复杂度,所以先画出UML图通过UML图来把握整体架构是明智的。(UML工具推荐StarUML)

                                                                                                                      图1 --SimpleSoccer程序架构

这里对照着图1,对游戏用到的各个类及基本功能做个简单的介绍:文章开头提到了Simple Soccer的实现主要用到了SteeringBehavior和StateMachine两部分,这里主要是指AI用到的实现技术。游戏光有AI当然不可能跑起来。所以还有实体系统, 画面渲染系统,游戏循环系统。

Simple的实体系统按照抽象到具象的递进层次为: BaseGameEntity->MovingEntity->PlayerBase->FieldPlayer/GoalKeeper

其中BaseGameEntity抽象层次最高,定义了实体类必须有的属性和接口。比如EntityID()-->获取实例ID, Update()-->GameLoop接口, Render()-->渲染, HandleMessage()-->消息的发送和接收。MovingEntity定义了运动类实体的共有属性如方向,速度,质量等,及对应属性的接口。一个游戏的实体系统的设计有组合式和继承式,这里用的是继承式实体架构.④

渲染系统Render(): Game Rendering System这又是游戏体系的另一大子模块了,画面渲染也是游戏开发中的一个非常复杂的技术环节,目前游戏的即时运算画面已经达到CG级别,你可以使用DirectX,如果要跨平台的话可以用OpenGL,或者直接使用一个渲染引擎,调用API。在SimpleSoccer这里用的是Windows GDI. ⑥ GDI作为技术验证使用完全足够了。如果完全不考虑渲染只输出游戏的逻辑运行结果, 也是可以的,可以用字符串的形式输出结果,也就是早期的MUD,不过这样有些不直观。

StateMachine: 有限状态机,负责游戏实体和游戏世界的状态转换,这个不用多做介绍,网上已经有很多文章。这里的状态机带消息处理机制。

SteeringBehaviors 行为操控:  提供运动实体自治操控行为即:定义行为目标-->产生操控力-->移动。常见的行为有靠近,抵达,离开,巡逻,跟随,躲避障碍,及不同的操控力的合并计算等等。②

三:  游戏主循环:

游戏主循环作为游戏世界所有对象更新的引擎, 驱动着游戏实体状态的更新. (In other words, like heartbeat to human.) ①

一般而言,分为游戏逻辑更新和画面渲染更新两部分。

下面整理Simple Soccer主循环调用: 先更新游戏逻辑再渲染输出画面:

GameUpdate():   //in this game, update() includes: entities physics update, game state update. WinMain()->SoccerPitch.Update()->Ball.Update(); SoccerTeam.Update();SoccerTeam.Update()->CalculateClosestPlayerToBall();CalculateClosestPlayerToOpponentSupporter();                                            StateMachine<SoccerTeam>*.Update();TeamMember.Update();TeamMember.Update()->FieldPlayer.Update();GoalKeeper.Update();FieldPlayer.Update()->StateMachine<FieldPlayer>*.Update();m_pSteering.Calculate();GoalKeeper.Update()->StateMachine<GoalKeeper>*.Update(); m_pSteering.Calculate();StateMachine<T>.Update(){GlobalState.Execute(StateOwner*);CurrentState.Execute(StateOwner*);}GameRender():SoccerPitch.Render()->RenderPichItself(),SoccerBall.Render();SoccerTeam.Render();SoccerTeam.Render()->TeamMember.Render()->FieldPlayer.Render();GoalKeeper.Render();

改进目标:
在原有的游戏架构上增加球员防守AI,也就是盯住攻方助攻人员,在球和对方助攻人员的连线上插入到对方助攻人员前面防止其拿球。


分析:

从改进目标可以看出要实现目标就要:

1) 先实现一个操控类,给出攻防助攻球员和球的位置,防守球员得移动到预定的拦截位置。

2) 增加一个FieldPlayer球员状态:即拦截状态

3) 更新状态间转换逻辑:状态与状态之间不是孤立的,新增了状态必须要和旧有状态进行整合,并实现状态转换的逻辑完备。

    对于状态多,转换逻辑复杂的程序而言,要修改之前最好画出原有的和更新后的状态转换图。

                                                                                                                    图2  --FiledPlayer状态转换图                                  

 

主要实现:

1)新增操控类定义:

//--------------------------- Interposeopteam ----------------------------------//  added by Marcus//  Given an closest player to opponent controlling player this method returns a//  force that attempts to position the agent between opponent controlling player//  and opponent attacker supporting player.//------------------------------------------------------------------------------Vector2D SteeringBehaviors::Interposeopteam(const SoccerBall* ball,                                        Vector2D target,   double       DistFromSupporter){if (Vec2DDistance(target, ball->Pos())>DistFromSupporter)return Arrive(target + Vec2DNormalize(ball->Pos() - target) *DistFromSupporter, fast);else return Arrive((target+ball->Pos()) / 2, fast);}

合并操控类:

//-------------------------- SumForces -----------------------------------////  this method calls each active steering behavior and accumulates their//  forces until the max steering force magnitude is reached at which//  time the function returns the steering force accumulated to that //  point//  --add comment by Marcus//  --In SumForces function, the order of  AcumulateForce actually is//  --order of prioritization//------------------------------------------------------------------------Vector2D SteeringBehaviors::SumForces(){   Vector2D force;    //the soccer players must always tag their neighbors   FindNeighbours();  if (On(separation))  {    force += Separation() * m_dMultSeparation;    if (!AccumulateForce(m_vSteeringForce, force)) return m_vSteeringForce;  }      if (On(seek))  {    force += Seek(m_vTarget);    if (!AccumulateForce(m_vSteeringForce, force)) return m_vSteeringForce;  }  if (On(arrive))  {    force += Arrive(m_vTarget, fast);    if (!AccumulateForce(m_vSteeringForce, force)) return m_vSteeringForce;  }  if (On(pursuit))  {    force += Pursuit(m_pBall);    if (!AccumulateForce(m_vSteeringForce, force)) return m_vSteeringForce;  }  if (On(interpose))  {    force += Interpose(m_pBall, m_vTarget, m_dInterposeDist);    if (!AccumulateForce(m_vSteeringForce, force)) return m_vSteeringForce;  }<span style="color:#ff6666;">//added by Marcus</span><span style="color:#ff6666;">  if (On(interposeopteam))  {  force += Interposeopteam(m_pBall, m_vTarget, m_dInterposeSpDist);  if (!AccumulateForce(m_vSteeringForce, force)) return m_vSteeringForce;  }</span>  return m_vSteeringForce;}



新增函数TryInterpose(),此函数主要功能是当被调用时,计算是否存在符合条件的拦截球员,如果存在向他发送拦截指令。

//added by Marcus on 15th Apr.15void PlayerBase::TryInterpose()const{    //if there is no interposer we need to find a suitable one.if (Team()->ClosestPlayerToOpponentSupporter() == NULL){PlayerBase* BestInterposePly = Team()->CalculateClosestPlayerToOpponentSupporter();Team()->SetPlayerClosestToOppSupporter(BestInterposePly);if (BestInterposePly){Dispatcher->DispatchMsg(SEND_MSG_IMMEDIATELY,ID(),Team()->ClosestPlayerToOpponentSupporter()->ID(),Msg_InterposeOp,NULL);}}PlayerBase* BestInterposePly = Team()->CalculateClosestPlayerToOpponentSupporter();//if the interpose defense player available to interpose the opponent, update//the pointers and send messages to the previous interpose player to go home //and update the new interposer to interpose statesif (BestInterposePly && (BestInterposePly != Team()->ClosestPlayerToOpponentSupporter())){if (Team()->ClosestPlayerToOpponentSupporter()){Dispatcher->DispatchMsg(SEND_MSG_IMMEDIATELY,ID(),Team()->ClosestPlayerToOpponentSupporter()->ID(),Msg_GoHome,NULL);}Team()->SetSupportingPlayer(BestInterposePly);Dispatcher->DispatchMsg(SEND_MSG_IMMEDIATELY,ID(),Team()->ClosestPlayerToOpponentSupporter()->ID(),Msg_InterposeOp,NULL);}}

在SoccerTeam中,找出离对方助攻球员最近的防守球员

//added by Marcus on Apr.2015 //sets  m_pClosestPlayerToOpponentSupporter//actually should be closest to middle point of opponent supporter and the soccer ball.PlayerBase* SoccerTeam::CalculateClosestPlayerToOpponentSupporter(){double ClosestSoFar = MaxFloat;PlayerBase* BestInterposer = NULL;std::vector<PlayerBase*>::iterator it = m_Players.begin();     for (it; it != m_Players.end(); ++it){if (((*it)->Role() == PlayerBase::defender) && (m_pControllingPlayer == NULL) && Opponents()->SupportingPlayer() != NULL && (*it)->isInterposeTriggered((*it)->Team()->Opponents()->SupportingPlayer()->Pos())){//calculate the dist. Use the squared value to avoid sqrt//double dist = Vec2DDistanceSq((*it)->Pos(), (Opponents()->SupportingPlayer()->Pos()+Opponents()->ControllingPlayer()->Pos())/2);double dist = Vec2DDistanceSq((*it)->Pos(), (Opponents()->SupportingPlayer()->Pos()+Pitch()->Ball()->Pos())/2);       if (dist < ClosestSoFar){ClosestSoFar = dist;BestInterposer = (*it);}} }return BestInterposer;}


 

2)增加拦截状态类

//added by Marcus on Apr.2015 add interpose opponent between the ball//and attacker supporter state in field player.InterposeOpponentSupporter* InterposeOpponentSupporter::Instance(){static InterposeOpponentSupporter instance;return &instance;}void InterposeOpponentSupporter::Enter(FieldPlayer* player){player->Steering()->SetTarget(player->Team()->Opponents()->SupportingPlayer()->Pos());player->Steering()->ArriveOn();player->Steering()->InterposeopteamOn(Prm.DefenderInterceptSpRange);#ifdef PLAYER_STATE_INFO_ONdebug_con << "Player " << player->ID() << " enters interpose state (Using Arrive)" << "";#endif}void InterposeOpponentSupporter::Execute(FieldPlayer* player){if (player->Team()->InControl()){player->GetFSM()->ChangeState(Wait::Instance());return;}////if the ball is within kicking range the player changes state to KickBall.//if (player->BallWithinKickingRange())//{//player->GetFSM()->ChangeState(KickBall::Instance());//return;//}//if the player is the closest player to the opponent supporter then he should keep//interpose to predefined point ahead the opponent supporter and face to controlling player.//in each update step interpose target position should be re-calculated.if (player->Team()->Opponents()->SupportingPlayer()){// re-calculate the target position if it changedif (player->Team()->Opponents()->SupportingPlayer()->Pos() != player->Steering()->Target()){player->Steering()->SetTarget(player->Team()->Opponents()->SupportingPlayer()->Pos());player->Steering()->ArriveOn();player->Steering()->InterposeopteamOn(Prm.DefenderInterceptSpRange);return;}}//condition is not exists then return to home region.player->GetFSM()->ChangeState(ReturnToHomeRegion::Instance()); return;}void InterposeOpponentSupporter::Exit(FieldPlayer* player){player->Steering()->ArriveOff();player->Steering()->InterposeopteamOff();player->Team()->SetPlayerClosestToOppSupporter(NULL);}


3) 状态与状态间的转换逻辑整合: 在全局状态的消息路由中加入Msg_InterposeOp, 在Wait和Gohome消息中在符合条件的情况下调用player->TryInterpose():

 

case Msg_InterposeOp:  {  //if already interposing just return  if (player->GetFSM()->isInState(*InterposeOpponentSupporter::Instance()))  {  return true;  }  //set the target to be the closest player to opponent supporting player  player->Steering()->SetTarget(player->Team()->Opponents()->SupportingPlayer()->Pos());  //change the state  player->GetFSM()->ChangeState(InterposeOpponentSupporter::Instance());  return true;  } case Msg_Wait:    {      //change the state      player->GetFSM()->ChangeState(Wait::Instance());        if( !player->Team()->InControl() &&  !player->Pitch()->GoalKeeperHasBall()&&  player->Team()->Opponents()->SupportingPlayer()!=NULL)  {player->TryInterpose();}      return true;    }    break;  case Msg_GoHome:    {      player->SetDefaultHomeRegion();            player->GetFSM()->ChangeState(ReturnToHomeRegion::Instance());  if( !player->Team()->InControl() &&  !player->Pitch()->GoalKeeperHasBall()&&  player->Team()->Opponents()->SupportingPlayer() != NULL)  {player->TryInterpose();}      return true;    }    break;

其余的修改就不一一列举了。

 

附上游戏体系更新后的UML图和状态转换图:

                                                                                                                  图三  更新后的架构图

 

                                                                                                      图4    更新后状态转换图


                                                                           图五   更新后的截球动作


总结:

有限状态机本身是一个强大的逻辑控制和状态转换机制,但是也有它的缺点,一旦状态多起来之后,状态之间要实现逻辑完备的困难度会迅速增大,

Simple Soccer使用的层次化状态机一定程度上增强了状态转换的可维护性和可扩展性,隔离了不同层次之间的状态跳转。同一层次状态内的子状态

间的跳转无关层次外的状态,在层次之间做到了隔离。比如我方进攻的时候,只需要关心进攻状态的子状态之间的跳转,防守的时候就在防守的子状态间转换。

进攻和防守的转换交有球队层次上的状态机去处理。 虽然如此,层次化的有限状态机在面对复杂的大型AI程序是仍有不足,所以目前主流的引擎使用行为树模式。③

在作者的Raven游戏用使用了基于目标的行为模式 Goal Driven Behavior或者叫目标导向(Goal-Oriented)的AI架构,也是目前主流引擎使用的AI技术,占了相当大的比例,实现起来有挑战。将来我们在Raven改进练习时再来体验它。


 

References:

GameLoop:      http://www.koonsolo.com/news/dewitters-gameloop/

Steering Behaviors For Autonomous Characters:  http://www.red3d.com/cwr/steer/gdc99/

Behavior Trees: http://en.wikipedia.org/wiki/Behavior_Trees

组合式实体的实现:http://www.aisharing.com/archives/475

Programming Game AI By Example (Wordware Game Developers Library)   by Mat Buckland  http://www.amazon.com/dp/1556220782/ref=cm_sw_su_dp

AI Techniques for Game Programming Chapter 1, 2  by Mat Buckland http://amzn.com/193184108X

0 0