有限状态机:游戏中的有限状态机(InGems)



这是GAMEGEMS中第 3章部分不好你可以直接阅读原文原本以为这是人工智能部分看到半才发现只是个简单框架如果你想学人工智能这里没有就不要浪费时间了由于本人水平有限其中难免会出现原则性希望指正

关键字:有限状态机、状态、输入、状态转换、输出状态当前状态

个有限状态机类
在这篇文章中我们创建了个通用有限状态机(FSM)C有限状态机是计算机科学和数学理论抽象它在许多方面是很有用处这里我们不去讲解有限状态机理论上知识而是讲如何实现个“有限状态机”“有限状态机”在游戏人工智能方面是很有用处

“有限状态机”是由有限状态组成个机制个“状态”就是个状况你考虑下门;它“状态”有“开”或“关”以及“锁”和“未锁”

对于个“有限状态机”它应该有个“输入”这个“输入”可以影响“状态转换”“有限状态机”应该有个简单(或复杂)状态转换这个可以决定什么状态可以变成“当前状态”

这个当前新状态被称为“有限状态机”“状态转换”“输出状态”如果你对这个概念有些迷惑就把“门”做为理解“有限状态机”例子个“门”处于“关闭”状态和“锁”状态当你输入了“使用钥匙”时状态可以变成“未锁”状态(即“状态转换”输出状态也就是门当前状态)当你输入了“使用手”时状态可以转换成“开”状态当门处于“开”状态时我们输入“使用手”时会使门状态重新回到“关”状态当“门”处于“关”状态时我们输入“使用钥匙”时这将会使门重新回到“锁”状态当门处于“锁”状态我们输入“使用手”就不能把门状态转换到“开”状态门仍然会保持“锁”状态还有当门处于“开”状态时我们输入“使用钥匙”是不能把门状态转换成“锁”状态

总的“有限状态机”是个有限状态组成其中个状态是“当前状态”“有限状态机”可以接受个“输入”这个“输入”结果将导致个“状态转换”发生(即从“当前状态”转换到“输出”状态)这个“状态转换”是基于“状态转换状态转换完成的后“输出状态”即变成了“当前状态”

输入 状态转换
当前状态-----状态转换-------------输出状态(当前状态)

那么人们是如何将这个概念应用到游戏AI系统中呢?有限状态机功能很多:管理游戏世界、模拟NPC思维、维护游戏状态、分析玩游戏输入或者管理个对象状态

假如在个冒险游戏中有个NPC名字可以叫MONSTER我们可以先假设这个MONSTER在游戏中有如下状态:BERSERK、RAGE、MAD、ANNOYED以及UNCARING(前几个状态不好分别)假设MONSTER对于区别状态可以执行区别操作并且假设你已经有了这些区别操作代码我们这时可以使用“有限状态机”来模拟这个MONSTER行为了只要我们给出区别“输入”MONSTER就会做出区别反应我们再来指出这些“输入”是什么:PLAYER SEEN、PLAYER ATTACKS、PLAYERGONE、MONSTER HURT、MONSTER HEALED这样我们可以得到个状态转换表格如下:游戏状态转换表:

当前状态 输入 输出状态
UNCARING PLAYER SEEN ANNOYED
UNCARING PLAYER ATTACKS MAD
MAD MONSTER HURT RAGE
MAD MONSTER HEALED UNCARING
RAGE MONSTER HURT BERSERK
RAGE MONSTER HEALED ANNOYED
BERSERK MONSTER HURT BERSERK
BERSERK MONSTER HEALED RAGE
ANNOYED PLAYER GONE UNCARING
ANNOYED PLAYER ATTACKS RAGE
ANNOYED MONSTER HEALED UNCARING

