重构SFML游戏

上一篇文章中,我讨论了如何使用SFML库制作简单的游戏。 在文章的最后,我保证我将继续并展示如何将程序代码转换为更正确的形式。 因此,重构的时候到了。

首先,我弄清楚了该游戏需要哪些课程。 原来,我需要一个使用游戏资源(资产)的课程。 在这些资源中,我现在只有可下载的字体,但是将来可能会添加其他资源,例如图像,音乐等。 我使该类成为单例,因为此模板非常适合Assets类。 该基础由著名的单例Myers决定。

接下来,实际上,您需要游戏的类,该类负责程序的逻辑及其状态的存储。 从MVC的意识形态的角度来看,此类是一个模型。 所以我叫它-GameModel。

为了直观地显示游戏,您需要一个负责渲染屏幕的类。 在思想上,MVC是View。 我将此类命名为GameRender,并从SFML库的一部分抽象类Drawable继承了该类。

好吧,我们需要的最后一个类-这是负责与玩家互动的类-这是Controller。 这里必须说,在MVC的经典思想中,控制器不应直接与表示进行交互。 它仅应作用于模型,并且视图可单独或通过消息系统的信号从模型读取数据。 但是在这种情况下,这种方法对我来说似乎有些多余,因此我将控制器与视图直接连接。 因此,根据MVP的思想,假设我们没有控制器,而是Presenter会更正确。 但是,该类称为GameController。

好了,现在让我们开始将代码分散到这些类中的有趣事情。

类别资产


#pragma once #include <SFML/Graphics.hpp> class Assets { public: sf::Font font; public: static Assets& Instance() { static Assets s; return s; } void Load(); private: Assets() {}; ~Assets() {}; Assets(Assets const&) = delete; Assets& operator= (Assets const&) = delete; }; 

从我添加到类的内容来看,这是一个用于存储已加载字体的字体类成员的声明,以及一个用于加载字体的Load方法。 其他所有内容都是Myers单例实现。

Load方法也非常简单:

 void Assets::Load() { if (!font.loadFromFile("calibri.ttf")) throw; } 

尝试加载calibri字体,如果失败,则抛出异常。

GameModel类


 #pragma once enum class Direction { Left = 0, Right = 1, Up = 2, Down = 3 }; class GameModel { public: static const int SIZE = 4; //      static const int ARRAY_SIZE = SIZE * SIZE; //   static const int FIELD_SIZE = 500; //      static const int CELL_SIZE = 120; //     protected: int elements[ARRAY_SIZE]; int empty_index; bool solved; public: GameModel(); void Init(); bool Check(); void Move(Direction direction); bool IsSolved() { return solved; } int* Elements() { return elements; } }; 

所有逻辑和所有游戏数据都放在该类中。 该类原则上应尽可能独立于外部环境,因此,它不应具有任何链接到屏幕输出或用户交互。 在开发时,请记住,即使演示类或控制器的实现发生了变化,游戏模型也必须保持可操作状态。

原则上,模型的所有方法在上一篇文章中都有介绍,因此在此不再赘述。 我唯一要说的是,IsSolved和Elements吸气剂已添加到模型中以满足表示类的需要。

GameRender类别


 #pragma once #include <SFML/Graphics.hpp> #include "GameModel.h" class GameRender : public sf::Drawable, public sf::Transformable { GameModel *m_game; sf::RenderWindow m_window; sf::Text m_text; public: GameRender(GameModel *game); ~GameRender(); sf::RenderWindow& window() { return m_window; }; bool Init(); void Render(); public: virtual void draw(sf::RenderTarget& target, sf::RenderStates states) const; }; 

