[因为我不懂啊]-什么是状态机编程(设计模式)(3)

上一篇文章写完之后,我一直在想做一个游戏示例,想将之前学到的状态机模式运用在其中。然而一直遇到各种奇怪的bug(之后写出来),所以一直没有完成这件事。而且加之自己又犯懒了,所以我决定做一件更简单的事,以更容易接近的目标来引诱懒的不行的自己。


废话说完,来说正经事了。前几天认真看了一篇 博客-游戏状态机(侵立删),觉得挺不错的,建议盆友们认真看一看。

实现一个UI切换示例

为比较心急的伙伴放一个示例的Github链接

读完博客之后,之前计划的示例貌似有了头绪,(废话型)总结出来,实现一个游戏状态机结构需要进行如下几步:

  1. 分析示例(需求),划分状态。
  2. 找出需求中的状态事件
  3. 写一个状态转换表,使得需求更加明确。
  4. 写一个控制类,包含状态转换方法,作为所有状态的根,所有的状态在这个根之上切换。
  5. 写一个状态基类,然后派生出需要的状态类。
  6. 在每个状态类中,写好需要的事件,以及事件发生的处理(就是进行状态转换)。

分析需求,划分状态

我要做一个简单的UI切换示例,所以简述需求就是: 打开程序进入主界面,主界面中点击开始游戏,进入游戏界面,游戏界面中点击结束游戏,进入结算界面,结算界面可以点击回到主界面,进入主界面,也可以点击重新开始游戏,再次进入游戏界面。

找出状态和事件

从需求描述来看,状态有三个:

  1. 主界面
  2. 游戏界面
  3. 结算界面

事件有四个:

  1. 主界面->点击开始游戏按钮
  2. 游戏界面->点击游戏结束按钮
  3. 结算界面->点击回到主页按钮
  4. 结算界面->点击重新游戏按钮

状态转换表

然后(装模作样的)画了一个状态转换表:

状态 事件 下一状态
主界面 点击开始游戏 游戏界面
游戏界面 点击游戏结束 结算界面
结算界面 点击回到主界面 主界面
点击重新游戏 游戏界面

写一个控制类

之前一篇文章中有提到过的,在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中主要包含了:

  1. void ChangeState(GameTwoState* pState);用于状态切换
  2. GameTwoState* m_pCurrentState;用于记录当前状态
  3. void addEventListener();用于监听切换状态事件
  4. 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 */

这个源文件中包含了:

  1. class GameTwoState 状态基类
  2. virtual void Enter(cocos2d::Scene* pLayer) = 0;状态进入的处理
  3. virtual void Execute() = 0;本状态执行的方法
  4. virtual void Exit(cocos2d::Scene* pLayer) = 0;状态退出的处理
  5. namespace GameTwoEvent包含状态转换事件的namespace

所有的派生状态类都会include这个文件,实现虚基类GameTwoState中的方法,所以所有的状态类都有统一的切换流程:

  1. last_state->exit
  2. current_state->enter
  3. 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);

进入主界面:

MainState.jpeg

此时的Log:

游戏开始
<Enter> ->MainLayer
<Execute> ->MainLayer

点击Start,进入游戏界面:

GameState.jpeg

此时的Log:

游戏开始
<Enter> ->MainLayer
<Execute> ->MainLayer
[Event]->Main_Start

<Exit> ->MainLayer
<Enter> ->GameLayer
<Execute> ->GameLayer

点击Over,进入结算界面:

ResultState.jpeg

此时的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 StateTypevoid ChangeState转换状态时不再传递一个GameTwoState *pState指针,而是传递一个状态枚举值StateType,这种方法在某些情况下很有效。

例如:不用在void Enter中传入一个cocos2d::Scene* pLayer指针,而是在void ChangeState中生成新的状态类,并addChild到Scene中。不用在void Exit中传入一个cocos2d::Scene* pLayer指针,直接使用removeFromParentAndClean方法就能从Scene中删除。

可能性多种多样,如果有小伙伴比较过这些方法。求留言告知,求分享经验。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容