Refactoring von SFML-Spielen

In einem früheren Artikel habe ich darüber gesprochen, wie man mit der SFML-Bibliothek ein einfaches Spiel erstellt. Am Ende des Artikels versprach ich, weiterzumachen und zu zeigen, wie man den Programmcode in eine korrektere Form bringt. Die Zeit für das Refactoring ist also gekommen.

Zuerst habe ich herausgefunden, welche Klassen ich für das Spiel brauchte. Es stellte sich heraus, dass ich eine Klasse für die Arbeit mit Spielressourcen brauchte - Assets. Von den Ressourcen habe ich jetzt nur eine herunterladbare Schriftart, aber in Zukunft können andere Ressourcen hinzugefügt werden, wie z. B. Bilder, Musik usw. Ich habe die Klasse zu einem Singleton gemacht, da diese Vorlage hervorragend für die Assets-Klasse geeignet ist. Grundlage war der bekannte Singleton Myers.

Als nächstes brauchen Sie in der Tat die Klasse des Spiels, die für die Logik des Programms und die Speicherung seines Zustands verantwortlich ist. Aus der Sicht der MVC-Ideologie ist diese Klasse ein Modell. Also habe ich es GameModel genannt.

Um das Spiel visuell anzuzeigen, benötigen Sie eine Klasse, die für das Rendern des Bildschirms verantwortlich ist. In der Ideologie ist MVC View. Ich habe diese Klasse GameRender genannt und sie von der abstrakten Klasse Drawable geerbt, die Teil der SFML-Bibliothek ist.

Nun, die letzte Klasse, die wir brauchen, ist die Klasse, die für die Interaktion mit dem Spieler verantwortlich ist - das ist der Controller. Es muss hier gesagt werden, dass in der klassischen Ideologie von MVC der Controller nicht direkt mit der Repräsentation interagieren sollte. Es sollte nur auf das Modell einwirken, und die Ansicht liest die Daten aus dem Modell allein oder gemäß dem Signal des Nachrichtensystems. Dieser Ansatz erschien mir in diesem Fall jedoch etwas überflüssig, so dass ich die Steuerung direkt mit der Ansicht verband. Es wäre also zutreffender anzunehmen, dass wir gemäß der MVP-Ideologie keinen Controller, sondern einen Presenter haben. Die Klasse habe ich allerdings GameController genannt.

Kommen wir nun zu der lustigen Sache, unseren Code in diese Klassen zu verteilen.

Klassen-Assets


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

Nach dem, was ich der Klasse hinzugefügt habe, ist dies eine Deklaration eines Mitglieds der Font-Klasse, um die geladene Schriftart zu speichern, und die Load-Methode, um sie zu laden. Alles andere ist eine Singleton-Myers-Implementierung.

Die Load-Methode ist ebenfalls sehr einfach:

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

Versucht, die calibri-Schriftart zu laden, und löst eine Ausnahme aus, wenn dies fehlschlägt.

GameModel-Klasse


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

Die gesamte Logik und alle Spieldaten werden in die Klasse aufgenommen. Die Klasse sollte im Prinzip so unabhängig wie möglich von der externen Umgebung sein und daher keine Links zur Ausgabe auf dem Bildschirm oder zur Benutzerinteraktion enthalten. Denken Sie beim Entwickeln daran, dass das Spielmodell auch dann funktionsfähig bleiben muss, wenn sich die Implementierung der Präsentationsklassen oder der Controller ändert.

Alle Methoden des Modells wurden im Prinzip in einem früheren Artikel beschrieben, weshalb ich sie hier nicht wiederholen werde. Das einzige, was ich sagen werde, ist, dass die IsSolved- und Elements-Getter für die Anforderungen der Präsentationsklasse zum Modell hinzugefügt wurden.

GameRender-Klasse


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

Der Zweck der GameRender-Klasse besteht darin, alle Daten zum Anzeigen des Spielfensters und zum Rendern des Spielfelds in sich selbst zu kapseln. Um sich mit dem Spielmodell zu verbinden, wird ein Zeiger auf das Modellobjekt für die gegebene Darstellung übertragen und im Konstruktor gespeichert. Der Konstruktor ruft auch die Init-Methode auf, die das Spielfenster erstellt und initialisiert.

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

Die Render () -Methode wird von der Nachrichtenverarbeitungsschleife im Gamecontroller aufgerufen, um das Fenster und den Zustand des Spielfelds zu zeichnen:

 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-Klasse


Die letzte im Spiel verwendete Klasse ist der Controller.

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

Die Klasse ist recht einfach und enthält nur eine Methode, die das Spiel startet - die Run () -Methode. Im Konstruktor akzeptiert und speichert der Controller Zeiger auf Instanzen der Modellklasse und der Spielpräsentationsklasse.

Die Methode Run () enthält den Hauptzyklus der Spielverarbeitungsnachrichten und des Renderings des Aufruffensters in der Präsentationsklasse.

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

Und schließlich bleibt die main () -Funktion erhalten

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

Erstellen Sie einfach Objekte und starten Sie das Spiel.

Nach dem Refactoring wurde der Code in Klassen unterteilt, von denen jede ihren eigenen funktionalen Zweck hat. Im Falle von Verbesserungen am Spiel ist es für uns einfacher, den Inhalt einzelner Programmteile zu ändern.

Abschließend werde ich versuchen, einige Regeln für die Codezerlegung kurz zu formulieren.

  • Jede Klasse sollte einen Zweck haben. Sie müssen keine Superklasse erstellen, die alles kann, sonst werden Sie selbst in Zukunft nicht damit fertig
  • Stellen Sie beim Hervorheben von Klassen sicher, dass die Klassen lose gekoppelt sind. Stellen Sie sich immer mental vor, dass die Implementierung der Klasse radikal anders werden kann. Dies sollte jedoch keine Auswirkungen auf andere Projektklassen haben. Verwenden Sie Schnittstellen, um zwischen Klassen zu interagieren.
  • Verwenden Sie bei Bedarf Entwurfsmuster. Auf diese Weise werden unnötige Fehler bei der Implementierung bewährter Lösungen vermieden.

Alle Quellen des Programms können hier entnommen werden .

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


All Articles