状态模式允许您拥有一个类的对象
Game
,并在游戏状态改变时更改其行为,从而产生这个
Game
对象已更改其类型的幻觉。
例如,想象一个具有初始菜单并且可以在玩游戏时按空格键暂停的游戏。当游戏暂停时,您可以通过按回退键返回到初始菜单,或者通过再次按空格键继续游戏:
![State-Diagram](https://istack.dev59.com/J2yWJ.webp)
首先,我们定义一个抽象类
GameState
:
struct GameState {
virtual GameState* handleEvent(const sf::Event&) = 0;
virtual void update(sf::Time) = 0;
virtual void render() = 0;
virtual ~GameState() = default;
};
所有状态类,即
MenuState
、
PlayingState
、
PausedState
,都将公开派生自这个
GameState
类。请注意,
handleEvent()
返回一个
GameState *
;这是为了提供状态之间的转换(即如果发生转换,则是下一个状态)。
现在让我们先关注
Game
类。最终,我们的意图是以以下方式使用
Game
类:
auto main() -> int {
Game game;
game.run();
}
也就是说,它基本上有一个
run()
成员函数,在游戏结束时返回。我们定义了
Game
类:
class Game {
public:
Game();
void run();
private:
sf::RenderWindow window_;
MenuState menuState_;
PausedState pausedState_;
PlayingState playingState_;
GameState *currentState_;
};
这里的关键点是
currentState_
数据成员。在任何时候,
currentState_
都指向游戏的三种可能状态之一(即
menuState_
、
pausedState_
、
playingState_
)。
run()
成员函数依赖于委托;它委托给
currentState_
所指向的对象:
void Game::run() {
sf::Clock clock;
while (window_.isOpen()) {
sf::Event event;
while (window_.pollEvent(event)) {
GameState* nextState = currentState_->handleEvent(event);
if (nextState)
currentState_ = nextState;
}
auto deltaTime = clock.restart();
currentState_->update(deltaTime);
currentState_->render();
}
}
Game::run()
调用GameState::handleEvent()
、GameState::update()
和GameState::render()
成员函数,每个继承自GameState
的具体类都必须重写这些函数。也就是说,Game
不实现处理事件、更新游戏状态和渲染的逻辑,它只将这些责任委托给其数据成员currentState_
所指向的GameState
对象。通过这种委托,当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_;
};
PlayingState::handleEvent()
必须在某个时刻返回下一个要转换的状态,这将对应于
Game::menuState_
或
Game::playingState_
。因此,此实现包含对
MenuState
和
PlayingState
对象的引用;它们将在
PlayState
的构造函数中设置为指向
Game::menuState_
和
Game::playingState_
数据成员。此外,在游戏暂停时,我们理想情况下希望以播放状态对应的屏幕作为起点进行渲染,如下所示。
PauseState::update()
的实现什么也不做,游戏世界保持不变。
void PausedState::update(sf::Time) { /* do nothing */ }
< p >
PausedState::handleEvent()
只对按下空格键或退格键的事件做出反应:
GameState* PausedState::handleEvent(const sf::Event& event) {
if (event.type == sf::Event::KeyPressed) {
if (event.key.code == sf::Keyboard::Space)
return &playingState_;
if (event.key.code == sf::Keyboard::Backspace) {
playingState_.reset();
return &menuState_;
}
}
return nullptr;
}
PlayingState::reset()
用于在我们回到初始菜单之前将PlayingState
清除为其初始状态,然后开始播放。
最后,我们定义PausedState ::render()
:
void PausedState::render() {
playingState_.render();
}
首先,该成员函数呈现与播放状态对应的屏幕。然后,在播放状态的呈现屏幕上方,它呈现一个透明背景的矩形,以适合整个窗口;这样,我们就可以使屏幕变暗。在此呈现的矩形之上,可以呈现类似“暂停”文本的内容。
一堆状态
另一种架构包括一堆状态:状态堆叠在其他状态之上。例如,暂停状态将位于播放状态之上。事件从最上面的状态传递到最下面的状态,因此状态也会更新。呈现是从下到上执行的。
可以将此变体视为上述情况的概括,因为您始终可以将仅由单个状态对象组成的堆栈视为特殊情况,而此情况对应于普通状态模式。
如果您有兴趣了解更多关于此其他架构的信息,我建议阅读书籍
SFML游戏开发 的第五章。
PausedState
中你有对menuState_
和playingState_
的私有引用,但是在Game
类中你创建整个对象而不是引用? - ThorvasGame
对象拥有所有状态对象(即类型为MenuState
、PlayingState
和PausedState
的对象),而PausedState
则没有;但是,它需要引用另外两个状态,因为它可以使Game
对象转换到这些状态之一:当处于暂停状态时,如果按下空格键,游戏将继续进行,而如果按下退格键,则返回菜单。我建议您查看PausedState::handleEvent()
,它返回指向下一个状态对象的GameState *
。 - JFMRPausedState
拥有一个PlayingState
对象。相反,状态堆栈中的PausedState
对象将位于PlayingState
对象的顶部。 - JFMR