根据上面这个表格我们可以很容易画出个MONSTER“状态转换图”MONSTER个状态就是图中顶点
因此根据当前状态和对FSM输入MONSTER状态将被改变这时根据MONSTER状态执行相应操作代码(假设已经实现)将被执行这时MONSTER好像是具备了人工智能显然我们可以定义更多“状态”写出更多“输入”写出更多“状态转换”这样MONSTER可以表现更真实生动当然这些游戏规则问题应该是策划制定

FSM以及FSMstate

现在我们如何把这些思路方法变成现实?使用FSM和它组成部分FSMstate可以实现这些想法

定义FSMstate
FSMstate
{
unsigned m_usNumberOfTransition; //状态最大数
* m_piInputs; //为了转换而使用输入
* m_piOutputState; //输出状态
m_iStateID; //这个状态标识符
public:
//个构造可以接受这个状态ID和它支持转换数目
FSMstate( iStateID,unsigned usTransitions);
//析构清除分配
~FSMstate;
//取这个状态ID
GetID{ m_iStateID;}
//向中增加状态转换
void AddTransition( iInput, iOutputID);
//从中删除个状态转换
void DeleteTransition( iOutputID);
//进行状态转换并得到输出状态
GetOutput( iInput);
};

对这个类分析:
功能:主要是实现和个状态相关各种操作我们前面假设了MONSTER各种状态:
# STATE_ID_UNCARING 1
# STATE_ID_MAD 2
# STATE_ID_RAGE 3
# STATE_ID_BERSERK 4
# STATE_ID_ANNOYED 5

状态转换所需输入有:
# INPUT_ID_PLAYER_SEEN 1
# INPUT_ID_PLAYER_ATTACK 2
# INPUT_ID_PLAYER_GONE 3
# INPUT_ID_MONSTER_HURT 4
# INPUT_ID_MONSTER_HEALED 5

以上是 5个状态标识符
我们就要声明5个FSMstate例子个例子代表个状态和和的有关操作假设我们先处理状态STATE_ID_MAD
类成员变量m_iStateID就等于STATE_ID_MAD类成员变量m_usNumberOfTransition就是可由这个状态转换成状态个数前面有个表其中有两个状态可以由这个状态产生它们分别是STATE_ID_UNCARING和STATE_ID_RAGE
这时m_usNumberOfTransition等于2



m_piInputs是个指针变量它保存和这个状态相关输入在前面表中我们知道和STATE_ID_MAD相关输入为
INPUT_ID_MONSTER_HURT和INPUT_ID_MONSTER_HEALED因此m_piInputs中存放是这两个数据
而m_piOutputState存放是和STATE_ID_MAD相关状态即STATE_ID_RAGE和STATE_ID_UNCARING这样m_piOutputState中存放数据便是这两个值

以上是对成员变量解释下面解释成员:
构造
FSMstate::FSMstate( iStateID,unsigned usTransitions)
{
(!usTransitions) //如果给出转换数量为0就算为1
m_usNumberOfTransitions=1;

m_usNumberOfTransitions=usTransitions;

//将状态ID保存起来
m_iStateID=iStateID;

//分配内存空间
try
{
m_piInputs= [m_usNumberOfTransitions];
for( i=0;i<m_usNumberOfTransitions;i)
m_piInputs[i]=0;
}
catch(...)
{
throw;
}
try
{
m_piOutputState= [m_usNumberOfTransition];
for( i=0;i<m_usNumberOfTransitions;i)
m_piOutputState[i]=0;
}
catch(...)
{
delete m_piInputs;
throw;
}
}

这就是构造在FSMstate类中共有 4个成员变量在这个中全部被化了FSMstate是个类是否还记得MONSTER状态(如MAD、UNCARING)这个类就是实现对MONSTER个状态管理假如这个状态是STATE_ID_MAD 和这个状态相关状态有两个上面已经讲过了这时我们给成员变量赋值在这个具体例子中它们值如下:

m_usNumberOfTransition=2
m_piInput[0]=0;
m_piInput[1]=0;
m_piOutputState[0]=0;
m_piOutputState[1]=0;
m_iStateID=STATE_ID_MAD;

