Refactoring game SFML

Dalam artikel sebelumnya, saya berbicara tentang cara membuat game sederhana menggunakan perpustakaan SFML. Di akhir artikel, saya berjanji akan melanjutkan dan menunjukkan cara membawa kode program ke bentuk yang lebih benar. Jadi, waktu untuk refactoring telah tiba.

Pertama-tama, saya mencari tahu kelas apa yang saya butuhkan untuk permainan. Ternyata saya membutuhkan kelas untuk bekerja dengan sumber daya game - Aset. Dari sumber daya, saya sekarang hanya memiliki font yang dapat diunduh, tetapi di masa depan, sumber daya lain dapat ditambahkan, seperti gambar, musik, dll. Saya membuat kelas sebagai singleton, karena templat ini bekerja sangat baik untuk kelas Aset. Dasar diambil oleh Myers singleton terkenal.

Selanjutnya, Anda perlu, pada kenyataannya, kelas permainan, yang bertanggung jawab atas logika program dan penyimpanan negaranya. Dari sudut pandang ideologi MVC, kelas ini adalah model. Jadi saya menyebutnya - GameModel.

Untuk menampilkan game secara visual, Anda membutuhkan kelas yang bertanggung jawab untuk menampilkan layar. Dalam ideologi, MVC adalah View. Saya menamai kelas ini GameRender dan mewarisinya dari kelas abstrak Drawable, yang merupakan bagian dari perpustakaan SFML.

Nah, kelas terakhir yang kita butuhkan - ini adalah kelas yang bertanggung jawab untuk berinteraksi dengan pemain - ini adalah Controller. Harus dikatakan di sini bahwa dalam ideologi klasik MVC, pengontrol tidak boleh berinteraksi dengan representasi secara langsung. Seharusnya hanya bertindak pada model, dan tampilan membaca data dari model sendiri, atau oleh sinyal sistem pesan. Tapi pendekatan ini sepertinya agak berlebihan bagi saya dalam hal ini, jadi saya menghubungkan controller dengan tampilan secara langsung. Jadi akan lebih tepat untuk mengasumsikan bahwa kita tidak memiliki controller, tetapi Presenter, sesuai dengan ideologi MVP. Namun di kelas, saya menelepon GameController.

Nah, sekarang mari kita ke hal yang menyenangkan dari menyebarkan kode kita ke kelas-kelas ini.

Aset Kelas


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

Dari apa yang saya tambahkan ke kelas, ini adalah deklarasi anggota kelas font untuk menyimpan font yang dimuat, dan metode Load untuk memuatnya. Yang lainnya adalah implementasi Myers tunggal.

Metode Load juga sangat sederhana:

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

Berusaha memuat font kalibri dan melempar pengecualian jika ini gagal.

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

Semua logika dan semua data game ditempatkan di kelas. Kelas pada prinsipnya harus se independen mungkin dari lingkungan eksternal, oleh karena itu, kelas tidak boleh memiliki tautan apa pun ke output ke layar atau interaksi pengguna. Ketika mengembangkan, ingat bahwa model permainan harus tetap operasional bahkan jika implementasi kelas presentasi atau pengontrol berubah.

Semua metode model, pada prinsipnya, dijelaskan dalam artikel sebelumnya, jadi saya tidak akan mengulanginya di sini. Satu-satunya hal yang akan saya katakan adalah bahwa getter IsSolved dan Elements ditambahkan ke model untuk kebutuhan kelas presentasi.

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

Tujuan dari kelas GameRender adalah untuk merangkum sendiri semua data untuk menampilkan jendela game dan merender bidang permainan. Untuk terhubung dengan model gim, penunjuk ke objek model untuk representasi yang diberikan ditransfer dan disimpan dalam konstruktor. Konstruktor juga memanggil metode Init, yang membuat dan menginisialisasi jendela permainan.

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

Metode Render () akan dipanggil dari loop pemrosesan pesan di pengontrol game untuk menggambar jendela dan keadaan lapangan bermain:

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

Kelas GameController


Kelas terakhir yang digunakan dalam game adalah 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(); }; 

Kelasnya cukup sederhana dan hanya berisi satu metode yang meluncurkan game - metode Run (). Dalam konstruktor, controller menerima dan menyimpan pointer ke instance kelas model dan kelas presentasi game.

Metode Run () berisi siklus utama permainan - memproses pesan dan rendering window panggilan di kelas presentasi.

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

Dan akhirnya, fungsi utama () tetap ada

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

Di dalamnya, hanya membuat objek dan meluncurkan game.

Akibatnya, setelah refactoring, kami mendapat kode yang dibagi menjadi beberapa kelas, yang masing-masing memiliki tujuan fungsionalnya sendiri. Dalam hal perbaikan pada gim, akan lebih mudah bagi kami untuk mengubah isi bagian-bagian individual dari program.

Sebagai kesimpulan, saya akan mencoba merumuskan secara singkat beberapa aturan untuk dekomposisi kode.

  • Setiap kelas harus memiliki satu tujuan. Anda tidak perlu membuat superclass yang dapat melakukan apa saja, jika tidak Anda sendiri tidak akan mengatasinya di masa depan
  • Saat menyorot kelas, pastikan kelas digabungkan secara longgar. Selalu bayangkan secara mental bahwa implementasi kelas dapat menjadi sangat berbeda. Namun, ini seharusnya tidak mempengaruhi kelas proyek lainnya. Gunakan antarmuka untuk berinteraksi antar kelas.
  • Gunakan pola desain jika perlu. Ini akan menghindari kesalahan yang tidak perlu dalam implementasi solusi yang sudah lama ada.

Semua sumber program dapat diambil di sini .

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


All Articles