在游戏中使用状态模式

Using The State Pattern in games

最近,我尝试在 SFML 中创建贪吃蛇游戏。但是,我也想使用一些设计模式来为以后的编程养成一些好习惯——这就是状态模式。但是 - 有一些问题我无法解决。

为了让一切都清楚,我试着做了几个菜单——一个主菜单,还有其他的,比如“选项”,或者类似的东西。主菜单的第一个选项会将玩家带到“游戏状态”。但是随后,问题出现了——我认为整个游戏应该是一个独立的模块来实现编程。那么,我应该如何处理程序所处的实际状态呢? (例如,我们称此状态为“MainMenu”)。

我是否应该创建一个名为“PlayingState”的额外状态,它代表整个游戏?我该怎么做?如何向单个状态添加新功能?你有什么想法吗?

对于您的设计,我认为您可以针对不同的状态使用增量循环:

简单示例:

// main loop
while (window.isOpen()) {
    // I tink you can simplify this "if tree"
    if (state == "MainMenu")
        state = run_main_menu(/* args */);
    else if (state == "Play")
        state = run_game(/* args */);
    // Other state here
    else
        // error state unknow
        // exit the app
}

而当比赛是运行时:

state run_game(/* args */)
{
    // loading texture, sprite,...
    // or they was passe in args

    while (window.isOpen()) {
        while (window.pollEvent(event)) {
            // checking event for your game
        }
        // maybe modifying the state
        // Display your game
        // Going to the end game menu if the player win/loose
        if (state == "End")
            return run_end_menu(/* args */);
            // returning the new state, certainly MainMenu
        else if (state != "Play")
            return state;
    }
}

您有主菜单和游戏,您的状态默认为"MainMenu"

当您进入主菜单时,单击播放按钮,然后进入 returns "Play" 状态,然后返回主循环。

状态是"Play"所以你去游戏菜单开始你的游戏。

当游戏结束时,您将状态更改为"EndGame"并退出游戏菜单进入结束菜单。

结束菜单 returns 要显示的新菜单,因此您返回主循环并检查每个可用菜单。

通过这种设计,您可以在不更改整个体系结构的情况下添加新菜单。

例如,状态模式允许您拥有 class Game 的对象,并在游戏状态更改时改变其行为,从而提供该 Game 对象的错觉已更改其类型。

举个例子,假设一个游戏有一个初始菜单,并且可以在玩游戏时按 space 栏暂停。游戏暂停时,您可以按返回space键返回初始菜单或再次按space栏继续游戏:

首先我们定义一个抽象class,GameState:

struct GameState {
    virtual GameState* handleEvent(const sf::Event&) = 0;
    virtual void update(sf::Time) = 0;
    virtual void render() = 0;
    virtual ~GameState() = default; 
};

所有状态 classes – 即 MenuStatePlayingStatePausedState – 将公开派生自此 GameState class .注意 handleEvent() returns a GameState *;这是为了提供状态之间的转换(即,如果发生转换,则为下一个状态)。

让我们暂时关注 Game class。最终,我们的目的是按以下方式使用 Game class:

auto main() -> int {
   Game game;
   game.run();
}

也就是说,它基本上有一个 run() 成员函数,当游戏结束时 return s。我们定义 Game class:

class Game {
public:
   Game();
    void run();
private:
   sf::RenderWindow window_;

   MenuState menuState_;
   PausedState pausedState_;
   PlayingState playingState_;

   GameState *currentState_; // <-- delegate to the object pointed
};

这里的关键点是currentState_数据成员。在任何时候,currentState_ 都指向游戏的三种可能状态之一(即 menuState_pausedState_playingState_)。

run()成员函数依赖委托;它委托给 currentState_:

指向的对象
void Game::run() {
   sf::Clock clock;

   while (window_.isOpen()) {
      // handle user-input
      sf::Event event;
      while (window_.pollEvent(event)) {
         GameState* nextState = currentState_->handleEvent(event);
         if (nextState) // must change state?
            currentState_ = nextState;
      }
     
      // update game world
      auto deltaTime = clock.restart();
      currentState_->update(deltaTime);

      currentState_->render();
   }
}

Game::run() 调用 GameState::handleEvent()GameState::update()GameState::render() 成员函数,每个从 GameState 派生的具体 class 都必须覆盖这些成员函数。也就是说,Game没有实现处理事件、更新游戏状态和渲染的逻辑;它只是将这些职责委托给其数据成员 currentState_ 指向的 GameState 对象。 Game 的内部状态改变时其类型似乎改变的错觉是通过此委托实现的。

现在,回到具体的状态。我们定义 PausedState class:

class PausedState: public GameState {
public:
   PausedState(MenuState& menuState, PlayingState& playingState):
      menuState_(menuState), playingState_(playingState) {}

    GameState* handleEvent(const sf::Event&) override;
    void update(sf::Time) override;
    void render() override;
private:
   MenuState& menuState_;
   PlayingState& playingState_;
};

PlayingState::handleEvent() 必须在某个时间 return 转换到下一个状态,这将对应于 Game::menuState_Game::playingState_。因此,此实现包含对 MenuStatePlayingState 对象的引用;在 PlayState 的构造中,它们将被设置为指向 Game::menuState_Game::playingState_ 数据成员。此外,当游戏暂停时,我们理想情况下希望渲染与播放状态对应的屏幕作为起点,如下所示。

PauseState::update() 的实现无所事事,游戏世界保持不变:

void PausedState::update(sf::Time) { /* do nothing */ }

PausedState::handleEvent() 仅对按下 space 栏或返回 space:

的事件作出反应
GameState* PausedState::handleEvent(const sf::Event& event) {
   if (event.type == sf::Event::KeyPressed) {

      if (event.key.code == sf::Keyboard::Space)
         return &playingState_; // change to playing state

      if (event.key.code == sf::Keyboard::Backspace) {
         playingState_.reset(); // clear the play state
         return &menuState_; // change to menu state
      }
   }
   // remain in the current state
   return nullptr; // no transition
}

PlayingState::reset()是为了把PlayingState建好后清到初始状态,在开始播放前回到初始菜单。

最后,我们定义PausedState::render()

void PausedState::render() {
   // render the PlayingState screen
   playingState_.render();

   // render a whole window rectangle
   // ...

   // write the text "Paused"
   // ...
}

首先,该成员函数渲染播放状态对应的画面。然后,在这个播放状态的渲染画面之上,它渲染了一个适合整个 window 的透明背景的矩形;这样,我们将屏幕变暗。在这个渲染的矩形之上,它可以渲染类似“暂停”的文本。

一堆状态

另一种架构由一堆状态组成:状态堆叠在其他状态之上。例如,暂停状态将位于播放状态之上。事件从最顶层的状态传递到最底层,因此状态也会更新。渲染是从下往上进行的。

这种变体可以被认为是上面公开的案例的概括,因为您总是可以拥有 - 作为一种特殊情况 - 一个仅由单个状态对象组成的堆栈,并且这种情况对应于普通的状态模式。

如果您有兴趣了解更多关于这种其他架构的信息,我建议您阅读本书的第五章 SFML Game Development