最近需要写一个简单状态机用作动画的状态机所以写了这样的一个东西。
由于动画状态机一般用简单的true或者false来作为transition,所以我决定使用一个整数来描述所有的条件变化情况,整数的每一个位代表一个条件,从而来描述当前的状态。
基本思路是使用一个数据结构(Desc)来描述状态机的逻辑,状态机中所有的状态节点和条件值都有一个索引。
每个状态机都对应一个Desc,同时状态机中保存当前状态的索引和条件值,对状态机的操作都通过修改条件值来完成,改变条件,由Desc来驱动状态机,改变当前状态。
状态机主要有以下几个部分组成
1.BitFSM
实际状态机只是一个简单的数据集合,它主要保存了当前的状态的索引和各个条件的值,而状态机的逻辑由BitFSMDesc来描述。通过改变状态机的条件值来切换各个状态。
2.BitFSMDesc
主要用于描述状态机的逻辑,构造分配State节点和Transition,管理条件。
3.State
用于描述状态进入(Enter)、退出(Exit)和更新(Update)时的行为。
4.StateNode
状态节点中保存了一个State,同时还保存了这个状态节点的所有的Transition。每个StateNode都有一个唯一的ID,由Desc分配,从而FSM能通过Desc来找到当前的状态。
5.Transition
状态切换的条件。
实现上,为了通用性,我使用模板类来构建这个状态机。
1. StateNode 和 Transition
// ============================================================================
/*! \brief BitFSMStateNode_Simple
*/
// ============================================================================
template<typename TFSM, typename TState,typename IdType,typename CondSet>
class BitFSMStateNode_Simple
{
public:
typedef BitFSMStateNode_Simple StateNode;
// ============================================================================
/*! \brief BitFSMTransition
*/
// ============================================================================
class Transition
{
public:
Transition() :Transition(-1, true, nullptr) {}
Transition(IdType condId, bool condValue, StateNode* pToState)
:m_iCondId(condId), m_bCondVal(condValue), m_pToState(pToState)
{}
IdType m_iCondId;
bool m_bCondVal;
StateNode* m_pToState;
};
BitFSMStateNode_Simple(const NameString& name, IdType id)
: m_stateName(name)
, m_iID(id)
, m_pState(nullptr)
{}
const char* GetStateName()const { return m_stateName.c_str(); }
void AddTransition(IdType condId, bool condValue, StateNode* pToState)
{
m_vTransitions.emplace_back(condId, condValue, pToState);
m_CondFilter.Set(condId);
m_CondMask.Set(condId, !condValue);
}
inline StateNode* UpdateTransition(const CondSet& condSet, IdType& condId)const
{
if (AnyConditiontriggered(condSet))
{
for (const auto& transition : m_vTransitions)
{
if (condSet[transition.m_iCondId] == transition.m_bCondVal)
{
condId = transition.m_iCondId;
return transition.m_pToState;
}
}
}
return nullptr;
}
void Update(TFSM& fsm, float dt)const
{
if (m_pState) m_pState->Update(fsm, dt);
}
void EnterNode(TFSM& fsm)const
{
if (m_pState) m_pState->Enter(fsm);
}
void ExitNode(TFSM& fsm)const
{
if (m_pState) m_pState->Exit(fsm);
}
void SetState(TState* pState) { m_pState = pState; }
IdType GetID()const { return m_iID; }
TState* GetState() { return m_pState; }
Transition* GetLastTransition() { return &(m_vTransitions.back()); }
const Vector<Transition>& GetAllTransitions()const { return m_vTransitions; }
inline bool AnyConditiontriggered(const CondSet& curValue)const
{
CondSet maskedValue = curValue.Xor(m_CondMask);
return m_CondFilter&maskedValue;
}
private:
NameString m_stateName;
Vector<Transition> m_vTransitions;
TState* m_pState;
IdType m_iID;
CondSet m_CondMask;
CondSet m_CondFilter;
};
首先是Transition,它主要保存了条件的ID,需要的条件值(True或者False),以及对应的状态机节点。
然后是StateNode。包含了状态机运行的主要逻辑。主要看UpdateTransition
首先判断这个state中是否有Transition触发,再遍历所有的Transition,找到触发的那个Transition,退出当前State进入下一个State。
前面提到状态机中的状态和条件都有一个对应的index,这个index是从0开始的正整数。
假设有状态机状态如下
Index | State Name |
---|---|
0 | Idle |
1 | Run |
2 | Jump |
3 | Attack |
条件列表如下
Index | Condition Name |
---|---|
0 | IsRunning |
1 | IsJumpping |
2 | IsAttacking |
看AddTransition函数,除了使用m_vTransitions.emplace_back(condId, condValue, pToState) 来构造新的Transition之外,还有两个操作。
m_CondFilter.Set(condId);
m_CondMask.Set(condId, !condValue);
这两个是用来判断是否有Transition触发的。这里的CondSet实际上可以简答的看做是一个整数,我把一个32bit的整数封装成了简单的BitSet,用来方便的操作每个位。这里Set就是将制定位置0或者置1。
对Idle状态,有三个Transition,如下
IsRunning —>Run
IsAttack —>Attack
IsJump —>Jump
所以这里的m_CondFilter的值应为0111,同理m_CondMask的值为0000。
看函数AnyConditiontriggered,
对任意的状态,我们都只关心和这个状态有关的条件值,所以用m_CondFilter来过滤掉那些不需要的条件值。
而m_CondMask是我们需要的条件值的集合,对所有的Transition,只要有任何一个Transition满足条件,那么就会触发。
所以这里先进行一次Xor操作,假设当前状态机的条件值是1001,则异或之后的结果应该是1001^0000=1001
之后再用Filter进行一次且运算得到的结果为0001>0,所以在此情况下有Transition触发,然后再遍历当前State的Transition,找到触发并返回新的State,这就是整个UpdateTransition的过程。
- BitFSMDesc和 BitFSM
代码如下
// ============================================================================
/*! \brief BitFSMDesc_Simple
// Describe the behavior of the FSM.
*/
// ============================================================================
template<class TState, char Inverter = '!'>
class BitFSMDesc_Simple
{
public:
typedef UInt8 IdType;
typedef Int8 SIdType;
typedef BitField<UInt> CondSet;
private:
static constexpr char* sm_ConditionIsOver = "IFSMIsOver";
static constexpr char* sm_ConditionUnnamed = "IFSMUnnamed%d";
static constexpr IdType sm_InvalidID = -1;
public:
// ============================================================================
/*! \brief BitFSM
*/
// ============================================================================
class BitFSM
{
friend class BitFSMDesc_Simple;
public:
BitFSM() :m_iCurrentState(0), m_pDesc(nullptr), m_iLastTriggerCond(0)
{}
void ResetFSM()
{
m_iCurrentValue.ClearAll();
m_iCurrentState = 0;
m_iLastTriggerCond = 0;
m_pDesc = nullptr;
}
void UpdateFSM(float dt)
{
if (m_pDesc)
{
m_pDesc->UpdateFSM(*this, dt);
}
}
void SetDesc(const BitFSMDesc_Simple* pDesc)
{
m_pDesc = pDesc;
}
const BitFSMDesc_Simple* GetDesc()const
{
return m_pDesc;
}
TState* GetCurrentState() {
return m_pDesc->GetStateById(m_iCurrentState);
}
//Change the value of the condition,0 is always used as the IsOver Condition.
inline void SetValueById(IdType valIndex, bool bVal)
{
if (bVal) {
m_iCurrentValue.Set(valIndex);
}
else {
m_iCurrentValue.Clear(valIndex);
}
}
inline void SetValueByName(const StringID &valUId, bool bVal)
{
IdType uid = m_pDesc->GetConditionIndex(valUId);
SetValueById(uid, bVal);
}
inline bool GetValueById(IdType valIndex)const
{
return m_iCurrentValue[valIndex];
}
inline bool GetValueByName(const StringID &valUId)const
{
IdType uid = m_pDesc->GetConditionIndex(valUId);
return GetValueById(uid);
}
//! \brief Set the state is over, will auto clear when enter new state.
inline void SetStateIsOver()
{
m_iCurrentValue.Set(0);
}
inline void ClearStateIsOver(bool bIsOverTriggered)
{
m_iCurrentValue.Clear(0);
if (bIsOverTriggered)
{
m_iCurrentValue.Set(m_iLastTriggerCond, !m_iCurrentValue[m_iLastTriggerCond]);
}
}
void DebugDraw()const
{
m_pDesc->DebugDraw(*this);
}
private:
IdType m_iCurrentState;
IdType m_iLastTriggerCond;
CondSet m_iCurrentValue;
const BitFSMDesc_Simple* m_pDesc;
};
private:
typedef BitFSMStateNode_Simple<BitFSM, TState, IdType, CondSet> StateNode;
// ============================================================================
/*! \brief ConditionParser
*/
// ============================================================================
class ConditionParser
{
public:
IdType id;
bool isTrue;
ConditionParser(BitFSMDesc_Simple& desc, const char* conditionName)
{
isTrue = (conditionName[0] != Inverter);
if (!isTrue)
{
++conditionName;
}
id = desc.GetOrCreateCondition(conditionName);
}
ConditionParser(BitFSMDesc_Simple& desc, SIdType sID)
{
isTrue = (sID >= 0);
id = isTrue ? static_cast<IdType>(sID) : static_cast<IdType>(-sID);
id = desc.GetOrCreateCondition(id);
}
};
friend class BitFSM;
friend class ConditionParser;
public:
BitFSMDesc_Simple():m_pBuildingNode(nullptr)
{
CreateCondition(sm_ConditionIsOver);
}
BitFSMDesc_Simple& Begin(const char* pStateName)
{
Assert(m_pBuildingNode == nullptr, "Try to Begin state %s before state %s End.", pStateName, m_pBuildingNode->GetStateName());
m_pBuildingNode = GetOrCreateNode(pStateName);
return *this;
}
TState& SetState(TState* pState)
{
Assert(m_pBuildingNode, "No node in building, forget Begin?");
Assert(pState, "Don't set empty ptr as state, prepare to crahs.");
m_pBuildingNode->SetState(pState);
return *pState;
}
//! \brief Will create unnamed condition
BitFSMDesc_Simple& AddTransition(SIdType conditionId, const char* pToStateName)
{
ConditionParser parser(*this, conditionId);
StateNode *pToState = GetOrCreateNode(pToStateName);
m_pBuildingNode->AddTransition(parser.id,parser.isTrue, pToState);
return *this;
}
//! \brief Will create condition will specified conditionName, and auto give it an id
BitFSMDesc_Simple& AddTransition(const char* conditionName, const char* pToStateName)
{
ConditionParser parser(*this, conditionName);
StateNode *pToState = GetOrCreateNode(pToStateName);
m_pBuildingNode->AddTransition(parser.id, parser.isTrue, pToState);
return *this;
}
//! \brief Add a transition on state is over.It will reset the entry transition condition of this state to invalid value.
BitFSMDesc_Simple& AddOnStateIsOver(const char* pToStateName)
{
return AddTransition((SIdType)0, pToStateName);
}
void End()
{
Assert(m_pBuildingNode, "No node in building, forget Begin?");
m_pBuildingNode = nullptr;
}
IdType GetConditionIndex(const StringID &condUid)const
{
auto cIt = m_nameToCondMap.find(condUid);
return cIt->second;
}
private:
void UpdateFSM(BitFSM& fsm, float dt)const
{
const StateNode& curState = *(m_vStateList[fsm.m_iCurrentState]);
//////////////////////////////////////////////////////////////////////////
curState.Update(fsm, dt);
//////////////////////////////////////////////////////////////////////////
IdType triggeredCond(sm_InvalidID);
StateNode* pToState = curState.UpdateTransition(fsm.m_iCurrentValue, triggeredCond);
if (pToState)
{
fsm.m_iCurrentState = pToState->GetID();
//////////////////////////////////////////////////////////////////////////
curState.ExitNode(fsm);
fsm.ClearStateIsOver(triggeredCond==0);
pToState->EnterNode(fsm);
//////////////////////////////////////////////////////////////////////////
fsm.m_iLastTriggerCond = triggeredCond;
}
}
StateNode* GetOrCreateNode(const char* pName)
{
NameString name(pName);
auto foundIt = m_nameToStateMap.find(name.GetUniqueID());
if (foundIt == m_nameToStateMap.end())
{
StateNode* pNode = new StateNode(name, (IdType)m_vStateList.size());
m_vStateList.push_back(pNode);
m_nameToStateMap[name.GetUniqueID()] = pNode;
return pNode;
}
else
{
return foundIt->second;
}
}
IdType GetOrCreateCondition(const char* pName)
{
StringID nameID(pName);
auto foundIt = m_nameToCondMap.find(nameID);
if (foundIt == m_nameToCondMap.end())
{
return CreateCondition(pName);
}
else
{
return foundIt->second;
}
}
IdType GetOrCreateCondition(IdType condID, const char* pCondName = nullptr)
{
if (m_vConditionList.size() <= condID || m_vConditionList[condID].empty())
{
return CreateCondition(condID, pCondName);
}
else
{
return condID;
}
}
IdType CreateCondition(IdType condID, const char* pCondName = nullptr)
{
if (pCondName == nullptr)
{
pCondName = FormatString(sm_ConditionUnnamed, condID);
}
if (condID == sm_InvalidID)
{
condID = static_cast<IdType>(m_vConditionList.size());
}
if (m_vConditionList.size() <= condID)
{
m_vConditionList.resize(condID + 1);
}
StringID nameID(pCondName);
RESLOVE_STRID(nameID);
Assert(m_vConditionList[condID].empty()
, "ID %d is used as name %s,overwrite it to %s will cause unexpected result!"
, condID, m_vConditionList[condID].c_str(), pCondName);
Assert(m_nameToCondMap.find(nameID) == m_nameToCondMap.end()
, "Name %s has assigned as ID %d,overwrite it to %d will cause unexpected result! "
, pCondName, m_nameToCondMap[nameID], condID);
m_nameToCondMap[nameID] = condID;
m_vConditionList[condID] = pCondName;
return condID;
}
IdType CreateCondition(const char* pCondName)
{
return CreateCondition(sm_InvalidID, pCondName);
}
TState* GetStateById(IdType id)const
{
return m_vStateList[id]->GetState();
}
void DebugDraw(const BitFSM& fsm)const;
Vector<String> m_vConditionList;
HashMap<StringID, IdType> m_nameToCondMap;
Vector<StateNode*> m_vStateList;
HashMap<StringID, StateNode*> m_nameToStateMap;
StateNode* m_pBuildingNode;
};
虽然看起来有点长,但只看主要的部分。
这里的Desc主要负责创建新的State和Condition。为了方便使用,可以通过id或者名字,或者直接匿名创建一个condition。
大部分的代码还是用于方便使用者在代码中书写状态机而使用的。
最终在代码中的使用差不多如下所示。
m_animFSMDesc.Begin("Idle").SetState(new AnimState(AnimateTab::sm_Idle));
m_animFSMDesc.AddTransition("IsRun", "Run");
m_animFSMDesc.AddTransition("IsAttack", "Attack");
m_animFSMDesc.AddTransition("IsJump", "Jump");
m_animFSMDesc.End();
m_animFSMDesc.Begin("Attack").SetState(new AnimState(AnimateTab::sm_Attack));
m_animFSMDesc.AddTransition("!IsAttack", "Idle");
m_animFSMDesc.AddTransition("IsJump", "Jump");
m_animFSMDesc.End();
m_animFSMDesc.Begin("Run").SetState(new AnimState(AnimateTab::sm_Run));
m_animFSMDesc.AddTransition("!IsRun", "Idle");
m_animFSMDesc.AddTransition("IsAttack", "Attack");
m_animFSMDesc.AddTransition("IsJump", "Jump");
m_animFSMDesc.End();
m_animFSMDesc.Begin("Jump").SetState(new AnimState(AnimateTab::sm_Attack, 1));
m_animFSMDesc.AddOnStateIsOver("Idle");
m_animFSMDesc.End();
拓展
这个简单的状态机每个Transition只能接受一个条件,能否让它能够接受多个条件并处理一些简单的逻辑关系呢?
实际上我写了一个这样的版本,但个人还是比较喜欢简单的版本,所以不做介绍。大概叙述一下我的想法。
对于多个条件,假如他们都是或的关系,那么首先我们同样需要一个Filter来筛选我们需要的位。
如何判断条件成立呢?实际上判断方法同AnyConditiontriggered,进行一次异或和与操作,再判断是否大于0即可。
如果条件之间是与的关系,那么同样,我们需要一次异或和与操作,只不过这里的Condition Mark和前面的不同。假设我们需要的条件是0101,那么对应的Condition Mark也是0101。
假设条件成立,则此时的条件值应是0101,(0101^0101)&0111的结果是0。
假设条件不成立,则(xxxx^0101)&0111的结果必然不为0,由此可以判断是否条件成立。
进一步,对应与和或下的Condition Mark的值是不同的,这显然不太方便维护,让我们统一一下,都使用与关系下的Condition Mark。
注意到,假设我们需要的条件是0101,那么对应的Condition Mark也是0101。
假设所有的条件都不成立,则条件值应该是0010,(0010^0101)&0111=0111,也就是说,任意条件触发的要求是结果不等于Condition Filter,所以可以综合起来写成如下的函数
bool IsTriggered(const CondSet& fsmCondState)const
{
CondSet xOr = fsmCondState.Xor(m_CondMask);
CondSet result = m_CondFilter.And(xOr);
return m_bIsLogicAnd ? result.IsEmpty() : result != m_CondFilter;
}
这样就能比较好的统一整个逻辑了。
当然如果你还想支持再复杂的逻辑?可能我觉得大概需要别的状态机的实现了。这个状态机的目的就是为了轻量高效的描述动画状态机这种简单的状态机,并不是为了一些复杂的状态机准备的。
当然如果一定要用这种方法,可以考虑用多个Transition合并成一个的方式,那样子就能支持复杂的逻辑了。不过在我看来,不如换成别的实现要方便得多。