析构:
FSMState::~FSMState
{
delete m_piInputs;
delete m_piOutputState;
}
析构将动态分配存储空间释放了

void FSMstate::AddTransition( iInput, iOutputID)
{
for( i=0;i<m_usNumberOfTransitions;i)
(!m_piOutputState[i]) ;
(i<m_usNumberOfTransition)
{
m_piOutputState[i]=iOutputID;
m_piInputs[i]=iInput;
}
}

这个给两个前面构造动态分配空间加入数据首先要找到两个中找到适当位置的后如果位置是合法
我们就可以把数据加入这两个STATE_ID_MAD和两个状态有关因此我们可以两次这个把这两个状态加入到类中:

AddTransition(INPUT_ID_MONSTER_HURT,STATE_ID_RAGE);
AddTransition(INPUT_ID_MONSTER_HEALED,STATE_ID_UNCARING)
这样和状态STATE_ID_MAD相关“状态”和“输入”也加入了

void FSMstate::DeleteTransition( iOutputID)
{
// 遍历每个输出状态
for( i=0;i<m_usNumberOfTransitions;i)
{
//如果找到输出状态退出循环
(m_piOutputState[i]iOutputID)
;
}
//如果没有找到输出状态返回
(i>=m_usNumberOfTransitions)
;
//将输出状态内容置0
m_piInputs[i]=0;
m_piOutputState[i]=0;

//被删除输出状态后面输出状态前移
for(;i<(m_usNumberOfTransition-1);i)
{
(!m_piOUtputState[i])
;
m_piInputs[i]=m_piInputs[i+1];
m_piOutputState[i]=m_piOutputState[i+1];
}
//最后面输出状态置0
m_piInputs[i]=0;
m_piOutputState[i]=0;
}

这个是要删除和个状态相关输出状态个状态STATE_ID_MAD和的相关状态有两个STATE_ID_RAGE,STATE_ID_UNCARING当然这是经过化以及前面添加状态的后产生了这两个相关状态你想删除哪个?如果你想删除相关输出状态只要在删除中指出那个状态即可例如:

DeleteTransition(STATE_ID_RAGE);
你就可以删除输出状态STATE_ID_RAGE了

FSMstate::GetOutput( iInput)
{
//先给输出状态赋值(如果未找到和输入对应输出状态时返回这个值)
iOutputID=m_iStateID;

//遍历输出状态
for( i=0;i<m_usNumberOfTransitions;i)
{
//如果没找到退出循环
(!m_piOutputState[i])
;
//如果找到了和“输入”相对应“输出状态”进行赋值
(iInputm_piInputs[i])
{
iOutputID=m_piOutputState[i];
;
}
}
//返回“输出状态”
(iOutputID);
}

这个功能是返回和“输入”相对应“输出状态”标识如果没有和“输入”相对应“输出状态”返回原来状态如果有和的对应“输出状态”返回这个状态ID

下面定义是FSM这个类用于维护FSMstate对象集合
FSM
{
State_Map m_map; //包括了状态机所有状态
m_iCurrentState; //当前状态ID
public:
FSM( iStateID); //化状态
~FSM
//返回当前状态ID
GetCurrentState { m_iCurrentState;}
//设置当前状态ID
void SetCurrentState( iStateID) {m_iCurrentState=iStateID;}
//返回FSMstate对象指针
FSMstate* GetState( iStateID);
//增加状态对象指针
void AddState(FSMstate* pState);
//删除状态对象指针
void DeleteState( iStateID);
//根据“当前状态”和“输入”完成“状态”转换
StateTransition( iInput);
};



FSM::m_map是FSMstate对象集合是从STL<map>中实现
FSM::m_iCurrentState是FSMstate对象状态标识是“有限状态机”当前状态

