En un
artículo anterior, hablé sobre cómo hacer un juego simple usando la biblioteca SFML. Al final del artículo, prometí continuar y mostrar cómo llevar el código del programa a una forma más correcta. Entonces, ha llegado el momento de la refactorización.
En primer lugar, descubrí qué clases necesitaba para el juego. Resultó que necesitaba una clase para trabajar con los recursos del juego: activos. De los recursos, ahora solo tengo una fuente descargable, pero en el futuro, se pueden agregar otros recursos, como imágenes, música, etc. Hice de la clase un singleton, ya que esta plantilla funciona muy bien para la clase Assets. La base fue tomada por el conocido singleton Myers.
A continuación, necesita, de hecho, la clase del juego, que es responsable de la lógica del programa y el almacenamiento de su estado. Desde el punto de vista de la ideología de MVC, esta clase es un modelo. Entonces lo llamé GameModel.
Para mostrar visualmente el juego, necesitas una clase que se encargue de representar la pantalla. En ideología, MVC es View. Llamé a esta clase GameRender y la heredé de la clase abstracta Drawable, que forma parte de la biblioteca SFML.
Bueno, la última clase que necesitamos, esta es la clase responsable de interactuar con el jugador, este es el controlador. Hay que decir aquí que en la ideología clásica de MVC, el controlador no debe interactuar directamente con la representación. Solo debe actuar sobre el modelo, y la vista lee los datos del modelo por sí mismo o por la señal del sistema de mensajes. Pero este enfoque me pareció algo redundante en este caso, así que conecté el controlador con la vista directamente. Por lo tanto, sería más correcto suponer que no tenemos un controlador, sino un presentador, de acuerdo con la ideología de MVP. La clase sin embargo, llamé GameController.
Bueno, ahora vamos a lo divertido de dispersar nuestro código en estas clases.
Activos de clase
#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; };
Por lo que agregué a la clase, esta es una declaración de un miembro de la clase de fuente para almacenar la fuente cargada, y el método Load para cargarla. Todo lo demás es una implementación única de Myers.
El método de carga también es extremadamente simple:
void Assets::Load() { if (!font.loadFromFile("calibri.ttf")) throw; }
Intenta cargar la fuente calibri y lanza una excepción si esto falla.
Clase GameModel
#pragma once enum class Direction { Left = 0, Right = 1, Up = 2, Down = 3 }; class GameModel { public: static const int SIZE = 4;
Toda la lógica y todos los datos del juego se colocan en la clase. La clase debería, en principio, ser tan independiente del entorno externo como sea posible, por lo tanto, no debería tener ningún enlace para salir a la pantalla o la interacción del usuario. Al desarrollar, recuerde que el modelo de juego debe permanecer operativo incluso si cambia la implementación de las clases de presentación o el controlador.
Todos los métodos del modelo, en principio, se describieron en un artículo anterior, por lo que no repetiré aquí. Lo único que diré es que los captadores IsSolved y Elements se agregaron al modelo para las necesidades de la clase de presentación.
Clase 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; };
El propósito de la clase GameRender es encapsular en sí misma todos los datos para mostrar la ventana del juego y representar el campo de juego. Para conectarse con el modelo del juego, un puntero al objeto modelo para la representación dada se transfiere y almacena en el constructor. El constructor también llama al método Init, que crea e inicializa la ventana del juego.
GameRender::GameRender(GameModel *game) { m_game = game; Init(); } bool GameRender::Init() { setPosition(50.f, 50.f);
Se llamará al método Render () desde el bucle de procesamiento de mensajes en el controlador del juego para dibujar la ventana y el estado del campo de juego:
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);
Clase GameController
La última clase utilizada en el juego es el controlador.
#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 clase es bastante simple y contiene solo un método que inicia el juego: el método Run (). En el constructor, el controlador acepta y almacena punteros a instancias de la clase de modelo y la clase de presentación del juego.
El método Run () contiene el ciclo principal del juego: el procesamiento de mensajes y la representación de ventanas de llamada en la clase de presentación.
void GameController::Run() { sf::Event event; int move_counter = 0;
Y finalmente, la función main () permanece
#include "Assets.h" #include "GameModel.h" #include "GameRender.h" #include "GameController.h" int main() { Assets::Instance().Load();
Simplemente crea objetos y comienza el juego.
Como resultado, después de refactorizar, obtuvimos código dividido en clases, cada una de las cuales tiene su propio propósito funcional. En el caso de mejoras en el juego, será más fácil para nosotros cambiar el contenido de partes individuales del programa.
En conclusión, intentaré formular brevemente algunas reglas para la descomposición del código.
- Cada clase debe tener un propósito. No necesita hacer una superclase que pueda hacer nada, de lo contrario, usted mismo no lo hará en el futuro
- Al resaltar clases, asegúrese de que las clases estén unidas libremente. Siempre imagine mentalmente que la implementación de la clase puede volverse radicalmente diferente. Sin embargo, esto no debería afectar a otras clases de proyectos. Use interfaces para interactuar entre clases.
- Use patrones de diseño cuando sea necesario. Esto evitará errores innecesarios en la implementación de soluciones establecidas desde hace mucho tiempo.
Todas las fuentes del programa se pueden tomar
aquí .