GameRender类的目的是在其自身中封装所有数据,以显示游戏窗口和渲染比赛场地。 为了与游戏模型连接,将传递给定表示形式的模型对象的指针并将其存储在构造函数中。 构造函数还调用Init方法,该方法创建并初始化游戏窗口。

 GameRender::GameRender(GameModel *game) { m_game = game; Init(); } bool GameRender::Init() { setPosition(50.f, 50.f); //    600  600    60    m_window.create(sf::VideoMode(600, 600), "15"); m_window.setFramerateLimit(60); //     m_text = sf::Text("F2 - New Game / Esc - Exit / Arrow Keys - Move Tile", Assets::Instance().font, 20); m_text.setFillColor(sf::Color::Cyan); m_text.setPosition(5.f, 5.f); return true; } 

Render()方法将从游戏控制器中的消息处理循环中调用,以绘制窗口和比赛场地的状态:

 void GameRender::Render() { m_window.clear(); m_window.draw(*this); m_window.draw(m_text); m_window.display(); } void GameRender::draw(sf::RenderTarget& target, sf::RenderStates states) const { states.transform *= getTransform(); sf::Color color = sf::Color(200, 100, 200); //     sf::RectangleShape shape(sf::Vector2f(GameModel::FIELD_SIZE, GameModel::FIELD_SIZE)); shape.setOutlineThickness(2.f); shape.setOutlineColor(color); shape.setFillColor(sf::Color::Transparent); target.draw(shape, states); //       shape.setSize(sf::Vector2f(GameModel::CELL_SIZE - 2, GameModel::CELL_SIZE - 2)); shape.setOutlineThickness(2.f); shape.setOutlineColor(color); shape.setFillColor(sf::Color::Transparent); //        sf::Text text("", Assets::Instance().font, 52); int *elements = m_game->Elements(); for (unsigned int i = 0; i < GameModel::ARRAY_SIZE; i++) { shape.setOutlineColor(color); text.setFillColor(color); text.setString(std::to_string(elements[i])); if (m_game->IsSolved()) { //      shape.setOutlineColor(sf::Color::Cyan); text.setFillColor(sf::Color::Cyan); } else if (elements[i] == i + 1) { //        text.setFillColor(sf::Color::Green); } //   ,   if (elements[i] > 0) { //      sf::Vector2f position(i % GameModel::SIZE * GameModel::CELL_SIZE + 10.f, i / GameModel::SIZE * GameModel::CELL_SIZE + 10.f); shape.setPosition(position); //     text.setPosition(position.x + 30.f + (elements[i] < 10 ? 15.f : 0.f), position.y + 25.f); target.draw(shape, states); target.draw(text, states); } } } 

GameController类


游戏中使用的最后一个类是控制器。

 #pragma once #include <SFML/Graphics.hpp> #include "GameRender.h" class GameController { GameModel *m_game; GameRender *m_render; public: GameController(GameModel *game, GameRender *render); ~GameController(); void Run(); }; 

该类非常简单,仅包含一种启动游戏的方法-Run()方法。 在构造函数中,控制器接受并存储指向模型类和游戏表示类的实例的指针。

Run()方法包含游戏的主要周期-处理消息并在presentation类中调用窗口渲染。

 void GameController::Run() { sf::Event event; int move_counter = 0; //       while (m_render->window().isOpen()) { while (m_render->window().pollEvent(event)) { if (event.type == sf::Event::Closed) m_render->window().close(); if (event.type == sf::Event::KeyPressed) { //    -    if (event.key.code == sf::Keyboard::Escape) m_render->window().close(); if (event.key.code == sf::Keyboard::Left) m_game->Move(Direction::Left); if (event.key.code == sf::Keyboard::Right) m_game->Move(Direction::Right); if (event.key.code == sf::Keyboard::Up) m_game->Move(Direction::Up); if (event.key.code == sf::Keyboard::Down) m_game->Move(Direction::Down); //   if (event.key.code == sf::Keyboard::F2) { m_game->Init(); move_counter = 100; } } } //     ,    if (move_counter-- > 0) m_game->Move((Direction)(rand() % 4)); //      m_render->Render(); } } 

最后,main()函数仍然存在

 #include "Assets.h" #include "GameModel.h" #include "GameRender.h" #include "GameController.h" int main() { Assets::Instance().Load(); //   GameModel game; //    GameRender render(&game); //   GameController controller(&game, &render); //   controller.Run(); //   return 0; } 

在其中,仅创建对象并启动游戏。

结果,在重构之后,我们将代码分为多个类,每个类都有其自己的功能目的。 如果游戏有所改进,我们可以更轻松地更改程序各个部分的内容。

最后,我将尝试简要地制定一些代码分解规则。

  • 每一堂课都有一个目的。 您不需要创建可以做任何事情的超类,否则您自己将来将无法应对
  • 突出显示类时,请确保类之间是松散耦合的。 总是在脑海中想象,该类的实现可能会完全不同。 但是,这不应影响其他项目类。 使用接口在类之间进行交互。
  • 必要时使用设计模式。 这将避免在实施长期存在的解决方案时出现不必要的错误。

该程序的所有资源都可以在这里获取

Source: https://habr.com/ru/post/zh-CN480710/


All Articles