Refatorando jogos SFML

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

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

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

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

E, finalmente, a função main () permanece

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

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 .

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


All Articles