إعادة بيع ألعاب SFML

في مقال سابق ، تحدثت عن كيفية صنع لعبة بسيطة باستخدام مكتبة SFML. في نهاية المقال ، وعدتُ بالمتابعة وإظهار كيفية جعل رمز البرنامج في شكل أكثر صحة. لذلك ، لقد حان وقت إعادة بيع العقارات.

بادئ ذي بدء ، اكتشفت ما هي الفئات التي احتاجها للعبة. اتضح أنني بحاجة إلى فصل للعمل مع موارد اللعبة - الأصول. من بين الموارد ، لدي الآن خط قابل للتنزيل فقط ، ولكن في المستقبل ، يمكن إضافة موارد أخرى ، مثل الصور والموسيقى وما إلى ذلك. لقد جعلت الفصل منفردًا ، حيث يعمل هذا القالب بشكل رائع بالنسبة لفئة الأصول. وقد اتخذ الأساس من قبل سينغلتون المعروفة مايرز.

بعد ذلك ، تحتاج في الواقع إلى فئة اللعبة المسؤولة عن منطق البرنامج وتخزين حالته. من وجهة نظر أيديولوجية MVC ، هذه الفئة هي نموذج. لذلك دعوتها - GameModel.

لعرض اللعبة بصريًا ، فأنت بحاجة إلى فصل مسؤول عن عرض الشاشة. في الأيديولوجية ، MVC هو عرض. قمت بتسمية هذه الفئة GameRender ورثتها من الفئة التجريبية Drawable ، والتي تعد جزءًا من مكتبة SFML.

حسنًا ، الفئة الأخيرة التي نحتاجها - هذه هي الفئة المسؤولة عن التفاعل مع اللاعب - هذه هي وحدة التحكم. يجب أن يقال هنا أنه في الإيديولوجية الكلاسيكية لـ MVC ، يجب ألا تتفاعل وحدة التحكم مع التمثيل مباشرة. يجب أن يعمل فقط على النموذج ، ويقرأ العرض البيانات من النموذج من تلقاء نفسه ، أو بواسطة إشارة نظام الرسائل. لكن هذا النهج بدا لي زائداً إلى حد ما في هذه الحالة ، لذلك قمت بتوصيل وحدة التحكم مع العرض مباشرة. لذلك سيكون من الأصح افتراض أننا لا نملك وحدة تحكم ، ولكن مقدم العرض ، وفقًا لإيديولوجية MVP. الصف ومع ذلك ، دعوت GameController.

حسنًا ، دعنا الآن ننتقل إلى الأمر الممتع المتمثل في نشر التعليمات البرمجية الخاصة بنا في هذه الفئات.

الأصول الصفية


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

من ما أضفته إلى الفصل الدراسي ، هذا هو إعلان عضو من فئة الخط لتخزين الخط المحمل ، وطريقة التحميل لتحميله. كل شيء آخر هو تنفيذ المفرد مايرز.

طريقة التحميل بسيطة للغاية أيضًا:

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

يحاول تحميل الخط calibri ويلقي استثناء إذا فشل هذا.

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

يتم وضع جميع المنطق وجميع بيانات اللعبة في الفصل. يجب أن يكون الفصل ، من حيث المبدأ ، مستقلًا عن البيئة الخارجية قدر الإمكان ، وبالتالي ، يجب ألا يكون لديه أي روابط لإخراج الشاشة أو تفاعل المستخدم. عند التطوير ، تذكر أن نموذج اللعبة يجب أن يظل قيد التشغيل حتى لو تغير تطبيق فئات العرض التقديمي أو وحدة التحكم.

تم وصف جميع أساليب النموذج ، من حيث المبدأ ، في مقال سابق ، لذلك لن أكرر هنا. الشيء الوحيد الذي سأقوله هو أنه تمت إضافة حالتين IsSolved و Elements إلى النموذج لتلبية احتياجات فئة العرض التقديمي.

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

والغرض من فئة GameRender هو تجميع جميع البيانات في حد ذاتها لعرض نافذة اللعبة وتقديم الملعب. للتواصل مع طراز اللعبة ، يتم نقل مؤشر لكائن النموذج للتمثيل المحدد وتخزينه في المُنشئ. يقوم المنشئ أيضًا باستدعاء طريقة التهيئة ، والتي تنشئ وتهيئ نافذة اللعبة.

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

سيتم استدعاء أسلوب Render () من حلقة معالجة الرسائل في وحدة التحكم في اللعبة لرسم النافذة وحالة الملعب:

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

فئة لعبة التحكم


الفئة الأخيرة المستخدمة في اللعبة هي وحدة التحكم.

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

الفصل بسيط للغاية ويحتوي على طريقة واحدة فقط تطلق اللعبة - طريقة Run (). في المُنشئ ، تقبل وحدة التحكم المؤشرات وتخزينها على مثيلات فئة الطراز وفئة عرض اللعبة.

تحتوي طريقة Run () على الدورة الرئيسية للعبة - معالجة الرسائل وتقديم نافذة الاتصال في فئة العرض التقديمي.

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

وأخيراً ، تبقى الوظيفة الرئيسية ()

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

في ذلك ، مجرد خلق الأشياء وإطلاق اللعبة.

نتيجة لذلك ، بعد إعادة التوطين ، حصلنا على رمز مقسم إلى فئات ، لكل منها هدف وظيفي خاص بها. في حالة إدخال تحسينات على اللعبة ، سيكون من الأسهل بالنسبة لنا تغيير محتويات الأجزاء الفردية من البرنامج.

في الختام ، سأحاول صياغة بعض القواعد لفترة وجيزة لتحليل الشفرة.

  • يجب أن يكون لكل فصل هدف واحد. لا تحتاج إلى تكوين فئة فائقة الجودة يمكنها فعل أي شيء ، وإلا فلن تتمكن من التغلب عليها في المستقبل
  • عند تسليط الضوء على الفئات ، تأكد من أن الطبقات متقاربة. تخيل عقليا دائما أن تنفيذ الفصل يمكن أن يكون مختلفا جذريا. ومع ذلك ، يجب ألا يؤثر هذا على فئات المشاريع الأخرى. استخدام واجهات للتفاعل بين الطبقات.
  • استخدم أنماط التصميم عند الضرورة. هذا سوف تجنب الأخطاء غير الضرورية في تنفيذ الحلول الراسخة.

جميع مصادر البرنامج يمكن أن تؤخذ هنا .

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


All Articles