有限状态机在游戏开发中是经常用到的数学模型,虽然我做的是"万年捕鱼",但是有幸在2014年用Unity开发《怒海潜江》(已经成为线上尸体)中研究学习了有限状态机并且在项目中成功使用,对BOSS状态的控制让我惊艳到了(虽然是自己开发的,哈哈)。时至今日很多时候都在做重复的工作,最近的招聘好多应聘者都谈到了有限状态机这个东西,那么就重新来学习一次吧。
有限状态机,(
英语
:Finite-state machine, FSM),又称
有限状态自动机
,简称状态机,是表示有限个
状态
以及在这些状态之间的转移和动作等行为的
数学模型
。-------《百度百科》
首先我们谈谈对"有限状态自动机"这个名词的理解。首先"状态",就是指某个对象有多种状态,我们举个游戏中的例子,怪物有待机,行走,攻击,死亡等等这几个状态;“有限状态”就是指状态是有限个的,这个也很好理解;"自动机"的意思是说具有一定的AI能力。很多人都写过FSM,但是大多数人都是自己来控制状态的切换,并没有真正做到自动(AI)的程度。在有限个状态中自动切换,实现一个状态转换的死循环。
状态机由下列几部分组成:
状态集(States) ,事件(Event), 动作(Action), 转换(Transition)。
我们用实例一步一步来看FSM。如果让我们来实现一个游戏中的怪物,需求中怪物有饥饿,觅食,战斗,逃跑,玩耍这些状态 :
如果用(if else)或者(switch case)来实现的话,那么在Update中就充斥着大量的逻辑判断和跳转,如果增加一个状态或者减少一个状态,那么就要对应修改大量的逻辑判断,很容易造成bug,违反了高内聚低耦合的设计思维。
首先我们引入状态模式,把我们要实现的怪物状态进行封装:
#pragma once
#include "../HrState/State.h"
class HrFSMState : State
{
public:
HrFSMState();
~HrFSMState();
virtual void Enter() override;
virtual void Execute() override;
virtual void Exit() override;
};
#pragma once
#include <memory>
#include "HrFSMState.h"
class HrMonsterEntity;
class HrMonsterState : public HrFSMState
{
public:
HrMonsterState(std::shared_ptr<HrMonsterEntity> pEntity);
~HrMonsterState();
virtual void Enter() override;
virtual void Execute() override;
virtual void Exit() override;
protected:
std::shared_ptr<HrMonsterEntity> m_pStateOwnerEntity;
};
class HrMonsterHungryState : public HrMonsterState
{
public:
HrMonsterHungryState(std::shared_ptr<HrMonsterEntity> pEntity);
~HrMonsterHungryState();
virtual void Enter() override;
virtual void Execute() override;
virtual void Exit() override;
};
class HrMonsterPlayState : public HrMonsterState
{
public:
HrMonsterPlayState(std::shared_ptr<HrMonsterEntity> pEntity);
~HrMonsterPlayState();
virtual void Enter() override;
virtual void Execute() override;
virtual void Exit() override;
};
class HrMonsterFightState : public HrMonsterState
{
public:
HrMonsterFightState(std::shared_ptr<HrMonsterEntity> pEntity);
~HrMonsterFightState();
virtual void Enter() override;
virtual void Execute() override;
virtual void Exit() override;
};
class HrMonsterEatState : public HrMonsterState
{
public:
HrMonsterEatState(std::shared_ptr<HrMonsterEntity> pEntity);
~HrMonsterEatState();
virtual void Enter() override;
virtual void Execute() override;
virtual void Exit() override;
};
class HrMonsterEscapeState : public HrMonsterState
{
public:
HrMonsterEscapeState(std::shared_ptr<HrMonsterEntity> pEntity);
~HrMonsterEscapeState();
virtual void Enter() override;
virtual void Execute() override;
virtual void Exit() override;
};
class HrMonsterForagingState : public HrMonsterState
{
public:
HrMonsterForagingState(std::shared_ptr<HrMonsterEntity> pEntity);
~HrMonsterForagingState();
virtual void Enter() override;
virtual void Execute() override;
virtual void Exit() override;
};
#include "stdafx.h"
#include "HrMonsterState.h"
#include "HrMonsterEntity.h"
#include "HrRandomUtil.h"
#include <iostream>
HrMonsterState::HrMonsterState(std::shared_ptr<HrMonsterEntity> pEntity)
{
m_pStateOwnerEntity = pEntity;
}
HrMonsterState::~HrMonsterState()
{
}
void HrMonsterState::Enter()
{
}
void HrMonsterState::Execute()
{
}
void HrMonsterState::Exit()
{
}
// HungryState /
HrMonsterHungryState::HrMonsterHungryState(std::shared_ptr<HrMonsterEntity> pEntity) : HrMonsterState(pEntity)
{
}
HrMonsterHungryState::~HrMonsterHungryState()
{
}
void HrMonsterHungryState::Enter()
{
std::cout << " HungryState: Enter" << std::endl;
}
void HrMonsterHungryState::Execute()
{
std::cout << " HungryState: 饿了" << std::endl;
m_pStateOwnerEntity->ChangeState(HrMonsterEntity::ENUM_STATE_FORAGING);
}
void HrMonsterHungryState::Exit()
{
std::cout << " HungryState: Exit" << std::endl;
}
PlaySate ///
HrMonsterPlayState::HrMonsterPlayState(std::shared_ptr<HrMonsterEntity> pEntity) : HrMonsterState(pEntity)
{
}
HrMonsterPlayState::~HrMonsterPlayState()
{
}
void HrMonsterPlayState::Enter()
{
std::cout << " PlayState: Enter" << std::endl;
}
void HrMonsterPlayState::Execute()
{
std::cout << " PlayState: Playing 妈妈睡了起来嗨" << std::endl;
if (m_pStateOwnerEntity->IsHungry())
{
m_pStateOwnerEntity->ChangeState(HrMonsterEntity::ENUM_STATE_HUNGRY);
}
else if (HrRandomUtil::GetRandNum(1, 10000) <= 1000)
{
std::cout << " PlayState: 遭遇敌人。。。。" << std::endl;
m_pStateOwnerEntity->ChangeState(HrMonsterEntity::ENUM_STATE_FIGHT);
}
}
void HrMonsterPlayState::Exit()
{
std::cout << " PlayState: Exit" << std::endl;
}
// FightState /
HrMonsterFightState::HrMonsterFightState(std::shared_ptr<HrMonsterEntity> pEntity) : HrMonsterState(pEntity)
{
}
HrMonsterFightState::~HrMonsterFightState()
{
}
void HrMonsterFightState::Enter()
{
std::cout << " FightState: Enter" << std::endl;
}
void HrMonsterFightState::Execute()
{
std::cout << " FightState: Execute" << std::endl;
int nRandNum = HrRandomUtil::GetRandNum(1, 10000);
if (nRandNum < 1000)
{
std::cout << " FightState: 战胜了!" << std::endl;
if (m_pStateOwnerEntity->IsHungry())
m_pStateOwnerEntity->ChangeState(HrMonsterEntity::ENUM_STATE_EAT);
else
m_pStateOwnerEntity->ChangeToPreviousState();
}
else if (nRandNum < 2000)
{
std::cout << " FightState: 战败了!" << std::endl;
m_pStateOwnerEntity->ChangeState(HrMonsterEntity::ENUM_STATE_ESCAPE);
}
else
{
std::cout << " FightState: 战斗中。。。。" << std::endl;
}
}
void HrMonsterFightState::Exit()
{
std::cout << " FightState: Exit" << std::endl;
}
/ FightState
HrMonsterEatState::HrMonsterEatState(std::shared_ptr<HrMonsterEntity> pEntity) : HrMonsterState(pEntity)
{
}
HrMonsterEatState::~HrMonsterEatState()
{
}
void HrMonsterEatState::Enter()
{
std::cout << " EatState: Enter" << std::endl;
}
void HrMonsterEatState::Execute()
{
std::cout << " EatState: Execute 吃" << std::endl;
if (m_pStateOwnerEntity->Eat())
{
m_pStateOwnerEntity->ChangeState(HrMonsterEntity::ENUM_STATE_PLAY);
}
}
void HrMonsterEatState::Exit()
{
std::cout << " EatState: Exit" << std::endl;
}
// EscapeState //
HrMonsterEscapeState::HrMonsterEscapeState(std::shared_ptr<HrMonsterEntity> pEntity) : HrMonsterState(pEntity)
{
}
HrMonsterEscapeState::~HrMonsterEscapeState()
{
}
void HrMonsterEscapeState::Enter()
{
std::cout << " EscapeState: Enter" << std::endl;
}
void HrMonsterEscapeState::Execute()
{
std::cout << " EscapeState: Execute 逃跑中..." << std::endl;
if (HrRandomUtil::GetRandNum(1, 10000) <= 1000)
{
if (m_pStateOwnerEntity->IsHungry())
{
m_pStateOwnerEntity->ChangeState(HrMonsterEntity::ENUM_STATE_HUNGRY);
}
else
{
m_pStateOwnerEntity->ChangeState(HrMonsterEntity::ENUM_STATE_PLAY);
}
}
}
void HrMonsterEscapeState::Exit()
{
std::cout << " EscapeState: Exit" << std::endl;
}
// ForagingState //
HrMonsterForagingState::HrMonsterForagingState(std::shared_ptr<HrMonsterEntity> pEntity) : HrMonsterState(pEntity)
{
}
HrMonsterForagingState::~HrMonsterForagingState()
{
}
void HrMonsterForagingState::Enter()
{
std::cout << " ForagingState: Enter" << std::endl;
}
void HrMonsterForagingState::Execute()
{
std::cout << " ForagingState: Execute 寻找食物中。。。。" << std::endl;
if (HrRandomUtil::GetRandNum(1, 10000) <= 1000)
{
std::cout << " ForagingState: 找到一个小羊羔~ " << std::endl;
m_pStateOwnerEntity->ChangeState(HrMonsterEntity::ENUM_STATE_FIGHT);
}
}
void HrMonsterForagingState::Exit()
{
std::cout << " ForagingState: Exit" << std::endl;
}
对怪物状态的分析,我们针对每个状态进行封装,而具体的状态逻辑也封装在了每一个状态里,状态互不干扰,把耦合降到最低。而控制当前状态的是StateMachine来控制:
#pragma once
#include <memory>
class HrFSMEntity;
class HrFSMState;
class HrFSMStateMachine
{
public:
HrFSMStateMachine();
virtual ~HrFSMStateMachine();
virtual void SetOwnerEntiry(std::shared_ptr<HrFSMEntity> pEntity);
virtual void SetCurrentState(std::shared_ptr<HrFSMState> pState);
virtual void SetPreviousState(std::shared_ptr<HrFSMState> pState);
virtual void ChangeState(std::shared_ptr<HrFSMState> pState);
virtual void ChangeToPreviousState();
virtual void Update();
virtual std::shared_ptr<HrFSMState> GetCurretnState(std::shared_ptr<HrFSMState> pState);
virtual std::shared_ptr<HrFSMState> GetPreviousState(std::shared_ptr<HrFSMState> pState);
protected:
std::shared_ptr<HrFSMEntity> m_pEntityOwer;
std::shared_ptr<HrFSMState> m_pPreviousState;
std::shared_ptr<HrFSMState> m_pCurrentState;
};
具体的实体,即怪物继承状态模式的Context,并且持有FSMStateMachine。在状态转换的实现时,我用ID来控制具体状态,并通过状态ID来实现状态的转化,这里和之前的状态模式稍有区别:
#pragma once
#include "../HrState/Context.h"
class HrFSMEntity : public Context
{
public:
HrFSMEntity();
~HrFSMEntity();
virtual void ChangeState(std::shared_ptr<State> pState) override;
virtual void ChangeState(int nStateID) override;
virtual void ChangeToPreviousState() override;
virtual void Execute() override;
};
#pragma once
#include "HrFSMEntity.h"
#include <iostream>
class HrFSMStateMachine;
class HrMonsterHungryState;
class HrMonsterEatState;
class HrMonsterEscapeState;
class HrMonsterFightState;
class HrMonsterForagingState;
class HrMonsterPlayState;
class HrMonsterEntity : public HrFSMEntity
{
public:
HrMonsterEntity();
~HrMonsterEntity();
enum ENUM_MONSTER_STATE
{
ENUM_STATE_HUNGRY,
ENUM_STATE_EAT,
ENUM_STATE_ESCAPE,
ENUM_STATE_FIGHT,
ENUM_STATE_FORAGING,
ENUM_STATE_PLAY,
};
void InitMonsterEntityState();
virtual void ChangeState(int nStateID) override;
virtual void ChangeToPreviousState() override;
virtual void Execute() override;
bool IsHungry();
bool Eat();
protected:
std::shared_ptr<HrFSMStateMachine> m_pStateMachine;
std::shared_ptr<HrMonsterHungryState> m_pStateHungry;
std::shared_ptr<HrMonsterEatState> m_pStateEat;
std::shared_ptr<HrMonsterEscapeState> m_pStateEscape;
std::shared_ptr<HrMonsterFightState> m_pStateFight;
std::shared_ptr<HrMonsterForagingState> m_pStateForaging;
std::shared_ptr<HrMonsterPlayState> m_pStatePlay;
//最大值100 低于10饥饿
int m_nDegreeOfStarvation;
};
#include "stdafx.h"
#include "HrMonsterEntity.h"
#include "HrFSMStateMachine.h"
#include "HrMonsterState.h"
HrMonsterEntity::HrMonsterEntity()
{
m_nDegreeOfStarvation = 10;
}
HrMonsterEntity::~HrMonsterEntity()
{
}
void HrMonsterEntity::InitMonsterEntityState()
{
m_pStateMachine = std::make_shared<HrFSMStateMachine>();
m_pStateHungry = std::make_shared<HrMonsterHungryState>(std::dynamic_pointer_cast<HrMonsterEntity>(shared_from_this()));
m_pStateEat = std::make_shared<HrMonsterEatState>(std::dynamic_pointer_cast<HrMonsterEntity>(shared_from_this()));
m_pStateEscape = std::make_shared<HrMonsterEscapeState>(std::dynamic_pointer_cast<HrMonsterEntity>(shared_from_this()));
m_pStateFight = std::make_shared<HrMonsterFightState>(std::dynamic_pointer_cast<HrMonsterEntity>(shared_from_this()));
m_pStateForaging = std::make_shared<HrMonsterForagingState>(std::dynamic_pointer_cast<HrMonsterEntity>(shared_from_this()));
m_pStatePlay = std::make_shared<HrMonsterPlayState>(std::dynamic_pointer_cast<HrMonsterEntity>(shared_from_this()));
m_pStateMachine->SetOwnerEntiry(std::dynamic_pointer_cast<HrFSMEntity>(shared_from_this()));
m_pStateMachine->SetCurrentState(m_pStatePlay);
m_pStateMachine->SetPreviousState(m_pStatePlay);
}
void HrMonsterEntity::ChangeState(int nStateID)
{
switch (nStateID)
{
case ENUM_STATE_HUNGRY:
m_pStateMachine->ChangeState(m_pStateHungry);
break;
case ENUM_STATE_EAT:
m_pStateMachine->ChangeState(m_pStateEat);
break;
case ENUM_STATE_ESCAPE:
m_pStateMachine->ChangeState(m_pStateEscape);
break;
case ENUM_STATE_FIGHT:
m_pStateMachine->ChangeState(m_pStateFight);
break;
case ENUM_STATE_FORAGING:
m_pStateMachine->ChangeState(m_pStateForaging);
break;
case ENUM_STATE_PLAY:
m_pStateMachine->ChangeState(m_pStatePlay);
break;
default:
std::cout << " Error State " << std::endl;
break;
}
}
void HrMonsterEntity::ChangeToPreviousState()
{
m_pStateMachine->ChangeToPreviousState();
}
void HrMonsterEntity::Execute()
{
--m_nDegreeOfStarvation;
if (m_nDegreeOfStarvation < 0)
{
m_nDegreeOfStarvation = 0;
}
m_pStateMachine->Update();
}
bool HrMonsterEntity::IsHungry()
{
return m_nDegreeOfStarvation <= 5;
}
bool HrMonsterEntity::Eat()
{
return (m_nDegreeOfStarvation += 2 ) >= 10;
}
这样就实现了一个非常简单的状态机,各个状态之间的转换逻辑还非常简单,具体的转换要根据具体的需求来实现。具体的实现我觉得不必死板,只要保持着设计模式的思维,结构的改动要根据具体的需求。在之前的怒海项目中,我另外加入了一个全局状态来控制一个更大范围的状态,比如无敌状态,在无敌的时候各个状态会有另外的一系列行为。加入把无敌状态也对等封装的话需要封装更多的状态,增加了逻辑复杂度。