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

SFML. Учимся переключаться между сценами

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

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

Пока что думаю этого достаточно. В этой публикации мы «оживим» базовый интерфейс, а домашним заданием вам будет сделать то же самое для контента внутри! ;).

SFML.  Учимся переключаться между сценами

И так, напомню, что эта публикация — это часть череды статей, где мы пишем простенькую игру. По этому сегодня мы отталкиваемся от того кода, который у нас уже был в конце прошлой публикации, ссылку на которую вы можете найти в заголовке. Чтоб понять о чём сегодня пойдёт речь — как минимум пробегите её взглядом! ;)

Добавляем конфиг

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

class config{
public:
    short int getWinHigt(){ // Высота окна
        return this -> WinHeight;
    }

    short int getWinWith(){ // Ширина окна
        return this -> WinWidth;
    }

    sf::Color getMainWindoWColor(){ // Цвет фоновой заливки окна
        return this -> MainWindowColor;
    }

    sf::Color getMainWindoWHeader(){ // Цвет шапки в окне
        return this -> HeaderColor;
    }

    bool Start(); // Тут будем загружать конфиг файл, вынесли в отдельный файл т.к. он довольно грамоздкий

private:
    short int WinHeight = 500; // Высота окна
    short int WinWidth = 400; // Ширина окна

    sf::Color MainWindowColor{8,150,40}; // Цвет фоновой заливки окна
    sf::Color HeaderColor = sf::Color::Blue; // Цвет шапки
};

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

Единственное, что стоит заметить для новичков и о чём до сих пор не упоминалось на сайте — это о использовании так называемых геттеров и сеттеров. Т.е. мы в private создаём все переменные и пишем целый список методов, задача которых — просто возвращать значения.

Геттер, если говорить максимально просто — это способ защитить поля класса от случайного изменения. Т.е. сделать так, чтоб получить значение — вы могли, а вот изменить — нет. Т.е. если бы, к примеру, поле WinWidth было в области public — вы могли бы по ошибке его изменить после создания окна. И из за этого шапка уже рисовалась бы уже или шире, чем само окно. Такой подход называется инкапсуляцией. Хотя не стоит забывать, что у данного подхода есть и минусы. Так же можете почитать ответы на этот же вопрос тут.

Метод bool Start() мы пока что опустим. Этот метод будет запускаться в main() и загружать параметры из файла. Таким образом мы и получим возможность смены дизайна нашей игры без вмешательства в исходный код. Но это задел на будущее, сегодня это так и останется, как есть.

Обработка событий

И так, давайте теперь займёмся обработкой событий внутри окна. Этим будут заниматься два метода класса. short int winEvent() и virtual short int pageEvent(). Как можно догадаться из названия, первый у нас будет отвечать за обработку глобальных событий внутри окна и будет единым для всех сцен, а второй будет перегружаться для каждой сцены по отдельности.

Так же обратите внимание на тип возвращаемого значения. Дело в том, что по клику, к примеру на заголовок, нам нужно будет переключаться между сценами. Для этого, напомню, нам нужно удалить этот класс и пересоздать новый. Но мы не можем удалить класс, который сейчас работает. По этому, если произошло событие, которое должно сменить сцену на другую, мы возвращаем номер сцены, на которую нужно переключиться и в функции main() уже удаляем этот класс. После чего создаём на его месте новый. Этот момент будет ниже. А пока что — разберём подробно сам метод winEvent().

virtual short int pageEvent(const sf::Event &event){ // обработчик кликов в классах наследниках
        return -1; // Если у наследников функция не перегружена - возвращаем значение по умолчанию
}

short int winEvent(sf::RenderWindow &window){
        sf::Event event;
        short int NextStep = -1;

        while (window.pollEvent(event)){
            NextStep = this -> pageEvent(event); // обрабатываем события внутри сцены

            switch (event.type){
                case sf::Event::Closed:
                    std::cout << "Закрыли окно" << std::endl;
                    window.close();
                    break;

                case sf::Event::MouseMoved: // Сохраняем положение указателя курсора мыши
                    this -> x = event.mouseMove.x;
                    this -> y = event.mouseMove.y;

                    if(event.mouseMove.y > 100) // Если указатель выше шапки - подсвечиваем заголовок белым
                        this -> text.setFillColor(sf::Color::Red);
                    else
                        this -> text.setFillColor(sf::Color(255, 255, 255));
                break;

                case sf::Event::MouseButtonPressed: // Клик мышки
                    // Клик правой кнопкой по шапке - переход к главной сцене
                    if(event.mouseButton.button == sf::Mouse::Left && this -> y < 100)
                        NextStep = 0;
                break;
            }
        }

        return NextStep; // Возвращаем маркер, нужен ли переход к другой сцене
}

В первую очередь мы создаём event со списком событий, а так же переменную NextStep, в которой будет по умолчанию значение -1. Это будет означать, что сцену менять не нужно. Если по ходу работы метода значение будет изменено — значит произошло событие смены.

И так, внутри цикла мы в первую очередь отправляем событие в локальный обработчик страницы this -> pageEvent(event). Пока что мы эти обработчики внутри страницы не пергружали, но позже сделаем это. После чего  у нас идёт обычный switch, который был в публикации о обработке событий.

В case sf::Event::MouseButtonPressed (событие клика мыши) мы проверяем, если клик произошёл именно левой кнопкой мыши, когда курсор был в области шапки — в таком случае мы присваиваем переменной NextStep значение 0, что означает, что нам нужно переключиться на сцену с индексом 0;

