Em um
artigo anterior, eu falei sobre como criar um jogo simples usando a biblioteca SFML. No final do artigo, prometi continuar e mostrar como trazer o código do programa para uma forma mais correta. Então, chegou a hora da refatoração.
Primeiro de tudo, eu descobri quais classes eu precisava para o jogo. Acabou que eu precisava de uma aula para trabalhar com recursos do jogo - Ativos. Dos recursos, agora só tenho uma fonte para download, mas no futuro outros recursos, como imagens, músicas etc. Tornei a classe um singleton, pois esse modelo funciona muito bem para a classe Assets. A base foi tomada pelo conhecido cantor Myers.
Em seguida, você precisa, de fato, da classe do jogo responsável pela lógica do programa e pelo armazenamento de seu estado. Do ponto de vista da ideologia do MVC, essa classe é um modelo. Então eu chamei - GameModel.
Para exibir visualmente o jogo, você precisa de uma turma responsável por renderizar a tela. Na ideologia, MVC é View. Chamei essa classe de GameRender e a herdei da classe abstrata Drawable, que faz parte da biblioteca SFML.
Bem, a última classe que precisamos - esta é a classe responsável por interagir com o jogador - este é o Controller. Deve-se dizer aqui que, na ideologia clássica do MVC, o controlador não deve interagir diretamente com a representação. Ele deve atuar apenas no modelo, e a visualização lê os dados do modelo por conta própria ou pelo sinal do sistema de mensagens. Mas essa abordagem me pareceu um pouco redundante nesse caso, então eu conectei o controlador diretamente à visualização. Portanto, seria mais correto supor que não temos um controlador, mas o Presenter, de acordo com a ideologia do MVP. A turma, no entanto, chamei GameController.
Bem, agora vamos à coisa divertida de espalhar nosso código nessas classes.
Ativos 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; };
Pelo que adicionei à classe, esta é uma declaração de um membro da classe de fonte para armazenar a fonte carregada e o método Load para carregá-la. Tudo o resto é uma implementação única de Myers.
O método Load também é extremamente simples:
void Assets::Load() { if (!font.loadFromFile("calibri.ttf")) throw; }
Tenta carregar a fonte calibri e lança uma exceção se isso falhar.
Classe GameModel
#pragma once enum class Direction { Left = 0, Right = 1, Up = 2, Down = 3 }; class GameModel { public: static const int SIZE = 4;
Toda a lógica e todos os dados do jogo são colocados na classe. A classe deve, em princípio, ser o mais independente possível do ambiente externo, portanto, não deve ter links para saída para a tela ou para a interação do usuário. Ao desenvolver, lembre-se de que o modelo do jogo deve permanecer operacional, mesmo se a implementação das classes de apresentação ou do controlador mudar.
Todos os métodos do modelo, em princípio, foram descritos em um artigo anterior, portanto não repetirei aqui. A única coisa que direi é que os getters IsSolved e Elements foram adicionados ao modelo para as necessidades da classe de apresentação.
Classe 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; };
O objetivo da classe GameRender é encapsular todos os dados para exibir a janela do jogo e renderizar o campo de jogo. Para conectar-se ao modelo de jogo, um ponteiro para o objeto de modelo para a representação fornecida é transferido e armazenado no construtor. O construtor também chama o método Init, que cria e inicializa a janela do jogo.
GameRender::GameRender(GameModel *game) { m_game = game; Init(); } bool GameRender::Init() { setPosition(50.f, 50.f);
O método Render () será chamado a partir do loop de processamento de mensagens no controlador do jogo para desenhar a janela e o estado do campo de jogo:
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);
Classe GameController
A última classe usada no jogo é o 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(); };
A classe é bastante simples e contém apenas um método que inicia o jogo - o método Run (). No construtor, o controlador aceita e armazena ponteiros para instâncias da classe model e da classe de apresentação do jogo.
O método Run () contém o ciclo principal das mensagens de processamento do jogo e renderização da janela de chamada na classe de apresentação.
void GameController::Run() { sf::Event event; int move_counter = 0;
E, finalmente, a função main () permanece
#include "Assets.h" #include "GameModel.h" #include "GameRender.h" #include "GameController.h" int main() { Assets::Instance().Load();
Nele, basta criar objetos e iniciar o jogo.
Como resultado, após a refatoração, obtivemos o código dividido em classes, cada uma das quais com seu próprio objetivo funcional. No caso de melhorias no jogo, será mais fácil alterar o conteúdo de partes individuais do programa.
Em conclusão, tentarei formular brevemente algumas regras para decomposição de código.
- Cada classe deve ter um propósito. Você não precisa criar uma superclasse capaz de fazer qualquer coisa; caso contrário, você mesmo não lidará com ela no futuro.
- Ao destacar classes, verifique se as classes estão fracamente acopladas. Sempre imagine mentalmente que a implementação da classe pode se tornar radicalmente diferente. No entanto, isso não deve afetar outras classes de projeto. Use interfaces para interagir entre classes.
- Use padrões de design sempre que necessário. Isso evitará erros desnecessários na implementação de soluções estabelecidas há muito tempo.
Todas as fontes do programa podem ser obtidas
aqui .