Welcome, Guest. Please login or register. Did you miss your activation email?

Author Topic: Callback based logic?  (Read 6304 times)

0 Members and 1 Guest are viewing this topic.

korczurekk

  • Full Member
  • ***
  • Posts: 150
    • View Profile
    • Email
Callback based logic?
« on: December 27, 2016, 11:29:22 am »
So I've been bored for a while and thought about callback based system. SFML already is (partially) event–based, so It's basically the same thing, but keeps main loop a lot shorter and cleaner. What do you think? I'll use it in my next project tell you about results.

Code:
// CallbackContext.hpp

#ifndef CALLBACKCONTEXT_HPP
#define CALLBACKCONTEXT_HPP

#include <SFML/Config.hpp>

namespace sfcb {
        /*
         * Callback context specifies set of callbacks that are called in given situation,
         * that lets programmer add a few callbacks to one event and chose which one should be called
         */


        class CallbackContext {
                const sf::Int32 m_UID;

        public:
                CallbackContext(const sf::Int32 UID)
                        : m_UID { UID }
                { }

                sf::Int32 uid() const {
                        return this->m_UID;
                }
        };
}

#endif // CALLBACKCONTEXT_HPP
 

// CallbackWindow.hpp

#ifndef SFMLCALLBACKS_H
#define SFMLCALLBACKS_H

#include <SFML/Window/Window.hpp>
#include <SFML/Window/Event.hpp>
#include <functional>
#include <exception>
#include <atomic>
#include <vector>
#include <map>

#include "CallbackContext.hpp"

namespace sfcb {
        template<typename window_t = sf::Window>
        class CallbackWindow
                : public window_t
        {
        private:
                std::map<std::pair<sf::Event::EventType, size_t>, std::function<void(CallbackWindow<window_t>&, sf::Event)>> callbacks;
                static std::atomic_int_fast32_t contextCount;
                sf::Int32 currentContext = -2;

        protected:
                bool pollEvent(sf::Event& ev) {
                        return window_t::pollEvent(ev);
                }

                void useCallbacks() {
                        for(sf::Event ev; this->pollEvent(ev);) {
                                auto key = std::pair<sf::Event::EventType, sf::Int32>(ev.type, this->currentContext);
                                if(callbacks.find(key) != callbacks.end()) {
                                        callbacks[key](*this, ev);
                                }

                                key = std::pair<sf::Event::EventType, sf::Int32>(ev.type, -1);
                                if(callbacks.find(key) != callbacks.end()) {
                                        callbacks[key](*this, ev);
                                }
                        }
                }

        public:
                using window_t::window_t;

                static std::function<void(CallbackWindow<window_t>&, sf::Event)> getEmptyCallback() {
                        return [](window_t&, sf::Event){ };
                }

                CallbackContext createCallbackContext() {
                        return CallbackContext(++contextCount);
                }

                CallbackContext getUniversalCallbackContext() {
                        return CallbackContext(-1);
                }

                void setCurrentContext(CallbackContext c) {
                        if(c.uid() == -1)
                                throw std::invalid_argument("Can't set current callback context to universal one");

                        this->currentContext = c.uid();
                }

                void setCallback(sf::Event::EventType type, CallbackContext context, std::function<void(CallbackWindow<window_t>&, sf::Event)> func) {
                        const auto key = std::pair<sf::Event::EventType, sf::Int32>(type, context.uid());
                        callbacks[key] = func;
                }

                void display() {
                        useCallbacks();
                        window_t::display();
                }
        };

        template<typename T>
        std::atomic_int_fast32_t CallbackWindow<T>::contextCount { 0 };
}

#endif // SFMLCALLBACKS_H
 

// main.cpp

#include "CallbackWindow.hpp"

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

/* Namespace with callbacks */
namespace callbacks {
        void onClose(sf::RenderWindow& window, sf::Event) {
                std::cout << "Y u do dis? ;-;" << std::endl;
                window.close();
        }

        void onMouseMoved1(sf::RenderWindow& window, sf::Event ev) {
                window.clear({
                        static_cast<sf::Uint8>(double(ev.mouseMove.x) / 300. * 235. + 20),
                        20,
                        20});
        }

        void onMouseMoved2(sf::RenderWindow& window, sf::Event ev) {
                window.clear({
                        20,
                        static_cast<sf::Uint8>(double(ev.mouseMove.x) / 300. * 235. + 20),
                        20});
        }

