如何让你的蠢小人动起来——C++中包含优先级复杂条件FSM的实现
2017-06-23 11:22
260 查看
有限状态机(FSM)最大的优点在于实现函数的复用,通过条件(condition)与状态(state)的连接,让逻辑主体在不同的状态对象中切换,执行其中的功能代码。在引入了优先级(priority)的概念后,FSM可以被应用在一些复合条件的AI应用场景中,简单地来说,在某个状态下,同时满足多个条件时按照优先级排序,决定最终执行的条件判断和随后的状态切换。本文将通过一个简单的应用实例,初步探讨在C++中如何实现复杂条件的FSM.
首先,我们想要实现的FSM结构如下:
可以清除的看到这个简单AI的完整逻辑:完成初始化后进入第一个state:
searchTarget, 并执行其中的功能函数,搜索目标,根据返回结果判定后续的两个条件hastarget和notarget是否成立,因为这两个条件是互斥的,所以priority是否相同没有影响;当notarget成立的时候,进入idle状态,AI进行无目的的游荡,经过一段时间后,当timeUP_2成立时,重新进行searchTarget;当hastarget成立时进入chaseTarget状态,chaseTarget由另一个时间条件timeUp驱动,执行追逐目标的功能,当目标死亡(targetDead)或者超出范围(tooLong)的时候进入resetTarget状态,并最终回到searchTarget状态;在idle或者chaseTarget这两个状态下,当满足agentDead条件时,进入最后的Finish状态,结束状态机并进行清理。
上述所有的条件都可以具备自身的priority,比如在chaseTarget状态下,对应的三个条件agentDead,
targetDead和tooLong,其中agentDead的优先级显然应该最高,我们将其设置为6,targetDead和tooLong可以具有相同的优先级,如果某个时刻三个条件同时满足,则优先切换到priority最高的agentDead对应的Finish状态。下面我们看看如何在C++里实现这一简单FSM的模拟。
首先我们利用工厂模式建立一个状态/条件和枚举变量之间的映射:
typedefvoid*(*factoryFun)
(void);
classmyFactory
{
private:
staticmyFactory*
m_index;
map<string,factoryFun>
myFactoryMap;
public:
staticmyFactory*
getSingleInstance()
{
if(m_index
==NULL)
{
m_index=
newmyFactory();
}
returnm_index;
}
void*getClassByname(string
classname);
voidregistionFun(string
classname, factoryFunmyfactoryFun);
};
classmyRegistion
{
public:
myRegistion(stringclassname,factoryFunmyfactoryFun)
{
myFactory::getSingleInstance()->registionFun(classname,myfactoryFun);
}
};
#defineREGISTER(classname) \
classname* FSM##classname() \
{ \
returnnewclassname(); \
} \
myRegistionmyFSM##classname(#classname,
(factoryFun)FSM##classname())
//..........................................................................................//
myFactory*myFactory::m_index=NULL;
void*myFactory::getClassByname(stringclassname)
{
map<string,factoryFun>::const_iteratorit;
it
=myFactoryMap.find(classname);
if(it==
myFactoryMap.end())
{
returnNULL;
}
else
{
returnit->second;
}
}
voidmyFactory::registionFun(stringclassname,factoryFunmyFun)
{
myFactoryMap.insert(make_pair(classname,myFun));
}
有了这个映射表 myFactoryMap,我们就可以方便地用代表状态的枚举值获得对应的状态和条件对象了。
接下来,我们要建立state的基类:
//state的基类
classinit_FSM
{
public:
//init_FSM();
public:
string
nowState;
inta;
clock_t
tick1;
clock_t
tick2;
clock_t
tick3;
public:
virtualboolprossesing(init_FSM*
myEntry);
};
boolinit_FSM::prossesing(init_FSM*myEntry)
{
myEntry->tick1=
clock();
myEntry->tick2=
clock();
myEntry->a=
0;
cout
<<myEntry->a<<endl;
cout
<<"agent的所有变量初始化"<<
endl;
returntrue;
}
REGISTER(init_FSM);
为了方便,我们就使用第一个状态init_FSM作为基类,它定义了FSM中使用的所有局部变量,并且将其初始化。
然后利用宏我们建立所有的派生类:
#defineCREATECHILDSTATE(classname) \
class
classname : publicinit_FSM \
{ \
public: \
boolprossesing(init_FSM*
myEntry); \
};
注册这些派生类,例如:
CREATECHILDSTATE(searchTarget_FSM);
REGISTER(searchTarget_FSM);
Condition的建立和注册方法与state相同,实际上两者只是功能上的划分,没有本质的区别。
CREATECHILDSTATE(timeUp_FSMC);
REGISTER(timeUp_FSMC);
接下来我们要实现state与condition中的功能函数processing,比如:
boolchaseTarget_FSM::prossesing(init_FSM*myEntry)
{
myEntry->tick1=
clock();
myEntry->a=
3;
cout
<<myEntry->a<<endl;
cout
<<"实现追逐目标的过程"<<
endl;
returntrue;
}
好了,到这里为止我们的condition和state所有的类都已经建立好并且注册到工厂里了,那么接下来我们要怎样实现一个state和它对应的下游condition之间的映射表呢?我们可以简单地用XML将文章开头的FSM描述出来,然后将其加载到vs中变成一个map.先来看看这张XML:
可以看到,如果我们将每一个state都当作一个node来处理的话,那么每个state下都会对应数个子node,而且我们可以在这些node的下级中配置它们对应的condition和priority,这样我们这张XML的表格和前述的FSM关系图就是等效的。
现在我们将XML加载到vs里,这里我用的是比较常用的tinyxml,代码如下:
enum successEnum
{
FAILURE,
SUCCESS
};
structcAndsFormat
{
string
condition;
string
state;
intpriority;
};
//利用priority对vector进行冒泡排序
void
popVector(vector<cAndsFormat>&myVector)
{
vector<cAndsFormat>::iterator
it;
vector<cAndsFormat>::iterator
itBack = myVector.begin();
cAndsFormat
itBUFF;
for(it=myVector.begin();it!=myVector.end();it++)
{
for(itBack=myVector.begin();itBack!=myVector.end();itBack++)
{
if(it->priority
> itBack->priority)
{
itBUFF=*it;
*it=*itBack;
*itBack=itBUFF;
}
}
}
}
vector<string>stateNameArray;
map<string,vector<cAndsFormat>>myFlexMap;
//读取XML中的状态和条件信息并建立map
successEnumloadXML()
{
TiXmlDocument
doc;
if(!doc.LoadFile("monsterFSM.xml"))
{
cerr
<<doc.ErrorDesc()<<
endl;
returnFAILURE;
}
//project
TiXmlElement*
root = doc.FirstChildElement();
if(root
==NULL)
{
cerr
<<"Failed
to load file: No root element."<<endl;
doc.Clear();
returnFAILURE;
}
for(TiXmlElement*
elem = root->FirstChildElement(); elem != NULL;
elem = elem->NextSiblingElement())
{
//元素名称状态名称
string
eleName = elem->Attribute("ID");
//将找到的state名称存放
stateNameArray.push_back(eleName);
//遍历每个state下所有的子状态
to
TiXmlElement*
stateChild;
TiXmlElement*
conditionChild;
string
conditionName, stateName;
cAndsFormat
myObj;
vector<cAndsFormat>myFlex;
for(TiXmlElement*
stateChild = elem->FirstChildElement();stateChild != NULL;
stateChild =stateChild->NextSiblingElement())
{
stateName=stateChild->Attribute("ID");
conditionChild =stateChild->FirstChildElement();
conditionName=conditionChild->Attribute("ID");
myObj.condition=conditionName;
myObj.state=stateName;
myObj.priority= atoi(conditionChild->Attribute("Priority"));
myFlex.push_back(myObj);
}
//按照priority将找到的所有子状态冒泡排序
if(myFlex.size()
> 0)
{
popVector(myFlex);
}
//加入map中
myFlexMap.insert(make_pair(eleName,myFlex));
}
doc.Clear();
returnSUCCESS;
}
这样,这张state与condition之间的映射表myFlexMap就建立好了。在使用这个FSM的时候,首先建立init_FSM的对象并将其保存在容器里,比如vector<init_FSM*>myEntryArray,在循环里遍历这个类就可以实现FSM的运作了。
int
main()
{
loadXML();
init_FSM*
myEntry = newinit_FSM();
myEntryArray.push_back(myEntry);
//执行init的prossesing语句
myEntry->prossesing(myEntry);
//将状态置为init
myEntry->nowState="init_FSM";
vector<init_FSM*>::iterator
it;
while(true)
{
for(it=
myEntryArray.begin();it!=myEntryArray.end();)
{
if((*it)->nowState=="finish_FSM")
{
it=myEntryArray.erase(it);
}
else
{
string
stateName = (*it)->nowState;
init_FSM*
currentState = (init_FSM*)myFactory::getSingleInstance()->getClassByname(stateName);
vector<cAndsFormat>vectorBuffer
= myFlexMap.find(stateName)->second;
vector<cAndsFormat>::iterator
itCheck;
for(itCheck=
vectorBuffer.begin(); itCheck!=vectorBuffer.end();
itCheck++)
{
init_FSM*
myCondition = (init_FSM*)myFactory::getSingleInstance()->getClassByname(itCheck->condition);
if(myCondition->prossesing(*it))
{
init_FSM*
myState = (init_FSM*)myFactory::getSingleInstance()->getClassByname(itCheck->state);
myState->prossesing(*it);
(*it)->nowState=itCheck->state;
break;
}
}
it++;
}
}
if(myEntryArray.size()
< 1)
{
break;
}
}
}
首先,我们想要实现的FSM结构如下:
可以清除的看到这个简单AI的完整逻辑:完成初始化后进入第一个state:
searchTarget, 并执行其中的功能函数,搜索目标,根据返回结果判定后续的两个条件hastarget和notarget是否成立,因为这两个条件是互斥的,所以priority是否相同没有影响;当notarget成立的时候,进入idle状态,AI进行无目的的游荡,经过一段时间后,当timeUP_2成立时,重新进行searchTarget;当hastarget成立时进入chaseTarget状态,chaseTarget由另一个时间条件timeUp驱动,执行追逐目标的功能,当目标死亡(targetDead)或者超出范围(tooLong)的时候进入resetTarget状态,并最终回到searchTarget状态;在idle或者chaseTarget这两个状态下,当满足agentDead条件时,进入最后的Finish状态,结束状态机并进行清理。
上述所有的条件都可以具备自身的priority,比如在chaseTarget状态下,对应的三个条件agentDead,
targetDead和tooLong,其中agentDead的优先级显然应该最高,我们将其设置为6,targetDead和tooLong可以具有相同的优先级,如果某个时刻三个条件同时满足,则优先切换到priority最高的agentDead对应的Finish状态。下面我们看看如何在C++里实现这一简单FSM的模拟。
首先我们利用工厂模式建立一个状态/条件和枚举变量之间的映射:
typedefvoid*(*factoryFun)
(void);
classmyFactory
{
private:
staticmyFactory*
m_index;
map<string,factoryFun>
myFactoryMap;
public:
staticmyFactory*
getSingleInstance()
{
if(m_index
==NULL)
{
m_index=
newmyFactory();
}
returnm_index;
}
void*getClassByname(string
classname);
voidregistionFun(string
classname, factoryFunmyfactoryFun);
};
classmyRegistion
{
public:
myRegistion(stringclassname,factoryFunmyfactoryFun)
{
myFactory::getSingleInstance()->registionFun(classname,myfactoryFun);
}
};
#defineREGISTER(classname) \
classname* FSM##classname() \
{ \
returnnewclassname(); \
} \
myRegistionmyFSM##classname(#classname,
(factoryFun)FSM##classname())
//..........................................................................................//
myFactory*myFactory::m_index=NULL;
void*myFactory::getClassByname(stringclassname)
{
map<string,factoryFun>::const_iteratorit;
it
=myFactoryMap.find(classname);
if(it==
myFactoryMap.end())
{
returnNULL;
}
else
{
returnit->second;
}
}
voidmyFactory::registionFun(stringclassname,factoryFunmyFun)
{
myFactoryMap.insert(make_pair(classname,myFun));
}
有了这个映射表 myFactoryMap,我们就可以方便地用代表状态的枚举值获得对应的状态和条件对象了。
接下来,我们要建立state的基类:
//state的基类
classinit_FSM
{
public:
//init_FSM();
public:
string
nowState;
inta;
clock_t
tick1;
clock_t
tick2;
clock_t
tick3;
public:
virtualboolprossesing(init_FSM*
myEntry);
};
boolinit_FSM::prossesing(init_FSM*myEntry)
{
myEntry->tick1=
clock();
myEntry->tick2=
clock();
myEntry->a=
0;
cout
<<myEntry->a<<endl;
cout
<<"agent的所有变量初始化"<<
endl;
returntrue;
}
REGISTER(init_FSM);
为了方便,我们就使用第一个状态init_FSM作为基类,它定义了FSM中使用的所有局部变量,并且将其初始化。
然后利用宏我们建立所有的派生类:
#defineCREATECHILDSTATE(classname) \
class
classname : publicinit_FSM \
{ \
public: \
boolprossesing(init_FSM*
myEntry); \
};
注册这些派生类,例如:
CREATECHILDSTATE(searchTarget_FSM);
REGISTER(searchTarget_FSM);
Condition的建立和注册方法与state相同,实际上两者只是功能上的划分,没有本质的区别。
CREATECHILDSTATE(timeUp_FSMC);
REGISTER(timeUp_FSMC);
接下来我们要实现state与condition中的功能函数processing,比如:
boolchaseTarget_FSM::prossesing(init_FSM*myEntry)
{
myEntry->tick1=
clock();
myEntry->a=
3;
cout
<<myEntry->a<<endl;
cout
<<"实现追逐目标的过程"<<
endl;
returntrue;
}
好了,到这里为止我们的condition和state所有的类都已经建立好并且注册到工厂里了,那么接下来我们要怎样实现一个state和它对应的下游condition之间的映射表呢?我们可以简单地用XML将文章开头的FSM描述出来,然后将其加载到vs中变成一个map.先来看看这张XML:
<?xml version="1.0" encoding="UTF-8"?> <Project name="MonsterFSM"> <!-- Starting--> <state ID = "init_FSM"> <to ID = "searchTarget_FSM"> <condition ID = "true_FSMC" Priority = "0"></condition> </to> </state> <!-- searching for target--> <state ID = "searchTarget_FSM"> <to ID = "idle_FSM"> <condition ID = "noTarget_FSMC" Priority = "0"></condition> </to> <to ID = "chaseTarget_FSM"> <condition ID = "hasTarget_FSMC" Priority = "1"></condition> </to> </state> <!-- wardering around--> <state ID = "idle_FSM"> <to ID = "searchTarget_FSM"> <condition ID = "timeUp_2_FSMC" Priority = "0"></condition> </to> <to ID = "finish_FSM"> <condition ID = "agentDead_FSMC" Priority = "1"></condition> </to> </state> <!-- moving to target position while it still alive--> <state ID = "chaseTarget_FSM"> <to ID = "chaseTarget_FSM"> <condition ID = "timeUp_FSMC" Priority = "0"></condition> </to> <to ID = "finish_FSM"> <condition ID = "agentDead_FSMC" Priority = "6"></condition> </to> <to ID = "resetTarget_FSM"> <condition ID = "tooLong_FSMC" Priority = "1"></condition> </to> <to ID = "resetTarget_FSM"> <condition ID = "targetDead_FSMC" Priority = "1"></condition> </to> </state> <!-- reset target--> <state ID = "resetTarget_FSM"> <to ID = "searchTarget_FSM"> <condition ID = "true_FSMC" Priority = "0"></condition> </to> </state> <!-- finish--> <state ID = "finish_FSM"> </state> </Project>
可以看到,如果我们将每一个state都当作一个node来处理的话,那么每个state下都会对应数个子node,而且我们可以在这些node的下级中配置它们对应的condition和priority,这样我们这张XML的表格和前述的FSM关系图就是等效的。
现在我们将XML加载到vs里,这里我用的是比较常用的tinyxml,代码如下:
enum successEnum
{
FAILURE,
SUCCESS
};
structcAndsFormat
{
string
condition;
string
state;
intpriority;
};
//利用priority对vector进行冒泡排序
void
popVector(vector<cAndsFormat>&myVector)
{
vector<cAndsFormat>::iterator
it;
vector<cAndsFormat>::iterator
itBack = myVector.begin();
cAndsFormat
itBUFF;
for(it=myVector.begin();it!=myVector.end();it++)
{
for(itBack=myVector.begin();itBack!=myVector.end();itBack++)
{
if(it->priority
> itBack->priority)
{
itBUFF=*it;
*it=*itBack;
*itBack=itBUFF;
}
}
}
}
vector<string>stateNameArray;
map<string,vector<cAndsFormat>>myFlexMap;
//读取XML中的状态和条件信息并建立map
successEnumloadXML()
{
TiXmlDocument
doc;
if(!doc.LoadFile("monsterFSM.xml"))
{
cerr
<<doc.ErrorDesc()<<
endl;
returnFAILURE;
}
//project
TiXmlElement*
root = doc.FirstChildElement();
if(root
==NULL)
{
cerr
<<"Failed
to load file: No root element."<<endl;
doc.Clear();
returnFAILURE;
}
for(TiXmlElement*
elem = root->FirstChildElement(); elem != NULL;
elem = elem->NextSiblingElement())
{
//元素名称状态名称
string
eleName = elem->Attribute("ID");
//将找到的state名称存放
stateNameArray.push_back(eleName);
//遍历每个state下所有的子状态
to
TiXmlElement*
stateChild;
TiXmlElement*
conditionChild;
string
conditionName, stateName;
cAndsFormat
myObj;
vector<cAndsFormat>myFlex;
for(TiXmlElement*
stateChild = elem->FirstChildElement();stateChild != NULL;
stateChild =stateChild->NextSiblingElement())
{
stateName=stateChild->Attribute("ID");
conditionChild =stateChild->FirstChildElement();
conditionName=conditionChild->Attribute("ID");
myObj.condition=conditionName;
myObj.state=stateName;
myObj.priority= atoi(conditionChild->Attribute("Priority"));
myFlex.push_back(myObj);
}
//按照priority将找到的所有子状态冒泡排序
if(myFlex.size()
> 0)
{
popVector(myFlex);
}
//加入map中
myFlexMap.insert(make_pair(eleName,myFlex));
}
doc.Clear();
returnSUCCESS;
}
这样,这张state与condition之间的映射表myFlexMap就建立好了。在使用这个FSM的时候,首先建立init_FSM的对象并将其保存在容器里,比如vector<init_FSM*>myEntryArray,在循环里遍历这个类就可以实现FSM的运作了。
int
main()
{
loadXML();
init_FSM*
myEntry = newinit_FSM();
myEntryArray.push_back(myEntry);
//执行init的prossesing语句
myEntry->prossesing(myEntry);
//将状态置为init
myEntry->nowState="init_FSM";
vector<init_FSM*>::iterator
it;
while(true)
{
for(it=
myEntryArray.begin();it!=myEntryArray.end();)
{
if((*it)->nowState=="finish_FSM")
{
it=myEntryArray.erase(it);
}
else
{
string
stateName = (*it)->nowState;
init_FSM*
currentState = (init_FSM*)myFactory::getSingleInstance()->getClassByname(stateName);
vector<cAndsFormat>vectorBuffer
= myFlexMap.find(stateName)->second;
vector<cAndsFormat>::iterator
itCheck;
for(itCheck=
vectorBuffer.begin(); itCheck!=vectorBuffer.end();
itCheck++)
{
init_FSM*
myCondition = (init_FSM*)myFactory::getSingleInstance()->getClassByname(itCheck->condition);
if(myCondition->prossesing(*it))
{
init_FSM*
myState = (init_FSM*)myFactory::getSingleInstance()->getClassByname(itCheck->state);
myState->prossesing(*it);
(*it)->nowState=itCheck->state;
break;
}
}
it++;
}
}
if(myEntryArray.size()
< 1)
{
break;
}
}
}
相关文章推荐
- [原] XAF如何实现复杂条件查询
- C++如何实现string的trim功能? (已经包含trimLeft和trimRight)
- 如何解决复杂条件下的程序流的控制问题?
- 如何实现多个条件进行搜索?
- 如何理解c和c++的复杂类型声明
- C++--如何实现SDI程序使用CSplitterWnd创建的多个视图的动态地显示和关闭视图
- C++--如何实现SDI程序使用CSplitterWnd创建的多个视图的动态地显示和关闭视图
- 如何在C++中实现Deprecated API
- 如何理解c和c++的复杂类型声明
- c++中如何用string实现CString格式化的功能
- 从易到难编写C++程序,(3)问题:实现一个复杂的猜数字游戏
- 如何理解c和c++的复杂类型声明
- 高质量c++(如何在派生类中实现类的基本函数)
- 如何实现真正的动态包含
- C++中的property库的设计与实现过程(二)——如何为属性指定get_和set_函数?
- 在c++中如何实现非consle类型的计时器
- BNF范式 如何用C++实现自动推导
- 如何实现返回记录集的 Visual C++ COM 对象
- 如何多次包含一个含有实现的头文件
- C++--如何实现SDI程序使用CSplitterWnd创建的多个视图的动态地显示和关闭视图