基于Qt有限状态机人工智能的一种实现及改进方法
人工智能在今年是一个非常火的方向,当然了,不仅仅是今年,它一直火了很多年,有关人工智能的一些算法层出不穷。人工智能在很多领域都有应用,就拿我熟悉的游戏领域来说吧,一些寻路算法,比如说A*算法(我的《十日驱鬼记》就曾经使用了A*算法进行寻路),还有一些高级的算法,比如说决策树等,都在游戏中得以了广泛的应用。我目前想制作的项目和人工智能也有一定的关系,因此,我这个月开始学习搭建一些简单的人工智能框架。
蒋彩阳原创文章,首发地址:http://blog.csdn.net/gamesdev/article/details/46628447。欢迎同行前来探讨。
Qt为了更加方便地在既有的GUI界面上增添更加复杂的逻辑,在4.6的时候引入了有限状态机这个概念。有限状态机指的是以限定个数的状态进行相互转换,而形成的一种有机的整体,它在游戏中用得也非常多,我以前在制作游戏项目的时候也见过自己制作有限状态机来处理复杂逻辑的。因此我开始重新拾起有限状态机,看看能不能更深入地挖掘它的内容。
如果你和我一样了解了QML的用法,那么一定会有印象,Qt 将有限状态机模块移植到了QML环境中来了。要使用QML的有限状态机,需要来一句“import QtQml.StateMachine 1.0”这样的声明。Qt的文档非常丰富,在介绍有限状态机的时候甚至专门有一个章节,叫做“The Declarative State Machine Framework”,来介绍它的用法。如果大家还对QML的有限状态机不是很熟悉的话,还是看看这篇Qt帮助文档吧!
Qt的有限状态机,分为两个重要的内容。一个是“State”,指的是具体的某个状态,另外一个则是“Transition”,指的是两个状态之间的具体的转换。我在使用的时候发现,QML提供的有限状态机,只提供了SignalTransition以及TimeoutTransition这样的转换,并没有像Qt那样提供很多实用的Transition。刚开始尝试简单的时候,觉得还好,但是想到以后的状态机异常复杂,一旦涉及到的状态千变万化,就可能要写很多的状态,实在是不方便。我拿我正在制作的项目打比方吧:
上图是一个非常简单的有限状态机,它只有入口,没有出口,并且只有三个状态。除了初始状态s1之外,只是在s2和s3之间做切换。在图中,方框表示状态,箭头表示一个转换(transition)。那么不包括开始那个箭头,我们这里总共出现了6个状态,也是3×2个状态。用QML代码表示的话,是这个样子:
QtObject { id: root signal output( string text ) property string input property var stateMachine: StateMachine { running: true initialState: s1 State { id: s1 onEntered: output( "你好,欢迎来到人工智能测试平台。" ) SignalTransition { targetState: s2 signal: root.inputChanged guard: root.input == "我喜欢你。" } SignalTransition { targetState: s3 signal: root.inputChanged guard: root.input != "我喜欢你。" } } State { id: s2 onEntered: output( "我也喜欢你。" ) SignalTransition { targetState: s2 signal: root.inputChanged guard: root.input == "我喜欢你。" } SignalTransition { targetState: s3 signal: root.inputChanged guard: root.input != "我喜欢你。" } } State { id: s3 onEntered: output( "我刚来到这个世界,还不太懂人类的语言,能够教教我吗?" ) SignalTransition { targetState: s2 signal: root.inputChanged guard: root.input == "我喜欢你。" } SignalTransition { targetState: s3 signal: root.inputChanged guard: root.input != "我喜欢你。" } } } }
这仅仅是针对一个最小可执行的有限状态机而言,诸如Galgame这样的游戏,它的分支情况是非常多的,而且如果知道乘法原理的话,当x和y非常大的时候,产生的转换(Transition)的个数也是非常惊人的。究其原因,是因为SignalTransition必须依附于State类作为它的sourceState。因此我们必须想办法缩小规模才行。
因此我在研究Qt的有限状态机机制。幸运的是,在强大的Qt下,有限状态机的各个部分也是可以定制的。QState的祖先类是QAbstractState,QTransition的祖先类是QAbstractTransition,它们都是一定规模的抽象类,我们是需要实现它们的少数方法,就可以结合Qt的有限状态机做自己的处理了。于是我制作了一个名为AIState的状态类,它的作用是:
1、 保存它所发出的所有转换(Transition)的引用,当此状态激活时,统一让所有转换都为它服务。
同样地,我制作了一个名为ConditionalTransition的类,它有以下几个特性:
1、 可以设置sourceState,让其与State脱离父子关系,即可以定义在任何需要的位置;
2、 提供触发的条件属性;
3、 向状态机发送自定义的事件。
下面是它们的代码:
AIState.h
#ifndef AISTATE_H #define AISTATE_H #include <QState> #include <QQmlListProperty> #include "ConditionalTransition.h" class AIState : public QState { Q_OBJECT Q_PROPERTY( QQmlListProperty<ConditionalTransition> conditions READ conditions ) public: explicit AIState( QState* parent = Q_NULLPTR ); QQmlListProperty<ConditionalTransition> conditions( void ); private slots: void onActiveChanged( bool active ); protected: QList<ConditionalTransition*> m_conditions; }; #endif // AISTATE_H
AIState.cpp
#include "AIState.h" AIState::AIState( QState* parent ): QState( parent ) { connect( this, SIGNAL( activeChanged( bool ) ), this, SLOT( onActiveChanged( bool ) ) ); } QQmlListProperty<ConditionalTransition> AIState::conditions( void ) { return QQmlListProperty<ConditionalTransition>( this, m_conditions ); } void AIState::onActiveChanged( bool active ) { // 将原来transition的sourceState设置为AIState。 foreach ( ConditionalTransition* condition, m_conditions ) { condition->setSourceState( active? this: Q_NULLPTR ); } }
ConditionalTransition.h
#ifndef CONDITIONALTRANSITION_H #define CONDITIONALTRANSITION_H #include <QObject> #include <QObjectList> #include <QEvent> #include <QAbstractTransition> #include <QQmlListProperty> class ConditionalTransition: public QAbstractTransition { Q_OBJECT Q_PROPERTY( QState* sourceState READ sourceState WRITE setSourceState NOTIFY sourceStateChanged ) Q_PROPERTY( bool when READ condition WRITE setCondition NOTIFY conditionChanged ) public: explicit ConditionalTransition( QState* sourceState = Q_NULLPTR ); inline bool condition( void ) { return m_condition; } void setCondition( bool condition ); void setSourceState( QState* state ); signals: void sourceStateChanged( void ); void conditionChanged( void ); protected: virtual bool eventTest( QEvent* event ); virtual void onTransition( QEvent* event ); bool m_condition; }; class ConditionalEvent: public QEvent { public: explicit ConditionalEvent( bool condition, ConditionalTransition* sourceTransition ): QEvent( QEvent::Type( ConditionalType ) ), m_condition( condition ), m_sourceTransition( sourceTransition ) { } inline bool condition( void ) { return m_condition; } inline ConditionalTransition* sourceTransition( void ) { return m_sourceTransition; } enum Type { ConditionalType = QEvent::User + 2079 }; private: bool m_condition; ConditionalTransition* m_sourceTransition; }; #endif // CONDITIONALTRANSITION_H
ConditionalTransition.cpp
#include <QStateMachine> #include "ConditionalTransition.h" ConditionalTransition::ConditionalTransition( QState* sourceState ): QAbstractTransition( sourceState ) { m_condition = false; } void ConditionalTransition::setCondition( bool condition ) { m_condition = condition; emit conditionChanged( ); if ( condition && sourceState( ) != Q_NULLPTR && sourceState( )->active( ) && machine( )->isRunning( ) ) { // 只允许状态机正在运行并且源状态被激活的向状态机发送事件 machine( )->postEvent( new ConditionalEvent( condition, this ) ); } } void ConditionalTransition::setSourceState( QState* state ) { if ( sourceState( ) == state ) return; setParent( state ); emit sourceStateChanged( ); } bool ConditionalTransition::eventTest( QEvent* event ) { bool ret = false; if ( event->type( ) == QEvent::Type( ConditionalEvent::ConditionalType ) ) { // 如果当前条件为真,并且源转换为其本身,那么通过,执行转换 ConditionalEvent* ce = static_cast<ConditionalEvent*>( event ); ret = ce->sourceTransition( ) == this; } return ret; } void ConditionalTransition::onTransition( QEvent* event ) { Q_UNUSED( event ); }
接着将这几个类注册到QML环境中,就可以在QML中定义这些类的实例了。
StateMachine { id: machine running: true initialState: s1 StateSettings { id: settings } property alias inputWord: settings.inputWord property alias outputWord: settings.outputWord Condition { id: c2 objectName: "c2" when: inputWord.indexOf( "我喜欢你" ) != -1 targetState: s2 } Condition { id: c3 objectName: "c3" when: inputWord.indexOf( "我喜欢你" ) == -1 targetState: s3 } AIState { id: s1 objectName: "AI:s1" conditions: [ c2, c3 ] onEntered: outputWord = "你好,欢迎来到人工智能测试平台。" } AIState { id: s2 objectName: "AI:s2" conditions: [ c2, c3 ] onEntered: outputWord = "我也喜欢你。" } AIState { id: s3 objectName: "AI:s3" conditions: [ c2, c3 ] onEntered: outputWord = "我刚来到这个世界,还不太懂人类的语言,能够教教我吗?" } }
下面用状态机的图来分析一下:
红色代表的是处于激活的状态,绿色则是处于激活的状态所拥有的转换。结合上面的QML代码我们可以知道,程序中总共只定义了两个转换,并且转换定死的是targetState,而不是绑在了sourceState上,这么做可以把状态和转换进行解耦。比以前的实现少用了四个转换。如果有限状态机大起来了,这样的效率提升是非常可观的。
演示程序的运行截图:
源代码下载地址:这里