        void onKeyPressed(sfcb::CallbackWindow<sf::RenderWindow>& window, sf::Event ev, sfcb::CallbackContext context1, sfcb::CallbackContext context2) {
                std::cout << "Key pressed, SFML code: " << ev.key.code << std::endl;

                if(ev.key.code == 0) { //A
                        window.setCurrentContext(context1);
                        std::cout << "Switched to callback context1" << std::endl;
                } else if(ev.key.code == 1) { //B
                        window.setCurrentContext(context2);
                        std::cout << "Switched to callback context2" << std::endl;
                }
        }
}

int main()
{
        /* Create window, acts just like window given in parameter */
        sfcb::CallbackWindow<sf::RenderWindow> app({300, 300}, "app");

        /* Get universal context and create 2 new ones */
        auto universal = app.getUniversalCallbackContext();
        auto context1 = app.createCallbackContext();
        auto context2 = app.createCallbackContext();

        /* Set current context for callbacks */
        app.setCurrentContext(context1);

        /* Connect a few callbacks to some events */
        app.setCallback(sf::Event::Closed, universal, callbacks::onClose);
        app.setCallback(sf::Event::MouseMoved, context1, callbacks::onMouseMoved1);
        app.setCallback(sf::Event::MouseMoved, context2, callbacks::onMouseMoved2);

        /* Connect callback using std::bind */
        /* Create callable std::bind object which automatically passes third and fourth arg */
        auto binded = std::bind(
                callbacks::onKeyPressed,
                std::placeholders::_1,
                std::placeholders::_2,
                context1,
                context2
        );

        /* Pass this object as normal callback */
        app.setCallback(sf::Event::KeyPressed, universal, binded);

        /* Minimal main loop */
        app.clear({20, 20, 20});
        while(app.isOpen()) {
                /* All callbacks are called right before displaying window */
                app.display();
        }

        return 0;
}
 

Laurent

  • Administrator
  • Hero Member
  • *****
  • Posts: 32498
    • View Profile
    • SFML's website
    • Email
Re: Callback based logic?
« Reply #1 on: December 27, 2016, 08:20:42 pm »
I don't think it's a good idea to process events in display(). What if you want to have drawing and events in separate threads?
Laurent Gomila - SFML developer

korczurekk

  • Full Member
  • ***
  • Posts: 150
    • View Profile
    • Email
Re: Callback based logic?
« Reply #2 on: December 27, 2016, 10:17:33 pm »
It's just a draft, I'll most likely rewrite most of it. This topic was created rather to disuss idea than simple implementation.

sjaustirni

  • Jr. Member
  • **
  • Posts: 94
    • View Profile
Re: Callback based logic?
« Reply #3 on: December 27, 2016, 10:37:06 pm »
I don't consider callbacks to really be the way to go. Have you given a thought to an idea of an event queue where all the events are passed and all the observers are notified about them whilst only the observers interested in that particular type of message handle them and the rest simply ignores them?
This is highly influenced by my hunt for the ultimate ECS, hence this may not be exactly what you're looking for. It is what I am looking for though, ergo I am giving you my 2¢.
I can't find the article right now, the reason why I see it as a better alternative is that the event queue can be easily implemented in a way where the emitters and receivers of the events are completely decoupled. There's probably a way of dealing with this with callbacks, however, as I mentioned earlier, I have never really attempted to implement a massive callback system and I don't have the insight.

eXpl0it3r

  • SFML Team
  • Hero Member
  • *****
  • Posts: 11034
    • View Profile
    • development blog
    • Email
Callback based logic?
« Reply #4 on: December 28, 2016, 02:00:17 am »
A callback system can be quite nice, but there's no real reason to modify SFML for that. Similar to what Thor does with the ActionMap.

For any larger project I'll take on maybe one day, I'd probably use a message bus, where various components can subscribe to the bus and one can simply dispatch events to the bus and let it notify all the subscribers.
Official FAQ: https://www.sfml-dev.org/faq.php
Official Discord Server: https://discord.gg/nr4X7Fh
——————————————————————
Dev Blog: https://duerrenberger.dev/blog/

korczurekk

  • Full Member
  • ***
  • Posts: 150
    • View Profile
    • Email