FSM::GetCurrentState可以用的访问当前FSMstate对象状态标识符
FSM::SetCurrentState可以设置当前FSMstate对象状态标识符
FSM::GetState可以取得有限状态机中任何FSMstate对象指针
FSM::AddState增加有限状态机中FSMstate对象
FSM::DeleteState删除有限状态机中FSMstate对象
FSM::StateTransition化状态转换根据输入返回输出状态

这个类使用了STL我不知道它如何用:)听说是高人才使用它高人起码要写过上万行代码因此不能详细介绍这个类了

总的可以这么理解这两个类FSMstate,FSM.FSMstate代表了个状态以及和状态相关数据和操作如在MONSTER中有 5个状态我们就要声明 5个类对象每个对象中包括了和这个状态相关状态输入和各种转换可以说FSMstate是对每个状态封装(包括相关数据和操作)游戏中对象有多少状态就要声明多少个FSMstate对象而FSM则是对这若干个FSMstate对象(这个例子中MONSTER有 5个状态)进行封装在FSM中指明了若干个FSMstate中哪个是当前MONSTER拥有状态并且可以设置得到以及删除状态并且可以进行状态间转换

总的:游戏中MONSTER有多少状态游戏中就要声明多少FSMstate对象个FSMstate对象包括了和特定状态相关数据和操作而FSM只有它用于协调若干个FSMstate的间关系和操作

下面是如何在游戏中使用两个类例子:

首先是创建FSMstate对象(若干个)有多少状态就要循环多少次下面是增加STATE_ID_UNCARING状态例子:
FSMstate* pFSMstate=NULL;
//创建状态
try
{
//第个参数是增加状态标识第 2个参数指明了和这个
//状态相关状态个数
pFSMstate= FSMstate(STATE_ID_UNCARING,2);
}
catch(...)
{
throw;
}

//的后给这个状态加入相关“输入”和“输出状态”
pFSMstate->AddTransition(INPUT_ID_PLAYER_SEEN,STATE_ID_ANNOYED);
pFSMstate->AddTransition(INPUT_ID_PLAYER_ATTACKS,STATE_ID_MAD);

这个指明了和特定状态相关“输入”和“输出状态”
比如第它表明如果我要输入个INPUT_ID_PLAYER_SEEN这时就会产生个输出状态STATE_ID_ANNOYED
我们应该为每个状态做上面事情这里就略过了的后我们要声明个FSM对象用于协调上面FSMstate对象的间关系

try
{
m_pFSM= FSM(STATE_ID_UNCARING);
}
catch(...)
{
throw;
}

上面指明了MONSTER当前状态是STATE_ID_UNCARING最后将FSMstate对象分别加入到FSM

下面介绍如何使用FSM
使用十分简单只要我们给出个“输入”的后我们便可以得到个“输出状态”根据这个“输出状态”我们执行相应操作最后把这个“输出状态”变成MONSTER当前状态

在游戏中发生了些事情如玩游戏人指出他控制人进攻MONSTER(用鼠标点击了MONSTER)这时会产生个“输入”iInputID=INPUT_ID_PLAYER_ATTACK;

这时我们状态转换:
m_iOutputState=m_pFSM->StateTransition(iInputID);

这时我们“输入”对MONSTER产生了刺激产生了个“输出状态”这时我们根据这个输出状态相应代码执行就可以了这时MONSTER好像有应反了我们说它有了简单智能

(m_iOutputStateSTATE_ID_MAD)
{
//some code for the monster to act mad
}

当然我们也应该把其它状态执行操作也写出来但只写个就可以了使用这个状态机就是这么简单总的FSM不是全部人工智能相反它只是个框架个开始智能需要还很多只要你可以分出“状态”并且知道什么“输入”产生什么“输出状态”就可以了当然这是个游戏规则策划应当完成这个部分?

Tags:  状态机 有限状态机

延伸阅读

最新评论

发表评论