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.**