在游戏中使用状态模式
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 – 即 MenuState
、PlayingState
、PausedState
– 将公开派生自此 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_
。因此,此实现包含对 MenuState
和 PlayingState
对象的引用;在 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。
最近,我尝试在 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 – 即 MenuState
、PlayingState
、PausedState
– 将公开派生自此 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_
。因此,此实现包含对 MenuState
和 PlayingState
对象的引用;在 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。