Re: Callback based logic?
« Reply #5 on: December 30, 2016, 03:08:14 pm »
A callback system can be quite nice, but there's no real reason to modify SFML for that.
Of course, it's just my attempt to design application in different way than usually.

For any larger project I'll take on maybe one day, I'd probably use a message bus, where various components can subscribe to the bus and one can simply dispatch events to the bus and let it notify all the subscribers.
Emiter/Listener model is quite popular, but I really liked callbacks after using node.js and I decided to check if it can work nicely with SFML.

I redesigned original code a bit and added callback-based UDP socket, here's repo on GitHub with my current code.

//edit
There's probably a way of dealing with this with callbacks, however, as I mentioned earlier, I have never really attempted to implement a massive callback system and I don't have the insight.
Neither have I, that's why I wrote this. ;D
« Last Edit: December 30, 2016, 03:09:58 pm by korczurekk »

JayhawkZombie

  • Jr. Member
  • **
  • Posts: 76
    • View Profile
Re: Callback based logic?
« Reply #6 on: January 02, 2017, 08:46:45 am »
If I may provide some insight.
A callback system can be extremely useful.  We use it quite a lot in our engine.

If you curious about how we did it:

Each class holds a std::function object, with the function signature matching the callback.
For example, our UI class has a callback we've called
OnMouseRelease
Internally there is
class UIObject {
  public:
      ...
      std::function<void(const sf::Vector2f &, const sf::Mouse::Button &b)> MouseReleaseCallback; //invoke custom behavior
  protected:
    virtual void Internal_HandleMouseRelease(); //do internal code here - ie state management, etc
}
 

We'll use the Internal method to do state management, then invoke the bound function pointer (if it is valid).  In our code, we only allow the main event handling class to directly invoke the internal method, but how you invoke your stuff would be totally up to you. 

We've done things like:
Button->OnMouseRelease = [MainLevel](const sf::Vector2i &pos, const sf::Mouse::Button &b)
{
  MainLevel->LoadLevel("./SFEngine/Samples/Levels/Graveyard/Graveyard.xml");
};
 

