2
\$\begingroup\$

I want to create a very basic physics engine in SFML. I also want to be able to control objects using Dear ImGui. So I created this basic foundation. My main problem with it is how I handle rendering the GUI, because I couldn't find another way to implement it, either just drawing an object, or drawing object and a GUI. Mainly because the draw function must be const.

This is supposed to be able to handle both objects that have a GUI (IEntityWithGui) and those that don't (IEntity). But in this example there's just objects with a GUI.

main.cpp:

#include <iostream>
#include <SFML/Graphics.hpp>
#include "imgui/imgui.h"
#include "imgui/imgui-SFML.h"
#include "updater.h"
#include "renderer.h"
#include "shapeObj.h"

int main() {

    sf::RenderWindow window(sf::VideoMode(200, 200), "PhysicsBox");
    ImGui::SFML::Init(window);
    sf::Clock clock;
    std::atomic<bool> isRunning{ true };

    ShapeObj shape{window};

    Renderer render{ &window,isRunning };
    render.queueSubmit(&shape);

    Updater update{ &window,isRunning,std::ref(clock) };
    update.queueSubmit(&shape);

    window.setActive(false);
    window.setKeyRepeatEnabled(false);

    update.startThread();
    render.startThread();

    while (window.isOpen())
    {
        sf::Event event;
        while (window.pollEvent(event))
        {
            ImGui::SFML::ProcessEvent(event);
            if (event.type == sf::Event::Closed) {
                isRunning.store(false);
                render.joinThread();
                update.joinThread();
                window.setActive(true);
                window.close();
            }
            if (event.type == sf::Event::Resized)
            {
                // update the view to the new size of the window
                sf::FloatRect visibleArea(0.f, 0.f, event.size.width, event.size.height);
                window.setView(sf::View(visibleArea));
            }
            shape.eventProcess(event, clock.restart());

        }


    }
    ImGui::SFML::Shutdown();

    return 0;
}

IEntity.h:

#pragma once
#include <SFML/Graphics.hpp>
class IEntity : public sf::Drawable, public sf::Transformable
{
public:
    virtual void update(sf::Time dt)=0;
    virtual void eventProcess(sf::Event event,sf::Time dt)=0;
    virtual ~IEntity(){}
};

IEntityWithGui:

#pragma once
#include "IEntity.h"
class IEntityWithGui : public IEntity
{
public:
    virtual void processGui()=0; 
    virtual ~IEntityWithGui() {}
};

shapeObj.hpp:

#pragma once
#include "IEntityWithGui.h"

class ShapeObj : public IEntityWithGui
{
public:
    ShapeObj(sf::RenderWindow &window);

    virtual void draw(sf::RenderTarget& target, sf::RenderStates states) const override;
    virtual void update(sf::Time deltaTime) override ;
    virtual void processGui() override;
    virtual void eventProcess(sf::Event event, sf::Time time) override;
    virtual ~ShapeObj()=default;
private:
    sf::CircleShape m_shape{ 100.f,75 };
    sf::RenderWindow& m_window;
    bool m_show_gui = false;
    bool m_mouse_captured = false;
};

ShapeObj.cpp:

#include "shapeObj.h"
#include "imgui/imgui.h"
#include "imgui/imgui-SFML.h"

ShapeObj::ShapeObj(sf::RenderWindow& window)
    : m_window(window) {};

void ShapeObj::draw(sf::RenderTarget& target, sf::RenderStates states) const
{
    states.transform *= getTransform();
    target.draw(m_shape, states);
}

void ShapeObj::update(sf::Time deltaTime)
{
    //It will be used, just not now. 
}

void ShapeObj::processGui()
{
    if (!m_show_gui) return;
    ImGuiIO io = ImGui::GetIO();
    ImGui::SFML::Update(m_window, sf::seconds(1.f / 60.f));
    ImGui::Begin("Grid");
    if (io.WantCaptureMouse) {
        m_mouse_captured = true;
    }
    else {
        m_mouse_captured = false;
    }
    ImGui::Text("Mouse captured=%d\n",
        m_mouse_captured);
    ImGui::End();
    ImGui::SFML::Render(m_window);
}

void ShapeObj::eventProcess(sf::Event event, sf::Time time)
{
    auto mousePos= m_window.mapPixelToCoords(sf::Mouse::getPosition(m_window));

    if (!m_mouse_captured) {
        if(event.type==sf::Event::MouseButtonPressed &&
           m_shape.getGlobalBounds().contains(mousePos))
        {
            m_show_gui = !m_show_gui;
        }
        //Whatever other events
    }
}

