Небольшая библиотека для SFML для упрощения выполнения программы

В течение последних недель или двух я работал над небольшой библиотекой, которая призвана упростить разработку игр SFML, и после ее завершения мне хотелось бы получить о ней отзывы. У меня нет доступа к преподавателям, поэтому отзывы очень важны для меня, когда я пытаюсь стать лучше в программировании. Заранее благодарим всех, кто нашел время, чтобы изучить мой код.

Ссылка на SFML, если вы с ним не знакомы. Это библиотека для создания простых 2D-игр (так что да, моя библиотека – это библиотека для библиотеки).

Причина, по которой я начал работать над этим маленьким хобби-проектом, заключается в том, что я делал несколько игр с SFML, и я заметил, что main.cpp становится все более загруженным и загруженным, с все большим количеством вызовов обработчиков для обновления логики и рисования спрайтов на окно. Я хотел упростить выполнение программы, чтобы ее можно было читать почти как те парадигмы потока программы, которые вы видите в книгах по разработке игр:

while (gameRunning)
{
    handleInput();

    updateLogic();

    render();
}

В итоге я придумал два абстрактных базовых класса, LogicHandler а также GraphicalHandler, который разбил все обработчики на логику и графику. Каждый обработчик, который обрабатывает игровую логику, должен быть производным от LogicHandler и каждый обработчик, который обрабатывает графику, должен быть производным от GraphicalHandler.

Обработчик в моем коде – это объект, который отслеживает определенный аспект игры. Обработчики логики отслеживают логику игры, в то время как графические обработчики отслеживают графические объекты, которые можно нарисовать на экране. Если обработчику нужны обновления от другого обработчика, он должен хранить указатель на этот обработчик, чтобы он всегда мог обновлять себя, используя этот указатель.

Я хотел иметь возможность обновлять все обработчики всего несколькими вызовами методов в основном игровом цикле. Для достижения этой цели, LogicHandler а также GraphicalHandler оба используют метод, при котором они хранят указатели на каждый экземпляр своего класса в статическом векторе, называемом s_allInstances_(s_ для статики). Это позволяет им в статических методах перебирать каждый активный обработчик и вызывать для них методы. LogicHandler использует это для перебора всех логических обработчиков, доставки событий и их обновления. GraphicalHandler использует это, чтобы перебрать все графические обработчики, обновить их и нарисовать их спрайты на экране.

Это может показаться запутанным, поэтому позвольте мне вкратце объяснить, как эти классы используют s_allInstances_.

LogicHandler состоит из двух статических методов – handleEvent, updateLogic и два чисто виртуальных метода – receiveEvent а также update. Каждый виртуальный метод соответствует одному из статических методов, вы, вероятно, можете увидеть, какой из них принадлежит.

Когда LogicHandler::handleEvent вызывается из игрового цикла, когда обнаруживается ввод игрока, он проходит через s_allInstances_ и звонки receiveEvent (и передает событие) каждому логическому обработчику.

Когда LogicHandler::updateLogic вызывается во время каждой итерации игрового цикла, он проходит через s_allInstances_ и звонки update на каждом логическом обработчике.

Это делает так, что независимо от того, сколько существует логических обработчиков, все они могут быть одновременно обновлены или получать ввод от игрока с помощью всего лишь одного вызова метода.

GraphicalHandler имеет два статических метода – updateGraphicalObjects, render и один чисто виртуальный метод – update

Все графические обработчики наследуют вектор указателей на чертежи (спрайты, фигуры и т. Д.), Называемый graphicalObjects_. Все, что графический обработчик хочет отображать на экране, должно быть перенесено на graphicalObjects_.

Когда GraphicalHandler::updateGraphicalObjects вызывается, он перебирает все графические обработчики и вызывает update на них, когда обработчики могут обновлять спрайты в своих graphicalObjects_ vector, используя указатели на логические обработчики, которые они сохранили как члены.

Когда GraphicalHandler::render вызывается, он перебирает все графические обработчики и отображает спрайты внутри их graphicalObjects_ векторов.

Как и в случае с обработчиками логики, не имеет значения, сколько существует графических обработчиков, все они могут быть обновлены или отрисованы с помощью всего одного вызова метода.

