Refactorisation des jeux SFML

Dans un article précédent, j'ai expliqué comment créer un jeu simple à l'aide de la bibliothèque SFML. À la fin de l'article, j'ai promis de continuer et de montrer comment mettre le code du programme sous une forme plus correcte. Le moment de la refactorisation est donc venu.

Tout d'abord, j'ai trouvé les classes dont j'avais besoin pour le jeu. Il s'est avéré que j'avais besoin d'une classe pour travailler avec les ressources du jeu - Actifs. Parmi les ressources, je n'ai maintenant qu'une police téléchargeable, mais à l'avenir, d'autres ressources pourraient être ajoutées, telles que des images, de la musique, etc. J'ai fait de la classe un singleton, car ce modèle fonctionne très bien pour la classe Assets. La base a été prise par le célèbre singleton Myers.

Ensuite, vous avez besoin, en fait, de la classe du jeu, responsable de la logique du programme et du stockage de son état. Du point de vue de l'idéologie du MVC, cette classe est un modèle. Je l'ai donc appelé - GameModel.

Pour afficher visuellement le jeu, vous avez besoin d'une classe qui est responsable du rendu de l'écran. Dans l'idéologie, MVC est View. J'ai nommé cette classe GameRender et l'ai héritée de la classe abstraite Drawable, qui fait partie de la bibliothèque SFML.

Eh bien, la dernière classe dont nous avons besoin - c'est la classe responsable de l'interaction avec le joueur - c'est le contrôleur. Il faut dire ici que dans l'idéologie classique du MVC, le contrôleur ne doit pas interagir directement avec la représentation. Il ne doit agir que sur le modèle, et la vue lit les données du modèle seule ou par le signal du système de messagerie. Mais cette approche m'a semblé quelque peu redondante dans ce cas, j'ai donc connecté directement le contrôleur avec la vue. Il serait donc plus correct de supposer que nous n'avons pas de contrôleur, mais Presenter, selon l'idéologie de MVP. La classe cependant, j'ai appelé GameController.

Eh bien, passons maintenant à la chose amusante de disperser notre code dans ces classes.

Actifs de classe


#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; }; 

D'après ce que j'ai ajouté à la classe, il s'agit d'une déclaration d'un membre de la classe de police pour stocker la police chargée et de la méthode Load pour la charger. Tout le reste est une implémentation Myleton singleton.

La méthode Load est également extrêmement simple:

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

Tente de charger la police calibri et lève une exception en cas d'échec.

GameModel Class


 #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; } }; 

Toute la logique et toutes les données de jeu sont placées dans la classe. La classe doit, en principe, être aussi indépendante de l'environnement externe que possible, par conséquent, elle ne doit avoir aucun lien vers la sortie vers l'écran ou l'interaction avec l'utilisateur. Lors du développement, n'oubliez pas que le modèle de jeu doit rester opérationnel même si l'implémentation des classes de présentation ou le contrôleur change.

Toutes les méthodes du modèle, en principe, ont été décrites dans un article précédent, donc je ne répéterai pas ici. La seule chose que je dirai, c'est que les getters IsSolved et Elements ont été ajoutés au modèle pour les besoins de la classe de présentation.

GameRender Class


 #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; }; 

Le but de la classe GameRender est d'encapsuler en soi toutes les données pour afficher la fenêtre de jeu et rendre le terrain de jeu. Pour se connecter au modèle de jeu, un pointeur vers l'objet modèle pour la représentation donnée est transféré et stocké dans le constructeur. Le constructeur appelle également la méthode Init, qui crée et initialise la fenêtre de jeu.

 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; } 

La méthode Render () sera appelée à partir de la boucle de traitement des messages dans le contrôleur de jeu pour dessiner la fenêtre et l'état du terrain de jeu:

 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 Class


La dernière classe utilisée dans le jeu est le contrôleur.

 #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(); }; 

La classe est assez simple et ne contient qu'une seule méthode qui lance le jeu - la méthode Run (). Dans le constructeur, le contrôleur accepte et stocke des pointeurs vers des instances de la classe de modèle et de la classe de présentation de jeu.

La méthode Run () contient le cycle principal du jeu - traiter les messages et appeler le rendu de la fenêtre dans la classe de présentation.

 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(); } } 

Et enfin, la fonction main () reste

 #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; } 

Dans ce document, il suffit de créer des objets et de lancer le jeu.

En conséquence, après refactoring, nous avons obtenu le code divisé en classes, chacune ayant son propre objectif fonctionnel. Dans le cas d'améliorations du jeu, il nous sera plus facile de modifier le contenu des différentes parties du programme.

En conclusion, je vais essayer de formuler brièvement quelques règles pour la décomposition du code.

  • Chaque classe doit avoir un objectif. Vous n'avez pas besoin de créer une superclasse qui peut tout faire, sinon vous ne vous en sortirez pas à l'avenir
  • Lorsque vous mettez des classes en surbrillance, assurez-vous que les classes sont faiblement couplées. Imaginez toujours mentalement que l'implémentation de la classe peut devenir radicalement différente. Cependant, cela ne devrait pas affecter les autres classes de projet. Utilisez des interfaces pour interagir entre les classes.
  • Utilisez des modèles de conception si nécessaire. Cela évitera des erreurs inutiles dans la mise en œuvre de solutions établies de longue date.

Toutes les sources du programme peuvent être consultées ici .

Source: https://habr.com/ru/post/fr480710/


All Articles