Qbik-club
Дата публикации:21.07.22 20:01;Автор:Евгений;Категория: программирование;Теги:, ;

SFML. Создаём класс для рисования сцены

Сегодня в качестве продолжения темы графической библиотеки SFML мы перейдём от создания простых геометрических фигур к теме создания классов для рисования сцен. На самом деле, если у вас нет опыта в программировании на языке С++, тема может показаться довольно сложной. Но поверьте, как только вы разберётесь, вы поймёте, что на самом деле тут нет ничего сложного! ;)

SFML.  Создаём класс для рисования сцены

И так, для начала давайте вспомним, что у нас было до этого. До сегодняшнего дня ввесь наш код выглядел ориентировочно вот так

#include <SFML/Graphics.hpp>

int main(){
    sf::RenderWindow window(sf::VideoMode(200, 200), "Hello, world!");
    sf::CircleShape shape(100.f);
    shape.setFillColor(sf::Color::Green);

    while (window.isOpen()){
        sf::Event event;
        while (window.pollEvent(event)){
            if (event.type == sf::Event::Closed)
                window.close();
        }

        window.clear();
        window.draw(shape);
        window.display();
    }

    return 0;
}

В принципе, всё работало. Так в чём проблема? Проблема появилась уже в наших прошлых публикациях, взять хотя бы публикацию с рисованием фигур. Уже в этой публикации, в части, где мы пытались разместить рядом несколько фигур, у нас вырисовывалось не плохое такое полотно с кодом. А представьте себе, что у вас не две — три фигуры, а их десяток? И да, давайте не забывать, что в перспективе, в конце этой череды публикаций, мы хотим попробовать написать простенькую игрушку. А она как минимум подразумевает, что у нас будет несколько сцен.

По этому нам как минимум надо возможность смены сцен и возможность разделить вынести код каждой из них в разные места. Именно эти проблемы отлично решает воздание классов, каждый из которых отвечает за свою сцену.

Алгоритм работы

И так, повторюсь ещё раз, что в отличии от прошлых публикаций, где мы просто рисовали фигурки, данная статья может показаться довольно сложной для начинающих С++ программистов. А по скольку данный цикл публикаций предназначен в первую очередь для этой аудитории, то давайте я сейчас постараюсь максимально подробно всё описать простыми словами, а уже затем продемонстрировать то же самое в виде кода.

Начнём с постановления задачи. Нам нужно сделать так, чтоб мы могли написать код нашей сцены, а лучше нескольких, где то в стороне, а затем, по необходимости, «подключать» нужную сцену в нужный момент.

Для этого нам идеально подходит вариант написать базовый класс, затем от его наследовать классы, которые будут работать с каждой отдельно взятой сценой.

Благодаря такому подходу, даже если вам по ходу работы придёт в голову идея добавить что то новое, вам не понадобится переписывать весь код, как было бы, если бы мы работали «в старом стиле». Вам просто достаточно написать ещё один класс и добавить его в главной функции.

И так, что мы будем делать сегодня? Сегодня мы создадим базовый класс. В этом классе мы определим элементы, которые будут на всех сценах. В нашем случае это будет шапка с логотипом сайта.

После определения базового класса, мы создадим два класса — наследника. Один будет выводить в окне чёрный кружок, а второй — белый.

В итоге у нас получится окно, в котором уже будет оформленная шапка, а под шапкой, в зависимости от сцены, кружок белого или чёрного цвета.

Звучит просто, давайте теперь реализуем всё это на практике! :)

Базовый класс

И так, начнём с написания базового класса. Он у нас выглядит так.

class bazeDraw{
public:
    bazeDraw(){ // Базовый конструктор, создаём в окне шапку, которая будет общей для всех сцен
        if(this -> font.loadFromFile("/home/qbik/example_post/cpp/arial.ttf")){
            // Формируем текст
            this -> text.setFont(font); // Присваиваем тексту загруженный шрифт
            this -> text.setString("Qbik.club"); // Пишем текст
            this -> text.setCharacterSize(24); // Размер шрифта
            this -> text.setFillColor(sf::Color::Red); // Цвет текста
            this -> text.setStyle(sf::Text::Bold | sf::Text::Underlined); // Стиль текста
            this -> text.setPosition(150, 30); // И смещаем

            // И фон за текстом
            this -> rectangle.setFillColor(sf::Color::Blue); // Присваиваем шапке цвет
        }

        else{ // Если не удалось загрузить шрифт - вешаем ошибку и выводим сообщение
            std::cout << "Ошибка загрузки шрифта!" << std::endl;
            this -> MarkerError = true;
        }
    };

    virtual void drawPage(sf::RenderWindow &window){
        // Отрисовка сцен для унаследованных классов, будем описывать в классах-наследниках
    }