Объединение LogicHandler а также GraphicalHandler оставляет вам очень простой игровой цикл, похожий на парадигму, которую я показал в начале:

while(window.isOpen())
{
    sf::Event event;
    while(window.pollEvent(event)
    {
        LogicHandler::handleEvent(event, window);
    }
    LogicHandler::updateLogic();
    GraphicalHandler::updateGraphicalObjects();
    GraphicalHandler::render();
}

Разобравшись с общей идеей, перейдем к собственно коду.

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

OBS: Я также добавил класс и main.cpp в конце, используя библиотеку, создайте небольшую программу, в которой вы управляете квадратом, просто чтобы показать вам, как я намеревался использовать базу кода.

Наконец, вот код:

InstanceTracker.h

#pragma once
#include <vector>

template <typename T>
class InstanceTracker
{
public:
    InstanceTracker() noexcept
    {
        s_allInstances_.push_back(static_cast<T*>(this));
    }
    InstanceTracker(const InstanceTracker& source) noexcept
        : InstanceTracker()
    {
    }
    InstanceTracker(const InstanceTracker&& source) noexcept
        : InstanceTracker()
    {
    }
    virtual ~InstanceTracker() noexcept
    {
        auto it = std::find(s_allInstances_.begin(), s_allInstances_.end(), this);
        int index = it - s_allInstances_.begin();
        s_allInstances_.erase(s_allInstances_.begin() + index);
    }
    void moveMyInstanceToLast()
    {
        auto it = std::find(s_allInstances_.begin(), s_allInstances_.end(), this);
        std::rotate(it, it + 1, s_allInstances_.end());
    }
    void moveMyInstanceToFirst()
    {
        auto it = std::find(s_allInstances_.begin(), s_allInstances_.end(), this);
        s_allInstances_.erase(it);
        s_allInstances_.insert(s_allInstances_.begin(), static_cast<T*>(this));
    }
protected:
    static std::vector<T*> s_allInstances_;
};

template<typename T>
std::vector<T*> InstanceTracker<T>::s_allInstances_;

LogicHandler.h

#pragma once
#include "InstanceTracker.h"
#include "SFML/Graphics.hpp"
#include <vector>

class LogicHandler : public InstanceTracker<LogicHandler>
{
public:
    virtual bool receiveEvent(const sf::Event& event, const sf::RenderWindow& window) = 0;

    /* The reason receiveEvent() returns a boolean is to avoid unnecessary looping
    * in handleEvent(). If a handler discovers an event that definitely only applies
    * to that handler, then it should return true from receiveEvent().
    * handleEvent() will then stop looping when it encounters it, thus avoiding a lot of looping.
    * To take advantage of this, handlers should be declared in order of how common the events 
    * they're listening for are. E.g., a handler that listens for the arrow keys being pressed 
    * to move the player should be declared earlier than a handler that listens for a button being 
    * pressed, since the arrow keys input will be much more common.
    */

    virtual void update() = 0;

    static void updateLogic();
    static void handleEvent(const sf::Event& event, const sf::RenderWindow& window);
};

LogicHandler.cpp

#include "LogicHandler.h"
void LogicHandler::updateLogic()
{
    for (LogicHandler* handler : s_allInstances_)
    {
        handler->update();
    }
}

void LogicHandler::handleEvent(const sf::Event& event, const sf::RenderWindow& window)
{
    for (LogicHandler* handler : s_allInstances_)
    {
        if(handler->receiveEvent(event, window))
        {
            break;
        }
    }
}

PositionHandler.h

//Implementation in the header file for such a simple class
#pragma once
#include "LogicHandler.h"
class PositionHandler : public LogicHandler
{
public:
    PositionHandler() { position_ = sf::Vector2f(0, 0); rotation_ = 0; }
    sf::Vector2f getPosition() const { return position_; }
    float getRotation() const { return rotation_; }
protected:
    sf::Vector2f position_;
    float rotation_;
};

Button.h

#pragma once
#include "LogicHandler.h"
class Button : public LogicHandler
{
public:
    Button(sf::Vector2f size, sf::Vector2f position = sf::Vector2f(0.f, 0.f));
    virtual bool receiveEvent(const sf::Event& event, const sf::RenderWindow& window) override;
    virtual void buttonPressed() = 0;
protected:
    sf::Rect<float> hitbox_;
};

Button.cpp

#include "Button.h"

Button::Button(sf::Vector2f size, sf::Vector2f position)
{
    hitbox_.width = size.x;
    hitbox_.height = size.y;
    hitbox_.left = position.x;
    hitbox_.top = position.y;
}

bool Button::receiveEvent(const sf::Event& event, const sf::RenderWindow& window)
{
    if (event.type == sf::Event::MouseButtonPressed && hitbox_.contains(sf::Vector2f(sf::Mouse::getPosition(window))))
    {
        buttonPressed();
        return true;
    }
    return false;
}

PauseButton.h

#pragma once
#include "Button.h"
class PauseButton : public Button
{
    friend class PauseButtonIcon;
    friend class GameEngine;
public:
    PauseButton(sf::Vector2f size, sf::Vector2f position);
    void update() override;
    void buttonPressed() override;
private:
    bool bPaused_;
};

PauseButton.cpp

#include "PauseButton.h"

PauseButton::PauseButton(sf::Vector2f size, sf::Vector2f position)
    : Button(size, position), bPaused_(false)
{
}

void PauseButton::buttonPressed()
{
    bPaused_ = bPaused_ ? false : true;
}

void PauseButton::update()
{
}

GraphicalHandler.h

#pragma once

#include "InstanceTracker.h"
#include "ClonableDrawable.h"
#include "LogicHandler.h"

#include <SFML/Graphics.hpp>

#include <vector>
#include <memory>

class GraphicalHandler : public InstanceTracker<GraphicalHandler>
{
public:
    GraphicalHandler();
    GraphicalHandler(const GraphicalHandler& source);
    
    virtual void update() = 0;
    static void updateGraphicals();
    static void renderObjects(sf::RenderWindow& window);
protected:
    std::vector<std::shared_ptr<ClonableDrawable>> graphicalObjects_;
    bool bVisible_;
};

GraphicalHandler.cpp

#include "GraphicalHandler.h"

GraphicalHandler::GraphicalHandler()
    : bVisible_(true)
{
}

GraphicalHandler::GraphicalHandler(const GraphicalHandler& source)
    : bVisible_(source.bVisible_), InstanceTracker<GraphicalHandler>(source)
{
    for (std::shared_ptr<ClonableDrawable> ptr : source.graphicalObjects_)
    {
        graphicalObjects_.push_back(ptr->clone());
    }
}

void GraphicalHandler::updateGraphicals()
{
    for (GraphicalHandler* handler : s_allInstances_)
    {
        handler->update();
    }
}

void GraphicalHandler::renderObjects(sf::RenderWindow& window)
{
    for (GraphicalHandler* handler : s_allInstances_)
    {
        if (handler->bVisible_)
        {
            for (std::shared_ptr<ClonableDrawable> object : handler->graphicalObjects_)
            {
                window.draw(*object);
            }
        }
    }
}

ClonableDrawable.h

/* The reason ClonableDrawable exists is because graphical handlers cannot be copied if they can't copy 

the contents of their vector of drawables. `sf::Drawable` is abstract
 
so to be able to copy them I added these two classes below. */


#pragma once
#include "SFML/Graphics.hpp"
#include <memory>

class ClonableDrawable
{
public:
    virtual ~ClonableDrawable() = default;

    virtual std::unique_ptr<ClonableDrawable> clone() const = 0;

    virtual operator sf::Drawable& () = 0;
};

template <typename T>
class ClonableDraw : public T, public ClonableDrawable
{
public:
    ClonableDraw() = default;

    template<typename... Args>
    ClonableDraw(Args&... args): T(args...) {}

    template<typename... Args>
    ClonableDraw(Args&&... args): T(args...) {}

    std::unique_ptr<ClonableDrawable> clone() const override
    {
        return std::make_unique<ClonableDraw<T>>(*this);
    }

    operator sf::Drawable& () override { return *this; }
};

PauseButtonIcon.h

#pragma once
#include "GraphicalHandler.h"
#include "PauseButton.h"
#include "SFML/Graphics.hpp"
class PauseButtonIcon : public GraphicalHandler
{
public:
    PauseButtonIcon(std::shared_ptr<PauseButton> handler, sf::Color color);
    void update() override;
private:
    std::shared_ptr<PauseButton> handler_;

    std::shared_ptr<ClonableDraw<sf::VertexArray>> playIcon_;

    std::shared_ptr<ClonableDraw<sf::RectangleShape>> pauseIcon1_;
    std::shared_ptr<ClonableDraw<sf::RectangleShape>> pauseIcon2_;
};

PauseButtonIcon.cpp

#include "PauseButtonIcon.h"
PauseButtonIcon::PauseButtonIcon(std::shared_ptr<PauseButton> handler, sf::Color color)
    : handler_(handler)
{
    playIcon_ = std::make_shared<ClonableDraw<sf::VertexArray>>(sf::Triangles, 3);
    (*playIcon_)[0].position = sf::Vector2f(handler->hitbox_.left, handler->hitbox_.top);
    (*playIcon_)[1].position = sf::Vector2f(handler->hitbox_.left, handler->hitbox_.top+handler_->hitbox_.height);
    (*playIcon_)[2].position = sf::Vector2f(handler->hitbox_.left+handler_->hitbox_.width, handler->hitbox_.top+handler_->hitbox_.width/2);
    (*playIcon_)[0].color = color;
    (*playIcon_)[1].color = color;
    (*playIcon_)[2].color = color;

    pauseIcon1_ = std::make_shared<ClonableDraw<sf::RectangleShape>>(sf::Vector2f(handler_->hitbox_.width / 3, handler_->hitbox_.height));
    pauseIcon1_->setPosition(handler_->hitbox_.left, handler_->hitbox_.top);
    pauseIcon1_->setFillColor(color);
    pauseIcon2_ = std::make_shared<ClonableDraw<sf::RectangleShape>>(sf::Vector2f(handler_->hitbox_.width / 3, handler_->hitbox_.height));
    pauseIcon2_->setFillColor(color);
    pauseIcon2_->setPosition(handler_->hitbox_.left + 2*pauseIcon1_->getSize().x, handler_->hitbox_.top);
}

void PauseButtonIcon::update()
{
    if (handler_->bPaused_ && graphicalObjects_.size() != 1)
    {
        graphicalObjects_.clear();
        graphicalObjects_.push_back(playIcon_);
    }
    else if (!handler_->bPaused_ && graphicalObjects_.size() != 2)
    {
        graphicalObjects_.clear();
        graphicalObjects_.push_back(pauseIcon1_);
        graphicalObjects_.push_back(pauseIcon2_);
    }
}

SimpleGraphical.h

/* SimpleGraphical can be used when you don't need a very complicated graphical handler */


#include "GraphicalHandler.h"
#include "PositionHandler.h"

template<typename T>
class SimpleGraphical : public GraphicalHandler
{
public:
    template<typename... Args>
    SimpleGraphical(Args... args)
        : graphical_(std::make_shared<ClonableDraw<T>>(args...))
    {

        graphicalObjects_.push_back(graphical_);
    }
    
    void setVisible(bool bVisible) { bVisible_ = bVisible; }

    void setHandler(std::shared_ptr<PositionHandler> handler) { handler_ = handler; }

    std::shared_ptr<ClonableDraw<T>> get() { return graphical_; }

    void update() override 
    {
        if (handler_ != nullptr)
        {
            graphical_->setPosition(handler_->getPosition());
            graphical_->setRotation(handler_->getRotation());
        }
    }
private:
    std::shared_ptr<ClonableDraw<T>> graphical_;
    std::shared_ptr<PositionHandler> handler_;
};

GameEngine.h

#pragma once
#include "GraphicalHandler.h"
#include "SimpleGraphical.h"
#include "PauseButton.h"
#include "PauseButtonIcon.h"

#include "SFML/Graphics.hpp"
#include <memory>
class GameEngine
{
public:
    GameEngine
    (
    std::string&& gameTitle = "New Window", 
    int windowWidth = 800, 
    int windowHeight = 800, 
    sf::Color backgroundColor = sf::Color(sf::Color::White)
    );

    void run();
private:
    bool bPaused_;

    std::unique_ptr<sf::RenderWindow> window_;

    std::shared_ptr<PauseButton> pauseButton_;
    std::unique_ptr<PauseButtonIcon> pauseButtonIcon_;
    std::unique_ptr<SimpleGraphical<sf::RectangleShape>> pauseOverlay_;

    sf::Color windowBackgroundColor_;
};

GameEngine.cpp

#include "GameEngine.h"
GameEngine::GameEngine(std::string&& title, int width, int height, sf::Color backgroundColor)
    : bPaused_(false), windowBackgroundColor_(backgroundColor)
{
    window_ = std::make_unique<sf::RenderWindow>(sf::VideoMode(width, height), title);
    pauseButton_ = std::make_shared<PauseButton>(sf::Vector2f(50.f, 50.f), sf::Vector2f(float(width - 60) , 10.f));
    pauseButtonIcon_ = std::make_unique<PauseButtonIcon>(pauseButton_, sf::Color::White);

    pauseOverlay_ = std::make_unique<SimpleGraphical<sf::RectangleShape>>(sf::Vector2f((float)width, (float)height));
    pauseOverlay_->get()->setFillColor(sf::Color(0, 0, 0, 170));
    pauseOverlay_->setVisible(false);
}

void GameEngine::run()
{
    pauseButton_->moveMyInstanceToLast();
    pauseOverlay_->moveMyInstanceToLast();
    pauseButtonIcon_->moveMyInstanceToLast();
    while (window_->isOpen())
    {
        sf::Event event;
        while (window_->pollEvent(event))
        {
            if (event.type == sf::Event::Closed)
            {
                window_->close();
                break;
            }
            if (!bPaused_)
            {
                LogicHandler::handleEvent(event, *window_);
            }
            else
            {
                pauseButton_->receiveEvent(event, *window_);
                pauseButtonIcon_->update();
            }
        }
        if (!bPaused_)
        {

            LogicHandler::updateLogic();

            GraphicalHandler::updateGraphicals();
        }
        bPaused_ = pauseButton_->bPaused_;
        pauseOverlay_->setVisible(bPaused_);


        window_->clear(windowBackgroundColor_);
        GraphicalHandler::renderObjects(*window_);
        window_->display();
    }
}

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

ControllablePlayer.h

#pragma once
#include "PositionHandler.h"
class ControllablePlayer : public PositionHandler
{
public:
    bool receiveEvent(const sf::Event& event, const sf::RenderWindow& window) override;
    void update() override;
};

ControllablePlayer.cpp

#include "ControllablePlayer.h"
bool ControllablePlayer::receiveEvent(const sf::Event& event, const sf::RenderWindow& window)
{
    if (event.type == sf::Event::KeyPressed)
    {
        switch (event.key.code)
        {
        case sf::Keyboard::Left:
        case sf::Keyboard::A:
            position_.x -= 20.f;
            return true;
        case sf::Keyboard::Right:
        case sf::Keyboard::D:
            position_.x += 20.f;
            return true;
        case sf::Keyboard::Down:
        case sf::Keyboard::S:
            position_.y += 20.f;
            return true;
        case sf::Keyboard::Up:
        case sf::Keyboard::W:
            position_.y -= 20.f;
            return true;
        }
    }
    return false;
}

void ControllablePlayer::update()
{
}

main.cpp

#include "GameEngine.h"
#include "ControllablePlayer.h"

#include <SFML/Graphics.hpp>

#include <memory>

int main()
{
    GameEngine engine{"New", 800, 800, sf::Color::Red};

    auto rectHandler = std::make_shared<ControllablePlayer>();
    SimpleGraphical<sf::RectangleShape> rect(sf::Vector2f(100.f, 100.f));

    rect.get()->setFillColor(sf::Color::Green);
    rect.setHandler(rectHandler);


    engine.run();
    return 0;
}

2 ответа
2

Идеальная переадресация

template<typename... Args>
ClonableDraw(Args&... args): T(args...) {}

template<typename... Args>
ClonableDraw(Args&&... args): T(args...) {}

Здесь нам нужен только один конструктор, и мы должны использовать std::forward:

template<class... Args>
CloneableDraw(Args&&... args): T(std::forward<Args>(args)...) { }

(Аналогично SimpleGraphical конструктор.)


Неявные преобразования

operator sf::Drawable& () override { return *this; }

Мы должны избегать неявных преобразований и можем сделать это, сделав операторы преобразования явными:

explicit operator sf::Drawable& () override { return *this; }

Если T является sf::Drawable, то этот оператор нам вообще не нужен, так что, наверное, стоит просто удалить эту функцию !?


Ненужное выделение

Много лишнего std::shared_ptr а также std::unique_ptr Применение.

  • shared_ptr дает нам совместная собственность объекта. Если нам это не нужно (и мы не используем weak_ptr), то нам не нужно shared_ptr.

  • unique_ptr дает нам единоличное владение и указатель на объект, который будет действителен в течение всего времени существования этого объекта. Однако это также включает в себя выделение кучи, что не всегда необходимо.

    Если мы хотим убедиться, что указатель на объект действителен в течение всего времени существования объекта, нам нужно избегать изменения местоположения объекта в памяти. Мы можем сделать это, сделав объект недоступным для копирования и перемещения.

C ++ не помогает нам обеспечить правильное время жизни и использование указателей, поэтому это может быть немного страшно, но нет ничего плохого в хранении необработанных указателей на объекты. Нам просто нужно вручную убедиться, что указатель уничтожен до объекта.

С использованием std::shared_ptr ведь все может показаться безопаснее, но это значительно усложняет время жизни объектов (что в C ++ делает вещи намного менее безопасными!).


Глобальные переменные и доступ

Кажется плохой идеей, чтобы каждый объект имел доступ ко всем другим объектам через s_allInstances_.

Точно так же каждый графический объект может обращаться к любому другому объекту и влиять на него через graphicalObjects_.

Даже moveMyInstance* функции потенциально небезопасны и могут привести к большому «оттоку», если два объекта не согласятся, как они должны быть упорядочены.

Точно так же кнопка паузы сама решает очистить весь набор graphicalObjects_.

Возможно, мы могли бы удалить InstanceTracker класс, и просто иметь GameEngine содержат два вектора: std::vector<LogicHandler*> logicHandlers_; а также std::vector<GrahpicalHandler*> graphicalHandlers_;.

Движок мог сортировать эти объекты по мере необходимости.


Глубокие иерархии наследования

Полиморфизм времени выполнения с наследованием в C ++ дает нам возможность ссылаться на объекты разных типов с одним и тем же интерфейсом (т.е. хранить указатели на них в одном контейнере и вызывать на них функции, как если бы они были одного типа).

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

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

Смешивание наследования и static функции (например, в GraphicalHandler и его потомки) очень сбивает с толку.


Предложения

Более “нормальный” способ сделать это, вероятно, был бы …

Определите базовый класс для каждого «интерфейса»:

class ILogicHandler
{
public:
    
    virtual ~ILogicHandler() { }

    ILogicHandler(ILogicHandler const&) = delete; // no copy
    ILogicHandler& operator=(ILogicHandler const&) = delete; // no copy
    ILogicHandler(ILogicHandler&&) = delete; // no move
    ILogicHandler& operator=(ILogicHandler&&) = delete; // no move

    virtual void update() = 0;
    virtual bool receiveEvent(const sf::Event& event, const sf::RenderWindow& window) = 0;
};

class IGraphicsHandler
{
public:
    
    virtual ~IGraphicsHandler() { }

    IGraphicsHandler(IGraphicsHandler const&) = delete; // no copy
    IGraphicsHandler& operator=(IGraphicsHandler const&) = delete; // no copy
    IGraphicsHandler(IGraphicsHandler&&) = delete; // no move
    IGraphicsHandler& operator=(IGraphicsHandler&&) = delete; // no move
    
    virtual void render(const sf::RenderWindow& window) = 0;
};

Тогда мы можем определить System класс для каждого, который содержит вектор указателей, и держать эти системы в GameEngine:

class LogicSystem
{
public:
    
    void addHandler(ILogicHandler* handler);
    void removeHandler(ILogicHandler* handler);
    
    void update(); // calls update on all handlers
    void receiveEvent(const sf::Event& event, const sf::RenderWindow& window); // calls receive on all handlers

private:

    std::vector<ILogicHandler*> logicHandlers_;
};

class GraphicsSystem
{
public:

    void addHandler(IGraphicsHandler* handler);
    void removeHandler(IGraphicsHandler* handler);

    void render(const sf::RenderWindow& window); // renders all handlers
    
private:
    
    std::vector<IGraphicsHandler*> graphicsHandlers_;
};

class GameEngine
{
public:
    ...
    
    LogicSystem logicSystem_;
    GraphicsSystem graphicsSystem_;
};

Затем мы можем создать объект, например:

class PauseButton : public ILogicHandler, public IGraphicsHandler
{
public:
    
    void update() override;
    bool receiveEvent(const sf::Event& event, const sf::RenderWindow& window) override;
    void render(const sf::RenderWindow& window) override;
    
private:
    
    bool paused_;
    sf::Rect<float> hitbox_;
    
    sf::VertexArray playIcon_;
    sf::RectangleShape pauseIcon1_;
    sf::RectangleShape pauseIcon2_;
};

Конечно, тогда нам нужно не забыть зарегистрировать объект и (что еще труднее) отменить регистрацию объекта в системах:

PauseButton pauseButton_;
logicSystem_.addHandler(&pauseButton_);
graphicsSystem_.addHandler(&pauseButton_);

...

logicSystem.removeHandler(&pauseButton_);
graphicsSystem.removeHandler(&pauseButton_);

Что неудобно. Мы можем сделать это немного проще и безопаснее, изменив базовые классы на автоматическое выполнение:

class ILogicHandler
{
public:

    explicit ILogicHandler(LogicSystem& system):
        system_(system)
    {
        system_.addHandler(this);
    }

    ILogicHandler(ILogicHandler const&) = delete; // no copy
    ILogicHandler& operator=(ILogicHandler const&) = delete; // no copy
    ILogicHandler(ILogicHandler&&) = delete; // no move
    ILogicHandler& operator=(ILogicHandler&&) = delete; // no move
    
    virtual ~ILogicHandler()
    {
        system_.removeHandler(this);
    }
    
    virtual void update() = 0;
    virtual bool receiveEvent(const sf::Event& event, const sf::RenderWindow& window) = 0;
    
private:
    
    LogicSystem& system_;
};

Затем добавьте конструктор в PauseButton следующим образом:

    PauseButton(LogicSystem& logicSystem, GraphicsSystem& graphicsSystem):
        ILogicHandler(logicSystem),
        IGraphicsHandler(graphicsSystem)
        { }

Простой доступный для рисования объект может выглядеть примерно так:

template<class T>
SimpleDrawable : public IGraphicsHandler
{
    template<class... Args>
    explicit SimpleDrawable(GraphicsSystem& graphicsSystem, Args&&... args):
        IGraphicsHandler(graphicsSystem),
        visible_(true),
        drawable_(std::forward<Args>(args)...) { }
        
    void render(const sf::RenderWindow& window) override
    {
        if (visible_)
            window.draw(drawable);
    }
    
    bool visible_;
    T drawable_;
};

Хотя неудобно передавать системы каждому объекту, который в них нуждается, преимущество состоит в том, что зависимости явны и очевидны. Если бы мы действительно хотели, мы могли бы повернуть LogicSystem а также GraphicsSystem в Синглтон классы или используйте ServiceLocator шаблон.

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

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

    Сигналы и слоты

    По моему опыту, они облегчают жизнь (я думаю, это причина, по которой Qt даже добавил для них специальный синтаксис и ключевые слова). Сегодня это отличные автономные библиотеки, такие как SigSlot.

    В вашем случае использование сигналов позволит удалить virtual void buttonPressed() = 0; и замените его переменной-членом sigslot::signal<> clickedSignal;. Тогда любой, кто интересуется нажатием кнопки, просто pButton->clickedSignal.connect(&CAnyone::OnClick, &anyoneInstance);. Сигналы – это мощный инструмент, который позволяет разделять вещи и снижает потребность в наследовании.

    Сочетание обработки ввода и графики

    Qt просто имеет QWidget (один из основных классов в библиотеке) с многочисленными виртуальными методами, такими как

    virtual void    hideEvent(QHideEvent *event)
    virtual void    keyPressEvent(QKeyEvent *event)
    virtual void    keyReleaseEvent(QKeyEvent *event)
    virtual void    leaveEvent(QEvent *event)
    virtual void    mousePressEvent(QMouseEvent *event)
    virtual void    moveEvent(QMoveEvent *event)
    virtual void    paintEvent(QPaintEvent *event)
    virtual void    resizeEvent(QResizeEvent *event)
    virtual void    showEvent(QShowEvent *event)
    

    Как вы могли заметить, в этом случае обработка ввода и графика смешиваются сложнее. Я уверен, что у такого подхода есть свои недостатки, но мне он кажется намного удобнее. Если мне нужна кнопка, я просто наследую от QWidget и переопределение mousePressEvent а также paintEvent без необходимости создавать 2 класса, которые тесно связаны друг с другом и даже являются друзьями.

    Включение логики рисования в класс Painter

    Qt имеет QPainter с методами как

    void    drawArc(const QRect &rectangle, int startAngle, int spanAngle)
    void    drawEllipse(const QRectF &rectangle)
    void    drawGlyphRun(const QPointF &position, const QGlyphRun &glyphs)
    void    drawImage(const QRectF &target, const QImage &image, const QRectF &source, Qt::ImageConversionFlags flags = Qt::AutoColor)
    void    drawLine(const QLineF &line)
    void    drawLines(const QVector<QPoint> &pointPairs)
    void    drawPath(const QPainterPath &path)
    void    drawPicture(const QPointF &point, const QPicture &picture)
    
    void    setBrush(Qt::BrushStyle style)
    void    setPen(Qt::PenStyle style)
    

    так что в случае кнопки

    void CButton::paintEvent(QPaintEvent *)
    {
        QPainter painter(this);
        painter.setPen(Qt::black);
        painter.setFont(QFont("Arial", 30));
        painter.drawText(rect(), Qt::AlignCenter, "click me");
        painter.setBruch(QBrush(Qt::yellow));
        painter.drawRect(rect());
    }
    

    Я предлагаю использовать аналогичный подход, потому что он скрывает все низкоуровневые детали дроу и предоставляет понятный API.

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

    Форвардная декларация

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

    #pragma once
    #include "SimpleGraphical.h"
    
    #include "SFML/Color.hpp"
    #include <memory>
    
    class PauseButton;
    class PauseButtonIcon;
    
    namespace sf
    {
        class RenderWindow; //that's how to forward declare stuff in namespaces
        class RectangleShape;
    }
    
    class GameEngine
    {
    public:
        GameEngine
        (
        std::string&& gameTitle = "New Window", 
        int windowWidth = 800, 
        int windowHeight = 800, 
        sf::Color backgroundColor = sf::Color(sf::Color::White)
        );
    
        ~GameEngine(); //for forward declaration to work for unique pointer you need to have destructor in .cpp file
    
        void run();
    private:
        bool bPaused_;
    
        std::unique_ptr<sf::RenderWindow> window_;
    
        std::shared_ptr<PauseButton> pauseButton_;
        std::unique_ptr<PauseButtonIcon> pauseButtonIcon_;
        std::unique_ptr<SimpleGraphical<sf::RectangleShape>> pauseOverlay_;
    
        sf::Color windowBackgroundColor_;
    };
    

    Мелочи

    • Это спорно, но я бы предложил использовать макрос как FORWARD_CLASS. Чтобы
    std::weak_ptr<PauseButton> m_wpButton;
    std::unique_ptr<PauseButton> m_upButton;
    std::shared_ptr<PauseButton> m_spButton;
    

    Становится

    FORWARD_CLASS(PauseButton);
    PauseButtonWPtr m_wpButton;
    PauseButtonUPtr m_upButton;
    PauseButtonSPtr m_spButton;
    

    Надеюсь понятно, как FORWARD_CLASS может быть реализовано с использованием некоторой макро-магии / уродства. Такой подход делает код немного чище (особенно когда вы передаете poiters) и обрабатывает предварительное объявление.

    • Передача интеллектуальных указателей по константной ссылке

      Добавить комментарий

      Ваш адрес email не будет опубликован. Обязательные поля помечены *