在游戏中使用状态模式

Tho*_*vas 2 c++ design-patterns game-development sfml state-pattern

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

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

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

眠りネ*_*ネロク 5

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

举个例子,想象一个游戏,它有一个初始菜单,如果你按空格键可以在玩的时候暂停。游戏暂停时,您可以按退格键返回初始菜单,也可以再次按空格键继续游戏: 状态图

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

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

所有状态类——即, MenuState, PlayingState, PausedState——都将从这个GameState类公开派生。请注意,handleEvent()返回一个GameState *; 这是用于提供状态之间的转换(即,下一个状态,如果发生转换)。

让我们暂时将注意力集中在Game类上。最终,我们的目的是Game按以下方式使用该类:

auto main() -> int {
   Game game;
   game.run();
}
Run Code Online (Sandbox Code Playgroud)

也就是说,它基本上有一个run()在游戏结束时返回的成员函数。我们定义Game类:

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

   MenuState menuState_;
   PausedState pausedState_;
   PlayingState playingState_;

   GameState *currentState_; // <-- delegate to the object pointed
};
Run Code Online (Sandbox Code Playgroud)

这里的关键点是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();
   }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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_;
};
Run Code Online (Sandbox Code Playgroud)

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

的实现PauseState::update()包括什么都不做,游戏世界只是保持不变:

void PausedState::update(sf::Time) { /* do nothing */ }
Run Code Online (Sandbox Code Playgroud)

PausedState::handleEvent() 仅对按下空格键或退格键的事件作出反应:

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
}
Run Code Online (Sandbox Code Playgroud)

PlayingState::reset()用于PlayingState在构建后清除初始状态,因为我们在开始播放之前返回初始菜单。

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

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

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

   // write the text "Paused"
   // ...
}
Run Code Online (Sandbox Code Playgroud)

首先,该成员函数根据播放状态渲染屏幕。然后,在这个播放状态的渲染屏幕之上,它渲染一个矩形,具有适合整个窗口的透明背景;这样,我们使屏幕变暗。在这个渲染的矩形之上,它可以渲染类似“暂停”文本的内容。

一堆状态

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

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

如果您有兴趣了解有关其他架构的更多信息,我建议您阅读SFML 游戏开发一书的第五章。