    void draw(sf::RenderWindow &window){ // Метод, который будет рисовать сцены
        window.clear(this -> ColorPage); // Очищаем окно

        this -> drawPage(window); // Отрисовка сцены в наследованном классе

        // После отрисовки контента - добавляем в окно общие элементы для всех сцен
        window.draw(this -> rectangle); // И фон за текстом
        window.draw(this -> text); // Выводим текст

        // И выводим всё это добро на экран
        window.display();
    };

    bool issError(){ // Возвращаем маркер, были ли ошибки в процессе работы
        return this -> MarkerError;
    }

protected:
    bool MarkerError = false; // Маркер, были ли ошибки в процессе работы программы

private:
    sf::Font font; // Класс для загрузки шрифта
    sf::Text text; // И для текста
    sf::RectangleShape rectangle{sf::Vector2f{400, 100}}; // квадратик, на котором будет писаться заголовок
    sf::Color ColorPage{8,150,40}; // Цвет, которым будет заливаться окно

};

Возможно выглядит страшно, для не подготовленного пользователя, но не волнуйтесь, на самом деле тут нет ничего сложного. Сейчас всё разберём по пунктам.

Начнём с конструктора. Тут мы загружаем шрифт, которым будет писаться текст, оформляем текст, «прикручиваем» ему нужные стили... В общем всё то же самое, что делалось в этой, этой и этой публикациях с той разницей, что теперь мы код вынесли в конструктор класса.

Сделал я это не просто так. Дело в том, что раз у нас шапка будет статической, то логичнее сделать эту работу один раз в конструкторе класса и далее просто выводить уже заранее подготовленный контент.

Теперь давайте перейдём к полю protected. Тут мы хараним маркер работы класса. Пока что, по сути говоря, данный маркер нам нужен только чтоб отследить, удачно ли загружен шрифт. Но со временем, по мере роста, мы обязательно его расширим! ;)

И в поле private у нас хранятся переменные, которые нужны только для отрисовки шапки окна. По этому мы их и закрываем от наследников. Тут опять же ничего нового, с одной заметкой. Вы наверное обратили внимание, что обычные скобки, которые раньше передавали параметры в конструкторы классов, неожиданно превратились в фигурные. Сделано это не просто так. Дело в том, что если мы в классе будем передавать аргументы через обычные скобки, то компилятору будет сложно понять, что мы от его хотим. Создать экземпляр класса или же объявить метод. Чтоб не раздувать публикцию ещё больше, подробные объяснения этого явления я опущу. Если интересно — подробности можете узнать тут или тут.

И так, теперь кратко о методах. issError(), как не сложно догадаться, возвращает маркер, который говорит о том, были ли ошибки в процессе работы класса. Его мы будем вызывать в функции main, чтоб проверить, всё ли удачно создалось.

Далее draw(). Этот метод у нас будет вызываться для отрисовки кадров. Как и ранее, сразу мы очищаем окно, затем в нём что то рисуем, а в конце вызываем window.display(), чтоб отрисовать кадр. Но не забываем о том, что теперь у нас данный метод рисует только базовый интерфейс! Непосредственно контентом — должен заниматься кто то другой! Кто? Следующий метод.

Это виртуальный метод void drawPage(). Он пока что пустой т.к. это лишь базовый класс. Что будет рисовать данный метод — решит класс наследник.

Как всё работает?

И так, в конце давайте ещё раз повторим, как работает класс? Для начала мы в конструкторе создаём базовый интерфейс, который у нас будет на всех сценах. Далее на каждом шаге анимации вызывается метод draw(). Метод сразу очищает окно, затем вызывает метод drawPage(), который рисует всю сцену, а в конце добавляет сцене весь базовый интерфейс и выводит кадр в окно.

Кстати, вы не задумались, почему нужно сразу выводить контент сцены, а затем базовый интерфейс? Дело в том, что если элементы перекрывают друг друга, то сверху выводится тот, который рисовался позже. Именно по этому, если какой то элемент в окне будет пересекаться с нашей шапкой, он будет под ней, а не сверху.

Пишем наследника

И так, у любой программы должна быть какая то базовая точка. По этому давайте напишем класс, который в будущем станет нашим главным окном. Пусть это будет класс mainDraw. В таком случае выглядеть он будет так.

class mainDraw : public bazeDraw{
public:

    mainDraw() : bazeDraw(){ // формируем контент для сцены
        // Т.к. у нас пока что нет анимации, просто статическая картинка,
        // Всю сцену рисуем в конструкторе.

        this -> shape.setPosition(130, 150); // смещаем позицию круга
        this -> shape.setFillColor(sf::Color::Black); // И присваеваем ему белый цвет
    }