threadWithQueue.h:

#pragma once
#pragma once
#include <SFML/Graphics.hpp>
#include <vector>
#include <thread>

template <class T>
class ThreadWithQueue {
public:
    ThreadWithQueue<T>(sf::RenderWindow* window, std::atomic<bool>& isRunning);
    void startThread();
    void joinThread();
    virtual void queueSubmit(T object);

protected:

    virtual void queueThread()=0;
    std::atomic<bool>& isRunning;
    sf::RenderWindow* m_window;
    std::thread mThreadObj;
    std::vector<T> m_queue;
};

template <class T>
ThreadWithQueue<T>::ThreadWithQueue(sf::RenderWindow* window, std::atomic<bool>& isRunning)
    : m_window{ window }, isRunning{ isRunning } {}


template <class T>
void ThreadWithQueue<T>::startThread() {
    mThreadObj = std::thread{ &ThreadWithQueue::queueThread,this };
}
template <class T>
void ThreadWithQueue<T>::joinThread() {
    if (mThreadObj.joinable()) {
        mThreadObj.join();
    }
}
template <class T>
void ThreadWithQueue<T>::queueSubmit(T object) {
    m_queue.push_back(object);
}

updater.h:

#pragma once
#include "threadWithQueue.h"
#include "IEntity.h"


class Updater : public ThreadWithQueue<IEntity*> {
public:
    Updater(sf::RenderWindow* window, std::atomic<bool>& isRunning, sf::Clock& global_clock);
    ~Updater() = default;
private:

    virtual void queueThread();
    sf::Clock& m_global_clock;
};

updater.cpp

#include "updater.h" 
Updater::Updater(sf::RenderWindow* window, std::atomic<bool>& isRunning, sf::Clock& global_clock)
    : ThreadWithQueue<IEntity*>(window, isRunning), m_global_clock(global_clock) {}

void Updater::queueThread() {
    while (isRunning.load()) {
        for (auto& i : m_queue) {
            i->update(m_global_clock.restart());
        }
    }
}

renderer.h:

#include "updater.h" 
Updater::Updater(sf::RenderWindow* window, std::atomic<bool>& isRunning, sf::Clock& global_clock)
    : ThreadWithQueue<IEntity*>(window, isRunning), m_global_clock(global_clock) {}

void Updater::queueThread() {
    while (isRunning.load()) {
        for (auto& i : m_queue) {
            i->update(m_global_clock.restart());
        }
    }
}

renderer.cpp:

#include <SFML/Graphics.hpp>
#include <atomic>
#include <thread>
#include <vector>
#include <iostream>
#include "renderer.h"
#include "imgui/imgui.h"
#include "imgui/imgui-SFML.h"
Renderer::Renderer(sf::RenderWindow* window, std::atomic<bool>& isRunning)
    : ThreadWithQueue(window,isRunning) {}


void Renderer::queueThread() {
    m_window->setActive(true);
    while (isRunning.load()) {
        m_window->clear();
        for (auto& i: m_queue) {
            m_window->draw(*i);

        }
        for (auto& i : m_queue_gui) {
            m_window->draw(*i);
            i->processGui();
        }


        m_window->display();
    }
    
}

void Renderer::queueSubmit(IEntityWithGui* object)
{
    m_queue_gui.push_back(object);
}
\$\endgroup\$
1
  • \$\begingroup\$ It looks like you forgot to include renderer.h, and instead pasted a duplicate of updater.cpp. \$\endgroup\$
    – G. Sliepen
    Commented Dec 9, 2023 at 13:28

1 Answer 1

4
\$\begingroup\$

Since you are using C++20, consider using std::jthread instead of std::thread; it will remove the code for threadWithQueue. std::jthread automatically joins as part of its destruction. You can also get rid of the atomic variable(isRunning), using jthread::request_stop method. Your m_queue is totally decoupled from the threading logic, so you may even totally drop the threadWithQueue and provide m_queue as a parameter to your thread functions. Another important issue is using m_window in 3 different threads without proper synchronization. Adding mutexes can degrade your GUI performance and not synchronizing the threads just results in UB and surprising outputs. You need a lock-free mechanism in the rendering(soft realtime) thread to ensure smooth graphical experience. On the other hand, your event loop must actually initiate update actions in the updater thread. So you need another synchronization system(preferably lock-free) between event loop and updater thread. In short that means at least two atomic flags or other synchronization objects. I don't delve into detail now, because of the number of design decisions involved in determining them.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.