基於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上,這麼做可以把狀態和轉換進行解耦。比以前的實現少用了四個轉換。如果有限狀態機大起來了,這樣的效率提升是非常可觀的。
演示程序的運行截圖:
源代碼下載地址:這裏