人工智能算法在游戏中演示
人工智能算法在游戏中演示
人工智能是一个非常具有潜力的分支,顾名思义,它可以通过计算机指令模拟人的行为,在游戏中的人工智能就非常地多样了。对于FPS、TAB、RPG、STG、ADV等游戏,有着不同的人工智能,但都基于几种理论:有限状态机、遗传算法、神经网络等。下面我就采用游戏中最基础也是最常用的人工智能算法来进行演示。
我的开发环境是:
Windows: WindowsXP(SP3) + MinGW4.4/MinGW4.7 + Qt4.8.3/Qt5.0.1 + QtCreator2.6/QtCreator2.7
Linux: Ubuntu12.10 + gcc4.7 + Qt4.8.1/Qt4.8.4/Qt5.0.1 + QtCreator2.6/QtCreator2.7
演示程序下载地址:这里
源代码下载地址:
这里
注意:由于Qt5.0.1中一个未知的Bug,编译运行后角色无法移动,所以请使用Qt4.6+来进行编译。
程序截图如下所示:
通过子窗口的控制选项,我们可以设置我们的初音ミク、镜音リン和镜音レン的控制方法,可以选择人工控制也可以选择AI控制。如果选择人工控制,那么可以通过按下“上下左右”键控制角色的移动,如果选择AI控制,那么角色会沿着场景作顺时针移动。
整个项目的代码量较多,我将选择有关人工智能的内容进行讲解,也希望大家沿着我的思路思考下去。
首先如何让角色沿着窗口作顺时针旋转?一个简单的想法就是:当角色将要达到窗口顶端那么角色将往右移动;角色将要到达窗口右端那么角色将往下移动,以此类推。按照这样的思路,我写了这样的AI代码:
if ( m_pCharacter->pos( ).y( ) > 20.0 ) { qDebug( "AI go Up." ); m_pCharacter->SetAnimation( Character::_Up_ ); emit TriggerTransition( ); } else if ( m_pCharacter->pos( ).x( ) < 608.0 ) { qDebug( "AI go Right." ); m_pCharacter->SetAnimation( Character::_Right_ ); emit TriggerTransition( ); } else if ( m_pCharacter->pos( ).y( ) < 340.0 ) { qDebug( "AI go Down." ); m_pCharacter->SetAnimation( Character::_Down_ ); emit TriggerTransition( ); } else if ( m_pCharacter->pos( ).x( ) > 0.0 ) { qDebug( "AI go Left." ); m_pCharacter->SetAnimation( Character::_Left_ ); emit TriggerTransition( ); }
这也是我AI的第一个版本,但是正如QtMikuSnake7_ver_1应用程序截图中所示,它并不能达到应有的效果,角色一直在右上角打转。看来第一个版本有问题。问题在哪儿呢?这是由于我们将向上的判定优先于向下的判定,导致了角色在右上角处转至下后又转回了右上角。了解了这个问题之后第一个想法就是为向上判定添加约束条件,使其能够在右上角处正确地转至向下判定而不会折返。下面是AI的第二个版本:
if ( m_pCharacter->pos( ).y( ) > 20.0 && m_pCharacter->m_Direction != Character::_Down_ ) { qDebug( "AI go Up." ); m_pCharacter->SetAnimation( Character::_Up_ ); emit TriggerTransition( ); } else if ( m_pCharacter->pos( ).x( ) < 608.0 && m_pCharacter->m_Direction != Character::_Left_ ) { qDebug( "AI go Right." ); m_pCharacter->SetAnimation( Character::_Right_ ); emit TriggerTransition( ); } else if ( m_pCharacter->pos( ).y( ) < 340.0 && m_pCharacter->m_Direction != Character::_Up_ ) { qDebug( "AI go Down." ); m_pCharacter->SetAnimation( Character::_Down_ ); emit TriggerTransition( ); } else if ( m_pCharacter->pos( ).x( ) > 0.0 && m_pCharacter->m_Direction != Character::_Right_ ) { qDebug( "AI go Left." ); m_pCharacter->SetAnimation( Character::_Left_ ); emit TriggerTransition( ); }
为了保险,按照这种思路,将每一个方向判定都添加了约束条件,即判定当前的方向是何方向。按理说角色要往上走,那么当前的方向就肯定不是往下走,角色要往左走,当前的方向就肯定不是往右走。好了,运行一下,结果发现如QtMikuSnake7_ver_2所示的效果一样,角色在右下角处至左移了一格就往上走了。看来又是一次失误。
分析原因,发现向上判定的约束虽然解决了当前方向向下时仍进行向上判定的问题,可是未解决当前方向向左时仍然出现向上判定优先于向左判定的情况。看来还是需要再对向上判定进行进一步约束,按照“上右下左”的移动顺序,我们了解向上判定需要两个约束,向右判定需要一个约束,而向下判定不需要额外的约束,向左判定就更不需要了。下面AI的第三个版本:
if ( m_pCharacter->pos( ).y( ) > 20.0 && m_pCharacter->m_Direction != Character::_Down_ && m_pCharacter->m_Direction != Character::_Left_ ) { qDebug( "AI go Up." ); m_pCharacter->SetAnimation( Character::_Up_ ); emit TriggerTransition( ); } else if ( m_pCharacter->pos( ).x( ) < 608.0 && m_pCharacter->m_Direction != Character::_Left_ ) { qDebug( "AI go Right." ); m_pCharacter->SetAnimation( Character::_Right_ ); emit TriggerTransition( ); } else if ( m_pCharacter->pos( ).y( ) < 340.0 ) { qDebug( "AI go Down." ); m_pCharacter->SetAnimation( Character::_Down_ ); emit TriggerTransition( ); } else if ( m_pCharacter->pos( ).x( ) > 0.0 ) { qDebug( "AI go Left." ); m_pCharacter->SetAnimation( Character::_Left_ ); emit TriggerTransition( ); } else if ( m_pCharacter->pos( ).x( ) == 0.0 )// 为防止角色在左下角卡死设立的判断 { m_pCharacter->SetDirection( Character::_Up_ ); }
注意到最下面一个判断,因为角色走在左下角的时候会因为都满足不了这些判定条件而陷入“卡死”状态,所以我们要进行“解锁”操作——将当前的方向设为向上,这样又可以满足向上的判定了。下面是程序QtMikuSnake7_ver_3的截图。
似乎这个问题圆满地解决了。但是我觉得这个代码还是写得太被动了,因为这些代码都是出了问题而一个一个地打补丁打上去的,非常被动。我们得换一个角度考虑。试想,如果一条语句能够“排队”,当让它执行的时候它排在最前面,执行完毕后它轮到最末尾,给下一条语句机会,要是这样的话,我们可以让上、右、下、左四条语句依次排队,一条语句一条语句地轮着运行。
这完全有可能实现!回想起来了吗?这不就是数据结构中经典的队列结构!可是,怎样才能让语句智能地移动到后面执行呢?这里需要使用一个类对这条语句进行封装。因为语句的格式相当有条理:if 判定条件 then 执行语句。我是这样封装的:
class Clause: public QObject { public: Clause( Character* pParent = 0 ): QObject( pParent ), m_pCharacter( pParent ){ } virtual bool JudgeSentence( void ) = 0; virtual void Statement( void ) = 0; protected: Character* m_pCharacter; };
随后我设定一个队列,在Qt中有个现成的QQueue。
QQueue<Clause*> m_Clauses;
接着进行语句类的定义,让其继承Clause类:
class DirUpClause: public Clause { public: DirUpClause( Character* pParent = 0 ): Clause( pParent ) { } bool JudgeSentence( void ) { return m_pCharacter->pos( ).y( ) > 20.0; } void Statement( void ) { qDebug( "AI go Up." ); m_pCharacter->SetAnimation( Character::_Up_ ); } }; class DirDownClause: public Clause { public: DirDownClause( Character* pParent = 0 ): Clause( pParent ) { } bool JudgeSentence( void ) { return m_pCharacter->pos( ).y( ) < 340.0; } void Statement( void ) { qDebug( "AI go Down." ); m_pCharacter->SetAnimation( Character::_Down_ ); } }; class DirLeftClause: public Clause { public: DirLeftClause( Character* pParent = 0 ): Clause( pParent ) { } bool JudgeSentence( void ) { return m_pCharacter->pos( ).x( ) > 0.0; } void Statement( void ) { qDebug( "AI go Left." ); m_pCharacter->SetAnimation( Character::_Left_ ); } }; class DirRightClause: public Clause { public: DirRightClause( Character* pParent = 0 ): Clause( pParent ) { } bool JudgeSentence( void ) { return m_pCharacter->pos( ).x( ) < 608.0; } void Statement( void ) { qDebug( "AI go Right." ); m_pCharacter->SetAnimation( Character::_Right_ ); } };
最后我们在更新对象状态代码中进行一个简单地调用就可以了。
foreach ( Clause* pClause, m_Clauses ) { if ( pClause->JudgeSentence( ) ) { pClause->Statement( ); emit TriggerTransition( ); break; } else { m_Clauses.dequeue( ); m_Clauses.enqueue( pClause ); break; } }
上面的代码中,当条件满足的时候进行语句的执行,当条件不满足的时候将该语句从队列的头部移至队列的尾部。这样写虽然代码会比较多,但是思路清晰,对更复杂的状态维护起着重要的作用。执行起来效率也比较高,因为少了一些不必要的判定。下面是程序QtMikuSnake7_ver_4的截图。
上面的算法仅仅是一个很简单的演示,对于角色的运动还有诸如追击、自主规避、寻路等AI算法,对于复杂的游戏,状态的维护非常复杂且容易出错,而这个错误又不像程序宕机那样容易觉察,这时需要一个特定的职位——脚本设计师来解决此类问题。脚本设计师面对着各类的数据,通过自身熟练的脚本语言来对程序的各类参数进行微调,一款成功的游戏总有脚本设计师付出的辛勤汗水。所以说脚本设计师都是艺术家。
后记:程序QtMikuSnake7对
上一个版本的帧框大小进行了修正,刷新不会出现残影的现象,有关AI测试代码也将在下一个版本中被去除。