[因为我不懂啊]-什么是状态机编程(设计模式)(3)
上一篇文章写完之后,我一直在想做一个游戏示例,想将之前学到的状态机模式运用在其中。然而一直遇到各种奇怪的bug(之后写出来),所以一直没有完成这件事。而且加之自己又犯懒了,所以我决定做一件更简单的事,以更容易接近的目标来引诱懒的不行的自己。
废话说完,来说正经事了。前几天认真看了一篇 博客-游戏状态机(侵立删),觉得挺不错的,建议盆友们认真看一看。
实现一个UI切换示例
为比较心急的伙伴放一个示例的Github链接
读完博客之后,之前计划的示例貌似有了头绪,(废话型)总结出来,实现一个游戏状态机结构需要进行如下几步:
- 分析示例(需求),划分状态。
- 找出需求中的状态、事件。
- 写一个状态转换表,使得需求更加明确。
- 写一个控制类,包含状态转换方法,作为所有状态的根,所有的状态在这个根之上切换。
- 写一个状态基类,然后派生出需要的状态类。
- 在每个状态类中,写好需要的事件,以及事件发生的处理(就是进行状态转换)。
分析需求,划分状态
我要做一个简单的UI切换示例,所以简述需求就是: 打开程序进入主界面,主界面中点击开始游戏,进入游戏界面,游戏界面中点击结束游戏,进入结算界面,结算界面可以点击回到主界面,进入主界面,也可以点击重新开始游戏,再次进入游戏界面。
找出状态和事件
从需求描述来看,状态有三个:
- 主界面
- 游戏界面
- 结算界面
事件有四个:
- 主界面->点击开始游戏按钮
- 游戏界面->点击游戏结束按钮
- 结算界面->点击回到主页按钮
- 结算界面->点击重新游戏按钮
状态转换表
然后(装模作样的)画了一个状态转换表:
状态 | 事件 | 下一状态 |
---|---|---|
主界面 | 点击开始游戏 | 游戏界面 |
游戏界面 | 点击游戏结束 | 结算界面 |
结算界面 | 点击回到主界面 | 主界面 |
点击重新游戏 | 游戏界面 |
写一个控制类
之前一篇文章中有提到过的,在Cocos2d-x游戏开发中,刚好使用Scene来作为控制器,由Scene来控制各层的出现和消失。
这里贴出Scene的源码(完整的源文件,包括了后续步骤才会出现的状态基类,以及派生的状态类):
QFLGameTwoScene.hpp
//
// QFLGameTwoScene.hpp
// QFLTest
//
// Created by QuFangliu on 16/10/8.
//
//
#ifndef QFLGameTwoScene_hpp
#define QFLGameTwoScene_hpp
#include <stdio.h>
#include "cocos2d.h"
#include "QFLGameTwoStateBase.hpp"
class QFLGameTwoScene : public cocos2d::Scene
{
CC_CONSTRUCTOR_ACCESS:
QFLGameTwoScene();
virtual ~QFLGameTwoScene();
virtual bool init();
CREATE_FUNC(QFLGameTwoScene);
public:
void addUI(); //添加UI,黑色背景
void initGame(); //初始化游戏,进入一个初始状态
void addEventListener(); //添加事件监听,需要切换状态时通过发送自定义事件来实现
//切换状态的函数
void ChangeState(GameTwoState* pState);
private:
GameTwoState* m_pCurrentState; //当前状态
};
#endif /* QFLGameTwoScene_hpp */
QFLGameTwoScene.cpp
//
// QFLGameTwoScene.cpp
// QFLTest
//
// Created by QuFangliu on 16/10/8.
//
//
#include "QFLGameTwoScene.hpp"
#include "QFLTools/QFLHelper.hpp"
//基类
#include "QFLGameTwoStateBase.hpp"
//状态类
#include "QFLGameTwoMainLayer.hpp"
#include "QFLGameTwoGameLayer.hpp"
#include "QFLGameTwoResultLayer.hpp"
USING_NS_CC;
QFLGameTwoScene::QFLGameTwoScene()
{
}
QFLGameTwoScene::~QFLGameTwoScene()
{
}
bool QFLGameTwoScene::init()
{
if (!Scene::init()) {
return false;
}
else {
this->addUI();
this->addEventListener();
this->initGame();
return true;
}
}
void QFLGameTwoScene::addUI()
{
//黑色背景
auto pLayer = QFL_HELPER->getColorfulLayer();
QFL_HELPER->addNoTouchListener(pLayer);
this->addChild(pLayer);
}
void QFLGameTwoScene::initGame()
{
//进入一个初始状态
log("游戏开始");
m_pCurrentState = QFLGameTwoMainLayer::create();
m_pCurrentState->Enter(this);
}
void QFLGameTwoScene::addEventListener()
{
Director::getInstance()->getEventDispatcher()->addCustomEventListener(GameTwoEvent::Main_Start, [=](EventCustom* pEvent){
log("[Event]->Main_Start");
this->ChangeState(QFLGameTwoGameLayer::create());
});
Director::getInstance()->getEventDispatcher()->addCustomEventListener(GameTwoEvent::Game_Over, [=](EventCustom* pEvent){
log("[Event]->Game_Over");
this->ChangeState(QFLGameTwoResultLayer::create());
});
Director::getInstance()->getEventDispatcher()->addCustomEventListener(GameTwoEvent::Result_Home, [=](EventCustom* pEvent){
log("[Event]->Result_Home");
this->ChangeState(QFLGameTwoMainLayer::create());
});
Director::getInstance()->getEventDispatcher()->addCustomEventListener(GameTwoEvent::Result_Replay, [=](EventCustom* pEvent){
log("[Event]->Result_Replay");
this->ChangeState(QFLGameTwoGameLayer::create());
});
}
void QFLGameTwoScene::ChangeState(GameTwoState *pState)
{
//旧状态退出
m_pCurrentState->Exit(this);
//保存新状态
m_pCurrentState = pState;
//新状态进入
m_pCurrentState->Enter(this);
}
QFLTools/QFLHelper.hpp 这个文件是作为一个工具类存在的,本示例所有源码在我的Github中
上面可以看到,Scene中主要包含了:
-
void ChangeState(GameTwoState* pState);
用于状态切换 -
GameTwoState* m_pCurrentState;
用于记录当前状态 -
void addEventListener();
用于监听切换状态事件 -
void initGame();
用于进入初始状态
包含了以上内容,本示例中的这个Scene就能起到控制器的作用了。
写一个状态基类
状态基类是很有必要的,为了达到各个同级状态都能通过同一个控制器来进行状态转换的目标,各个状态类进行切换时必须有一套统一的流程。
这里贴出本示例中状态基类的源码:
QFLGameTwoStateBase.hpp
//
// QFLGameTwoStateBase.hpp
// QFLTest
//
// Created by QuFangliu on 16/10/8.
//
//
#ifndef QFLGameTwoStateBase_hpp
#define QFLGameTwoStateBase_hpp
#include <stdio.h>
#include "cocos2d.h"
//State的基类
class GameTwoState
{
public:
virtual ~GameTwoState() {}
virtual void Enter(cocos2d::Scene* pLayer) = 0;
virtual void Execute() = 0;
virtual void Exit(cocos2d::Scene* pLayer) = 0;
};
#define SC(__type__) static const __type__
//游戏中的事件(通过事件来进行状态切换)
namespace GameTwoEvent {
SC(std::string) Main_Start = "Event_Main_Start"; //[Main]开始游戏的事件
SC(std::string) Game_Over = "Event_Game_Over"; //[Game]结束游戏的事件
SC(std::string) Result_Home = "Event_Result_Home"; //[Result]结算界面回主页
SC(std::string) Result_Replay = "Event_Result_Replay";//[Result]结算界面重新开始
}
#endif /* QFLGameTwoStateBase_hpp */
这个源文件中包含了:
-
class GameTwoState
状态基类 -
virtual void Enter(cocos2d::Scene* pLayer) = 0;
状态进入的处理 -
virtual void Execute() = 0;
本状态执行的方法 -
virtual void Exit(cocos2d::Scene* pLayer) = 0;
状态退出的处理 -
namespace GameTwoEvent
包含状态转换事件的namespace
所有的派生状态类都会include
这个文件,实现虚基类GameTwoState
中的方法,所以所有的状态类都有统一的切换流程:
- last_state->exit
- current_state->enter
- current_state->execute
而且在完成某个事件,需要进行状态转换时,可以通过发送自定义事件来完成消息的传递,自定义事件名放在这里也是为了方便起见。
每个状态类中写好需要的事件及处理
这里直接贴出几个State的源码了,其中包含注释,废话就不重复再写了:
MainState源码
QFLGameTwoMainLayer.hpp
//
// QFLGameTwoMainLayer.hpp
// QFLTest
//
// Created by QuFangliu on 16/10/8.
//
//
#ifndef QFLGameTwoMainLayer_hpp
#define QFLGameTwoMainLayer_hpp
#include <stdio.h>
#include "cocos2d.h"
#include "QFLGameTwoStateBase.hpp"
class QFLGameTwoMainLayer : public cocos2d::Layer, public GameTwoState
{
public:
QFLGameTwoMainLayer();
virtual ~QFLGameTwoMainLayer();
virtual bool init() override;
CREATE_FUNC(QFLGameTwoMainLayer);
//实现StateBase的状态切换所需方法
void Enter(cocos2d::Scene* pLayer) override;
void Execute() override;
void Exit(cocos2d::Scene* pLayer) override;
private:
void addUI(); //添加背景UI,添加按钮等
};
#endif /* QFLGameTwoMainLayer_hpp */
QFLGameTwoMainLayer.cpp
//
// QFLGameTwoMainLayer.cpp
// QFLTest
//
// Created by QuFangliu on 16/10/8.
//
//
#include "QFLGameTwoMainLayer.hpp"
#include "QFLTools/QFLHelper.hpp"
USING_NS_CC;
QFLGameTwoMainLayer::QFLGameTwoMainLayer()
{
}
QFLGameTwoMainLayer::~QFLGameTwoMainLayer()
{
}
bool QFLGameTwoMainLayer::init()
{
if (!Layer::init()) {
return false;
}
else {
this->addUI();
return true;
}
}
void QFLGameTwoMainLayer::Enter(cocos2d::Scene* pLayer)
{
log("<Enter>\t\t->MainLayer");
//添加到Scene中
pLayer->addChild(this);
//执行各种事件
this->Execute();
}
void QFLGameTwoMainLayer::Execute()
{
log("<Execute>\t->MainLayer"); //执行内容只是打印一个Log
}
void QFLGameTwoMainLayer::Exit(cocos2d::Scene* pLayer)
{
log("<Exit>\t\t->MainLayer\n");
//移除
this->removeFromParentAndCleanup(true);
}
void QFLGameTwoMainLayer::addUI()
{
//黑色背景
auto pLayer = QFL_HELPER->getColorfulLayer();
QFL_HELPER->addNoTouchListener(pLayer);
this->addChild(pLayer);
//MainLogo
auto pLogo = Label::createWithSystemFont("MainLayer", "", 50);
pLogo->setPosition(Vec2(SCREEN_CENTER.x, SCREEN_CENTER.y + 60));
this->addChild(pLogo);
//菜单
auto pMenu = Menu::create();
pMenu->setPosition(Vec2::ZERO);
this->addChild(pMenu);
//按钮,点击发送游戏开始事件
auto pStart = MenuItemFont::create("Start", [=](cocos2d::Ref* pRef){
Director::getInstance()->getEventDispatcher()->dispatchCustomEvent(GameTwoEvent::Main_Start);
});
pStart->setPosition(Vec2(SCREEN_CENTER.x, SCREEN_CENTER.y - 100));
pMenu->addChild(pStart);
}
GameState源码
QFLGameTwoGameLayer.hpp
//
// QFLGameTwoGameLayer.hpp
// QFLTest
//
// Created by QuFangliu on 16/10/8.
//
//
#ifndef QFLGameTwoGameLayer_hpp
#define QFLGameTwoGameLayer_hpp
#include <stdio.h>
#include "cocos2d.h"
#include "QFLGameTwoStateBase.hpp"
class QFLGameTwoGameLayer : public cocos2d::Layer, public GameTwoState
{
public:
QFLGameTwoGameLayer();
virtual ~QFLGameTwoGameLayer();
virtual bool init() override;
CREATE_FUNC(QFLGameTwoGameLayer);
//实现StateBase的状态切换所需方法
void Enter(cocos2d::Scene* pLayer) override;
void Execute() override;
void Exit(cocos2d::Scene* pLayer) override;
private:
void addUI();
};
#endif /* QFLGameTwoGameLayer_hpp */
QFLGameTwoGameLayer.cpp
//
// QFLGameTwoGameLayer.cpp
// QFLTest
//
// Created by QuFangliu on 16/10/8.
//
//
#include "QFLGameTwoGameLayer.hpp"
#include "QFLTools/QFLHelper.hpp"
USING_NS_CC;
QFLGameTwoGameLayer::QFLGameTwoGameLayer()
{
}
QFLGameTwoGameLayer::~QFLGameTwoGameLayer()
{
}
bool QFLGameTwoGameLayer::init()
{
if (!Layer::init()) {
return false;
}
else {
this->addUI();
return true;
}
}
void QFLGameTwoGameLayer::addUI()
{
//黑色背景
auto pLayer = QFL_HELPER->getColorfulLayer();
QFL_HELPER->addNoTouchListener(pLayer);
this->addChild(pLayer);
//GameLogo
auto pLogo = Label::createWithSystemFont("GameLayer", "", 50);
pLogo->setPosition(Vec2(SCREEN_CENTER.x, SCREEN_CENTER.y + 60));
this->addChild(pLogo);
//菜单
auto pMenu = Menu::create();
pMenu->setPosition(Vec2::ZERO);
this->addChild(pMenu);
//按钮,点击发送游戏结束事件
auto pOver = MenuItemFont::create("GameOver", [=](cocos2d::Ref* pRef){
Director::getInstance()->getEventDispatcher()->dispatchCustomEvent(GameTwoEvent::Game_Over);
});
pOver->setPosition(Vec2(SCREEN_CENTER.x, SCREEN_CENTER.y - 100));
pMenu->addChild(pOver);
}
void QFLGameTwoGameLayer::Enter(cocos2d::Scene* pLayer)
{
log("<Enter>\t\t->GameLayer");
//添加到Scene中
pLayer->addChild(this);
//执行游戏
this->Execute();
}
void QFLGameTwoGameLayer::Execute()
{
log("<Execute>\t->GameLayer");
}
void QFLGameTwoGameLayer::Exit(cocos2d::Scene* pLayer)
{
log("<Exit>\t\t->GameLayer\n");
//移除游戏
this->removeFromParentAndCleanup(true);
}
ResultState源码
QFLGameTwoResultLayer.hpp
//
// QFLGameTwoResultLayer.hpp
// QFLTest
//
// Created by QuFangliu on 16/10/8.
//
//
#ifndef QFLGameTwoResultLayer_hpp
#define QFLGameTwoResultLayer_hpp
#include <stdio.h>
#include "cocos2d.h"
#include "QFLGameTwoStateBase.hpp"
class QFLGameTwoResultLayer : public cocos2d::Layer, public GameTwoState
{
public:
QFLGameTwoResultLayer();
virtual ~QFLGameTwoResultLayer();
virtual bool init() override;
CREATE_FUNC(QFLGameTwoResultLayer);
//实现StateBase的状态切换所需方法
void Enter(cocos2d::Scene* pLayer) override;
void Execute() override;
void Exit(cocos2d::Scene* pLayer) override;
private:
void addUI();
};
#endif /* QFLGameTwoResultLayer_hpp */
QFLGameTwoResultLayer.cpp
//
// QFLGameTwoResultLayer.cpp
// QFLTest
//
// Created by QuFangliu on 16/10/8.
//
//
#include "QFLGameTwoResultLayer.hpp"
#include "QFLTools/QFLHelper.hpp"
USING_NS_CC;
QFLGameTwoResultLayer::QFLGameTwoResultLayer()
{
}
QFLGameTwoResultLayer::~QFLGameTwoResultLayer()
{
}
bool QFLGameTwoResultLayer::init()
{
if (!Layer::init()) {
return false;
}
else {
this->addUI();
return true;
}
}
void QFLGameTwoResultLayer::addUI()
{
//黑色背景
auto pLayer = QFL_HELPER->getColorfulLayer();
QFL_HELPER->addNoTouchListener(pLayer);
this->addChild(pLayer);
//GameLogo
auto pLogo = Label::createWithSystemFont("ResultLayer", "", 50);
pLogo->setPosition(Vec2(SCREEN_CENTER.x, SCREEN_CENTER.y + 60));
this->addChild(pLogo);
//菜单
auto pMenu = Menu::create();
pMenu->setPosition(Vec2::ZERO);
this->addChild(pMenu);
//按钮,点击发送会到主界面事件
auto pHome = MenuItemFont::create("Home", [=](cocos2d::Ref* pRef){
Director::getInstance()->getEventDispatcher()->dispatchCustomEvent(GameTwoEvent::Result_Home);
});
pHome->setPosition(Vec2(SCREEN_CENTER.x, SCREEN_CENTER.y - 100));
pMenu->addChild(pHome);
//按钮,点击发送重新开始游戏事件
auto pReplay = MenuItemFont::create("Replay", [=](cocos2d::Ref* pRef){
Director::getInstance()->getEventDispatcher()->dispatchCustomEvent(GameTwoEvent::Result_Replay);
});
pReplay->setPosition(Vec2(SCREEN_CENTER.x, SCREEN_CENTER.y - 150));
pMenu->addChild(pReplay);
}
void QFLGameTwoResultLayer::Enter(cocos2d::Scene* pLayer)
{
log("<Enter>\t\t->ResultLayer");
//添加到Scene中
pLayer->addChild(this);
//执行游戏
this->Execute();
}
void QFLGameTwoResultLayer::Execute()
{
log("<Execute>\t->ResultLayer");
}
void QFLGameTwoResultLayer::Exit(cocos2d::Scene* pLayer)
{
log("<Exit>\t\t->ResultLayer\n");
//移除游戏
this->removeFromParentAndCleanup(true);
}
到这里,上面所提到的步骤全部进行完了。这个UI切换的状态机示例也就完成了。
运行效果
在AppDelegate.cpp中,直接进入Scene中:
auto pScene = QFLGameTwoScene::create();
director->runWithScene(pScene);
进入主界面:
此时的Log:
游戏开始
<Enter> ->MainLayer
<Execute> ->MainLayer
点击Start,进入游戏界面:
此时的Log:
游戏开始
<Enter> ->MainLayer
<Execute> ->MainLayer
[Event]->Main_Start
<Exit> ->MainLayer
<Enter> ->GameLayer
<Execute> ->GameLayer
点击Over,进入结算界面:
此时的Log:
游戏开始
<Enter> ->MainLayer
<Execute> ->MainLayer
[Event]->Main_Start
<Exit> ->MainLayer
<Enter> ->GameLayer
<Execute> ->GameLayer
[Event]->Game_Over
<Exit> ->GameLayer
<Enter> ->ResultLayer
<Execute> ->ResultLayer
点击Replay,重新进入游戏界面:
此时的Log:
游戏开始
<Enter> ->MainLayer
<Execute> ->MainLayer
[Event]->Main_Start
<Exit> ->MainLayer
<Enter> ->GameLayer
<Execute> ->GameLayer
[Event]->Game_Over
<Exit> ->GameLayer
<Enter> ->ResultLayer
<Execute> ->ResultLayer
[Event]->Result_Replay
<Exit> ->ResultLayer
<Enter> ->GameLayer
<Execute> ->GameLayer
点击Over,再次进入结算界面,然后在结算界面点击Home,回到主界面
上面的两次操作完成后,Log如下:
游戏开始
<Enter> ->MainLayer
<Execute> ->MainLayer
[Event]->Main_Start
<Exit> ->MainLayer
<Enter> ->GameLayer
<Execute> ->GameLayer
[Event]->Game_Over
<Exit> ->GameLayer
<Enter> ->ResultLayer
<Execute> ->ResultLayer
[Event]->Result_Replay
<Exit> ->ResultLayer
<Enter> ->GameLayer
<Execute> ->GameLayer
[Event]->Game_Over
<Exit> ->GameLayer
<Enter> ->ResultLayer
<Execute> ->ResultLayer
[Event]->Result_Home
<Exit> ->ResultLayer
<Enter> ->MainLayer
<Execute> ->MainLayer
总结
这里用FSM实现UI切换示例只是众多方法中的一种,暂时还没有去比较它们的优劣。(使用markdown编辑器的经验值+1)
例如:在状态基类文件中,添加一个状态枚举类型
enum StateType
,void ChangeState
转换状态时不再传递一个GameTwoState *pState
指针,而是传递一个状态枚举值StateType
,这种方法在某些情况下很有效。
例如:不用在
void Enter
中传入一个cocos2d::Scene* pLayer
指针,而是在void ChangeState
中生成新的状态类,并addChild
到Scene中。不用在void Exit
中传入一个cocos2d::Scene* pLayer
指针,直接使用removeFromParentAndClean
方法就能从Scene中删除。
可能性多种多样,如果有小伙伴比较过这些方法。求留言告知,求分享经验。