We have callbacks for MousePressed, MouseReleased, MouseOver (mousing over an element), MouseExit (mouse exiting an element), MouseMove (mouse moving while inside an element's bounds), FocusGained (like clicking on text input and then being able to enter text), and FocusLost (like clicking outside said text input and no longer being able to enter text), KeyPressed, KeyReleased, and TextEntered (TextEntered only being invoked if the key repeat is enabled).

For the state management, we'll do things like:
void Button::Internal_HandleMousePress(const sf::Vector2f &pos, const sf::Mouse::Button &b)
{
  ButtonRect.setFillColor(ButtonPressedColor);
  //etc, change our appearance
  State |= STATE_PRESSED;
  ///...

  //invoke custom bound behavior
  if (OnMousePress)
    OnMousePress(pos, b);
}

void Button::Internal_HandleMouseRelease(const sf::Vector2f &pos, const sf::Mouse::Button &b)
{
  ButtonRect.setFillColor(ButtonColorNormal);
  State &= ~STATE_PRESSED;
  //... change appearance back to normal, etc

  //invoke custom behavior
  if (OnMouseRelease)
    OnMouseRelease(pos, b);
}
 

Our Actor class has callbacks we can assign to invoke when actors collide, when others get "killed", whenever engine things happen, etc.  If you can detect the occurrence of it, you can turn it into an "event" and initiate a callback for it.

We also use callback for resource requests.
When we want a resource, we request it and provide a callback to be called when the request is filled.
void Button::Button()
{
  Resource::Request(ButtonFontPath,
                   /*callback to call when request is filled*/
                   [this](std::shared_ptr<sf::Font> fnt)
                   {
                      this->ButtonFont = fnt;
                      this->ButtonText.setFont(*fnt);
                    });
 

We personally delegate the filtering of event data (like which key was pressed) to the callback being invoked to make storing the callbacks simpler.  For example, a button that only cared about the left button and not the right would have to check to make sure that it was the right button that was pressed - but we pass the sf::Button value of the button that was pressed, so the callback just has to check the parameter to see if they want to use it. If not, the callback just returns.

Whenever we register an object with the engine, it is able to start having callbacks invoked.  We did make it, however, that an object need not explicitly register the callback.  It is automatically able to have its callbacks invoked at creation, and they are all initially bound to functions that just immediately return (in case we forget to register one, we don't want a crash).

We have a couple classes doing event handling. There is our main event handling class, which just dispatches our main event handling function, but, for example, the UI class has a UIHandler class that will initially receive the incoming event (if enabled).  The handler will determine what kind of action was done to any UI elements and then invoke the callbacks for those UI elements.  You certainly don't have to do it that way, that's just how we're doing it.

But now we can change our callbacks at runtime if we want to, which is useful if something in the environment changes.  We can re-use UI elements and just change the callback when we switch levels.  If we want to restore a callback from before, we can save it before we switch it and restore it when done with the new one.  We even have a small number of our callbacks internally set up to invoke a script defined in a file that is read when the engine starts up.  Now we can dynamically script our callbacks (though execution will be slower).

**Note that we've made sure our architecture guarantees that a callback will not be called if that object no longer exists.  Obviously, that would be very bad.**

korczurekk

  • Full Member
  • ***
  • Posts: 150
    • View Profile
    • Email
Re: Callback based logic?
« Reply #7 on: January 02, 2017, 06:18:58 pm »
Nice!  ;D

I've decided to avoid using std::function directly, as making my own callback class cleans it up a lot.
template<typename ... callback_args_t>
class Callback {
private:
        std::function<void(callback_args_t ...)> m_func;

public:
        template<typename func_t, typename ... args_t>
        void set(func_t func, const args_t&... args) {
                m_func = [func, args ...](callback_args_t ... callback_args) {
                        func(callback_args ..., args ...);
                };
        }

        template<typename func_t, typename ... args_t>
        Callback(func_t func, const args_t&... args) {
                this->set(func, args ...);
        }

        Callback()
        {
                m_func = [](callback_args_t ...) { };
        }

        inline void operator()(const callback_args_t ... callback_args) {
                this->m_func(callback_args ...);
        }
};
Then it's nicer to declare etc.:
void callback(int param, int additional_param, std::ostream& out) {
        out << param + additional_param << std::endl;
}

int main()
{
        Callback<int> c(callback, 5, std::ref(std::cout));

        c(2); //7
        c(6); //11

        return 0;
}

And I also realised that callback–based network system is a LOT cleaner and doesn't require anything like sf::SocketSelector.
As I said, my code is on GitHub, so you can check it out, if you're interested.

Btw I have a question about your code:
void Button::Button()
{
  Resource::Request(ButtonFontPath,
                   /*callback to call when request is filled*/
                   [this](std::shared_ptr<sf::Font> fnt)
                   {
                      this->ButtonFont = fnt;
                      this->ButtonText.setFont(*fnt);
                    });
How do you know that Button instance still exists when callback is being called?

JayhawkZombie

  • Jr. Member
  • **
  • Posts: 76
    • View Profile
Re: Callback based logic?
« Reply #8 on: January 02, 2017, 08:35:50 pm »
The boilerplate code always looks worse, but makes the code outside look nicer.  We opted for our method partially because we like using lambdas.

I did check out some of your code, but I'll have to look at it in more detail.  I'm intrigued by your use of variadic templates and lambda functions.

Our system can notify others when they get destroyed/are about to get destroyed, so references to the objects can be removed from anything that might be referring to it.

I guess the code I gave isn't totally complete :P It was just more of a general idea of how we do it. I should know better than to do that.
Memory for us is managed by std::shared_ptr and those are captured in most of the capture lists
Button->OnMouseRelease = [Button, MainLevel](const sf::Vector2i &pos, const sf::Mouse::Button &b)
{
  if (Button) //have to force use
    MainLevel->LoadLevel("./SFEngine/Samples/Levels/Graveyard/Graveyard.xml");
};
 
But it should always be callable regardless, since the class containing it manages the lifetime of the objects.  The event that would cause OnMouseRelease to be called can only be triggered on that button as long as it exists (otherwise collision detection would fail since no button would exist there).  An object can only be deleted by going through said class, and then it will no longer attempt to call any of its callbacks.

For things like that resource request, it's not as easy as we're still working on it. Generally, the request will be filled long before the object's lifetime ends.  It's not the cleanest, and that probably signifies we should probably we using a class to handle this for us.  This is the only one that is funky like this, so maybe it could be redone.
There's a way to notify that the object no longer needs the request to be filled since each request uses an ID.
It's more like
  Resource::Request(ButtonFontPath,
                    UniqueButtonID,
                    /*callback to call when request is filled*/
                    [this](std::shared_ptr<sf::Font> fnt)
                    {
                      this->ButtonFont = fnt;
                      this->ButtonText.setFont(*fnt);
                     });
 
When the object is being destroyed, it can signify that it is being destroyed and thus no longer needs the request to be filled.  The request is asynchronous, but it's done so that the request will either be fulfilled before the object cancels it OR the request is canceled, but there shouldn't ever be a race condition.

Explaining this makes me think this needs a redesign, a much cleaner way of making requests and handling possible dangling references.

korczurekk

  • Full Member
  • ***
  • Posts: 150
    • View Profile
    • Email
Re: Callback based logic?
« Reply #9 on: January 03, 2017, 05:11:34 pm »
The boilerplate code always looks worse, but makes the code outside look nicer.  We opted for our method partially because we like using lambdas.
Well, lambda expressions are great. :D My callbacks also allow using them, but in some cases it makes code a bit uglier, so sometimes I just avoid them.

I did check out some of your code, but I'll have to look at it in more detail.  I'm intrigued by your use of variadic templates and lambda functions.
I'd actually like to avoid some of those complicated templates, but there's no other good way to take unknown amount of parameters.

Our system can notify others when they get destroyed/are about to get destroyed, so references to the objects can be removed from anything that might be referring to it.
Nice, is there a chance that you'll publish that code (if it hasn't been done yet)?

I guess the code I gave isn't totally complete :P It was just more of a general idea of how we do it. I should know better than to do that.
Memory for us is managed by std::shared_ptr and those are captured in most of the capture lists
Button->OnMouseRelease = [Button, MainLevel](const sf::Vector2i &pos, const sf::Mouse::Button &b)
{
  if (Button) //have to force use
    MainLevel->LoadLevel("./SFEngine/Samples/Levels/Graveyard/Graveyard.xml");
};
But it should always be callable regardless, since the class containing it manages the lifetime of the objects.  The event that would cause OnMouseRelease to be called can only be triggered on that button as long as it exists (otherwise collision detection would fail since no button would exist there).  An object can only be deleted by going through said class, and then it will no longer attempt to call any of its callbacks.
Good to know, I was really curious. I actually need opionions of other people, as callbacks seem to be pretty good with SFML, but I only used them in JS.  :(

For things like that resource request, it's not as easy as we're still working on it. Generally, the request will be filled long before the object's lifetime ends.  It's not the cleanest, and that probably signifies we should probably we using a class to handle this for us.  This is the only one that is funky like this, so maybe it could be redone.
There's a way to notify that the object no longer needs the request to be filled since each request uses an ID.
ID–system is quite logical in this situation, I was thinking about implementing it myself.

Explaining this makes me think this needs a redesign, a much cleaner way of making requests and handling possible dangling references.
IMHO your resource manager is strange, it looks like it has one function for each resource type, am I right? If so, isn't it better to use some sf::InputStream object and use it to load data from it locally?
resources.requestStream("engine.png", [i](sf::InputStream& stream) {
        sf::Texture tex;
        tex.loadFromStream(stream);
        std::cout << "Done.\n";
});
Having one function per type is bad idea due to slow compilation and messy code.

JayhawkZombie

  • Jr. Member
  • **
  • Posts: 76
    • View Profile
Re: Callback based logic?
« Reply #10 on: January 05, 2017, 11:18:19 am »
Good to know, I was really curious. I actually need opionions of other people, as callbacks seem to be pretty good with SFML, but I only used them in JS.  :(
I got my inspiration from JS, though JS's is faaarrrrrrrr more expansive and complex.

IMHO your resource manager is strange, it looks like it has one function for each resource type, am I right? If so, isn't it better to use some sf::InputStream object and use it to load data from it locally?
resources.requestStream("engine.png", [i](sf::InputStream& stream) {
        sf::Texture tex;
        tex.loadFromStream(stream);
        std::cout << "Done.\n";
});
Having one function per type is bad idea due to slow compilation and messy code.
Yes, it is strange. Using streams would be better.  It's on the long list of things to do. I'll have to look more into using streams.
Though that will probably be less needed as time goes on. Our game objects are starting to be configured in files, and the Factory methods that create those objects handle the requests for them. It's all done in a different thread, so I could probably just load them and then hand ownership over to the manager after joining threads.