В case sf::Event::MouseMoved (движение указателя мыши) мы сохраняем положение курсора. Делаем мы это по двум причинам. Во-первых — мы таким образом даём возможность классам — наследникам максимально простой способ получить положение курсора. Да мы и сами используем эти параметры выше, в методе draw().

Но есть тут и ещё один момент. На самом деле, я пробовал получать значение event.mouseMove.x в случае, когда движения мыши не было — у меня почему то всегда выдавался 0. Но при этом event.mouseMove.y — всегда выдавал верные координаты. Честно, не знаю, это ошибка в библиотеке или я что то делал не так, но принял решение убить двух зайцев одновременно. И тут всегда иметь под рукой this -> x, и в классах наследниках тоже не заморачиваться. Хотя если вы можете подсказать, в чём суть такого странного поведения — буду рад, если поделитесь знаниями в комментариях! :)

Добавляем курсор

Теперь давайте добавим новый элемент в класс. Это маленький крудочек, который будет имитировать курсор. Для этого добавим в private поле sf::CircleShape cursor{5.f, 10}. И в конструкторе класса добавим свойства.

this -> cursor.setFillColor(sf::Color(255,255,255)); // Цвет указателя
this -> cursor.setPosition(1,1); // Позиция курсора по умолчанию
this -> cursor.setOrigin(5,5); // Смещаем, чтоб центр гружка был точно под курсором

После чего останется только вместе с движением мыши обновлять координаты этого кружка. Сделаем это в методе draw() одной строкой.

this -> cursor.setPosition(this -> x, this -> y);

Я же говорил, что они нам пригодятся! ;)

Обновляем main()

Теперь нам остаётся только все эти обновления соединить воедино в главной функции! Теперь она будет выглядеть так.

int main(){
    bazeDraw * scene = DravManager(1); // Создаём окно со сценой по умолчанию
    short int NextStep; // Маркер перехода к другой сцене

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

    sf::RenderWindow window(sf::VideoMode(Config.getWinWith(), Config.getWinHigt()), "Hello, World!", sf::Style::Default);
    window.setFramerateLimit(30); // Ограничеваем частоту кадров до 30 fps, для учебных целей нам этого вполне достаточно

    while (window.isOpen()){ // Основной жизненный цикл программы
        NextStep = scene -> winEvent(window); // Обрабатываем события текущего кадра

        if(NextStep < 0) // Проверяем, нужен ли переход к следующей сцене
            scene -> draw(window); // не нужен - отрисовываем контент нового кадра

        else{ // или переходим к следующей сцене
            scene -> ~bazeDraw(); // Очищаем память
            scene = DravManager(NextStep); // И создаём новый класс по этому же указателю
        }
    }

    return EXIT_SUCCESS;
}

И так, для начала мы создаём класс со сценой по умолчанию, используя функцию DravManager(0). Напомню, что нулевая сцена - это у нас сцена по умолчанию.

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

Далее изменения в основном цикле while. Тут мы каждый шаг обращаемся к только что написанному методу winEvent(). Он обрабатывает события и возвращает ответ, который проверяем ниже. Если он меньше нуля — просто отрисовываем следующий кадр. Если больше или равен — то вызываем у данного класса деструктор и создаём на его месте новый класс, с новой сценой.

И на этом моменте вы наверняка могли сделать одно, с одной стороны верное, замечание. При таком подходе мы теряем один кадр! Ведь когда NextStep >= 0 — срабатывает else и следующий кадр не рисуется! Да, замечание верное. Но это как раз пример того, когда оптимизация — важнее одного кадра. Дело в том, что проверка условия — тоже занимает некоторое количество ресурсов. И если процессору достаточно ресурсов — он может начать просчитывать условие в if ещё до того, как закончилась проверка условия. Проще говоря, если есть такая возможность — всегда в блок if добавляйте действие, которое будет выполняться с большей долей вероятности. А в данной ситуации — это как раз такой случай. Ведь переключение между сценами — это событие, которое происходит один раз из нескольких тысяч кадров. А то и вовсе пару раз за всю игру. И заставлять процессор просчитывать этот блок каждый раз — не самая логичная идея. Цена тому — один потерянный кадр на переключении, который вы даже и не заметите. Попробуйте сами запустить и вы увидите, что пропущенного кадра — даже не заметно! ;)

Послесловие

И да, вы не заметите переключение ещё и по тому, что при загрузке у нас выбрана та же сцена, что и по умолчанию. По этому измените строку bazeDraw * scene = DravManager(0) на bazeDraw * scene = DravManager(1). Таким образом вы сразу загрузите тестовую сцену, а по клику на шапке — переключитесь на сцену по умолчанию.

Опять же, как всегда, если у вас что то не получилось — весь получившийся исходный код можно скачать по этой ссылке и сравнить с тем, что получилось у вас.

И на этом у меня сегодня всё! Теперь мы можем не только рисовать разные сцены внутри интерфейса, а ещё и полноценно переключаться между ними! Разве не здорово? Теперь давайте вы попробуете сделать следующий шаг самостоятельно. Попробуйте самостоятельно перегрузить метод pageEvent() в классе mainDraw так, чтоб по клику на кружок — мы переключались на сцену класса testDraw. Такми образом мы окончательно победим тему обработки событий и работы с интерфейсом. По клику на кружок — сможем переключаться на тестовую сцену, а по клику на шапку — возвращаться обратно на главную страницу! Разве не здорово? :)

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

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

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

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

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