    void drawPage(sf::RenderWindow &window){ // Отрисовка окна
        // В качестве тестового контента сцены - добавляем кружок
        window.draw(this -> shape);
    }

private:
    sf::CircleShape shape{70.f, 50}; // Кружок, который будет выводиться в качестве контента сцены
};

Пока что в качестве теста весь контент у нас будет состоять из одного кружочка. по этому и класс у нас оказался минималистичный.

В данном классе мы сразу вызываем конструктор базового класса, который инициализирует базовый интерфейс, после чего в конструкторе текущего класса присваиваем нашему контенту, в данном случае кругу, нужные стили.

Так же описываем наш виртуальный метод drawPage(), который как раз и определяет то, что будет рисовать данный класс. В данном случае, как уже говорил выше, это просто вывод круга.

Главная функция

И так, класс у нас уже готов. Осталось только всё это добро правильно запустить. Для этого подключаем только что описанные классы в проект и переписываем функцию main() следующим образом.

#include <SFML/Graphics.hpp>
#include <iostream>
#include <string>

#include "window_event.cpp" // Файл с функцией WindowEvent()
#include "draw_baze.cpp" // Базовый класс, от которого будем наследоваться
#include "draw_main.cpp" // Сцена с чёрным кружком, который позже превратится в главную сцену
#include "draw_test.cpp" // Сцена с белым кружком, которая тоже когда нибудь во что нибудь превратится! :)

int main(){
    sf::RenderWindow window(sf::VideoMode(400, 500), "Hello, World!", sf::Style::Default);
    window.setFramerateLimit(30); // Ограничеваем частоту кадров до 30 fps, для учебных целей нам этого вполне достаточно
    mainDraw scene; // Создаём класс для работы с окном

    if(scene.issError()){ // Если при создании класса возникли ошибки - завершаем работу
        std::cerr << "При создании класса возникла ошибка, работа проргаммы завершена." << std::endl;
        return EXIT_FAILURE; // И возвращаем системе маркер, что программа завершилась с ошибкой
    }

    // Далее - всё без изменений, за исключением того, что вывод контента "спрятан" в класс

    while (window.isOpen()){
        sf::Event event;

        while (window.pollEvent(event)){
            WindowEvent(event, window);
        }

        scene.draw(window); // Теперь выводом контента занимается класс scene, по этому просим его отрисовать кадр
    }

    return EXIT_SUCCESS;
}

И так, в начале мы видим обычное создание окна, как мы делали ранее. Затем нечто новое, вызов window.setFramerateLimit(30). Тут мы ограничиваем частоту кадров до 30 fps. Делать это не обязательно, но я добавил т.к. в учебных целях нет смысла грузить систему лишними кадрами.

А вот далее уже вместо создания фигур, как было раньше, mainDraw scene. Тут мы создаём наш только что описанный класс главной страницы. И используя метод issError() проверяем, удачно ли всё прошло. Если удачно — отлично, можно приступать к рисованию! В итоге у нас получится вот такое окошко :)

Как видим, в основном цикле программы вместо всех методов рисования остался только один вызов scene.draw(window). Т.к. данный метод общий для всех классов наследников, нам нет разницы какой именно класс создан. Вы точно так же можете создать два, три, пять классов, описать в них любые сцены и они точно так же будут отрисованы внутри нашего окна, оформленного так, как было описано в базовом классе.

Проверяем работу

В конце давайте вы сами попробуете всё на практике. Я сохранил готовый проект по этой ссылке и описал в нём два класса. Один — mainDraw, с которым вы уже знакомы, второй — testDraw. Попробуйте для начала создать окно с использованием класса mainDraw, затем с testDraw. В итоге у вас получатся вот такие два окошка.

Примеры двух окон

Затем попробуйте создать свой, третий вариант и проверьте, как всё работает. Уверен, у вас всё получится! Не забудьте поделиться в комментариях ссылками на свои работы, посмотрим, что у кого получилось! ;)

А сегодня у меня на этом всё, и так публикация получилась слишком объёмной. Уже в следующих публикациях вы узнаете, как можно переключаться между сценами не перезапуская программу, как обрабатывать события и другие шаги, которые в плотную приблизят нас к созданию хоть и простенькой, но полноценной игрушки! Удачи, уверен, у вас всё получится! :)

Публикация относится к тематической подборке: «Библиотека SFML»

Цикл публикаций посвящённый библиотеке SFML. Разбираемся с работой этой библиотеки от самых первых шагов до создания простых игр.

Понравилась публикация? Поделись ей с друзьями!

Понравился сайт? Подпишьсь на нас в соцсетях!

Мы в TelegramМы ВконтактеМы в ТвиттерМы на фейсбукМы в одноклассниках
Опубликовать
Загрузка рекомендуемых публикаций