0
\$\begingroup\$

I am currently working on the UI functionality of my game, which will have very minimal GUI design, yet it needs to be functional and flexible e.g. easy to redesign.

I am planning to construct all my GUI elements (buttons, sliders, knobs, checkboxes) from Quads with an optional texture and a color. These quads will also serve as boundaries for text and mouse-click detection.

But I am having trouble to figure out an efficient way of linking actual functionality to triggers like buttons. I have thought of defining a Button like this:

struct Button{
    unsigned int quadIndex; //an index into the array of quads, which themselves store indices into arrays of positions and sizes
    void (*func)(int); //a pointer to the function to be called when this button is being interacted with. the parameter of the function will be the state of the relevant mouse button
}

But this way I could only make a button call a function void func(int), which will often force me to write a wrapper function for every individual button functionality. Is this the way everybody does it or is there a better, more flexible way?

Edit: Since performance/high responsiveness is very important to me, i would like to avoid pointers, virtual functions and derived classes. I am looking for a transparent, concrete solution which focusses on the minimal data which has to come in play for a functional and flexible solution

\$\endgroup\$
  • 5
    \$\begingroup\$ Pointers and virtual functions will not noticeably affect performance in the case of UI elements. You should build something readable, configurable, and re-usable, and focus your optimisation efforts on more cpu/memory intensive operations elsewhere in your larger codebase. \$\endgroup\$ – Ian Young Aug 16 '17 at 11:34
  • \$\begingroup\$ I think UI interaction should be highly optimised as it is the direct contact that the user has to the program and it really sets the "feel" of the application. Even though an object oriented approach might be sufficient during fluid program stages, unpleasant hangups might appear, more often in moments of high user/program interaction, which is especially frustrating with games. Of course i have no proof that these few pointer access stalls will actually be significant to the visible performance, but now that i am already at it, i would simply like to find an ideal solution. \$\endgroup\$ – stimulate Aug 16 '17 at 18:18
  • 2
    \$\begingroup\$ What I think Ian Young is getting at is that the click event is not the bottleneck in your game's responsiveness. Even if we have to dive through several layers of indirection to get to the action to execute, we only need to do that once per click. It's not code that executes in a tight loop hundreds to millions of times each frame. It's that "hot" code we do repeatedly that needs this exacting attention to performance to avoid stutters, lags, and framerate drops. A click handler will not itself cause a meaningful performance impact unless you do something very wrong. \$\endgroup\$ – DMGregory Aug 17 '17 at 22:46
  • 1
    \$\begingroup\$ @DMGregory Yes, that is exactly what I am getting at. Things like collision detection, physics etc are far better candidates for optimisation. \$\endgroup\$ – Ian Young Sep 19 '17 at 13:38
2
\$\begingroup\$

What C++ standard are you writing against? C++ 11 Introduced support for Lambdas, which are basically unnamed functions that can "capture" variables.

So you could just define a Lambda instead of your function and then write whatever function you actually want to call in there. You're still creating the function adapters, but since you can write them in-line it's much more compact:

myButton.func = [someExternalVariable](int idx){ doMagic( buttons[idx], someExternalVariable, 42 ); };
\$\endgroup\$
2
\$\begingroup\$

First off, your part about avoiding pointers and virtual functions due to responsiveness of button clicks is terribly misguided - you listened to someone you should not have listened to. If we're discussing pointers for performance, we're talking nanoseconds (worst case of 65 ns, according to one source), and if you want to react fast enough to a button click to be seen as responsive, you have 100'000'000 of these nanoseconds to spend as you please.

That said, you're either looking at virtual functions (likely in combination with std::unique_ptr), at std::function, or if you're on an old C++ version at boost::function. All of these allow you to store any number of additional arguments, and also allow correct lifetime management of resources by their destructors. Done properly, std::function with lambdas should be the cleanest code to read, but an implementation with virtual functions will be easier to debug.

\$\endgroup\$
  • \$\begingroup\$ I mainly want to avoid using pointers because I want to avoid using new. Pointers to instances created with new always have to access random memory (RAM)which causes the CPU to stall(, unless the requested instance is in a cache line). This is what I have read and that is why I store all instances of anything directly in arrays. When I process these instances I iterate over all of them once or access the arrays through indices. this could of course also be done with pointers (to local memory) but after all, indexing arrays is the same as dereferencing pointers to local memory. \$\endgroup\$ – stimulate Aug 16 '17 at 19:03
  • 5
    \$\begingroup\$ @stimulate No, these nanoseconds do not add up. You won't have more than one button press every 100 ms. And I did tell you how it could be done, in the second paragraph on my answer. I don't go into unrelated details, because they are unrelated, and you are currently getting distracted by them. Talking about unrelated details will not remove that distraction. \$\endgroup\$ – Peter - Unban Robert Harvey Aug 16 '17 at 19:17
  • 3
    \$\begingroup\$ @stimulate You're welcome. My advice was specific to buttons, resp. interactible UI elements. For other things, specifically data that is iterated over by the same code, sequentially, and in the critical path (e.g. particle effects), cache locality is absolutely something to pay attention to. But UI related stuff needs to be easy to use, easy to change, and performance simply doesn't matter at all, as long as you're fast enough (which is usually <100 ms and in some special cases <10 ms, several orders of magnitudes larger than a cache miss). \$\endgroup\$ – Peter - Unban Robert Harvey Aug 16 '17 at 19:35
  • 2
    \$\begingroup\$ What most people here mean, is that the code for executing button handler is just a fraction of the complexity of the handler itself (nanoseconds vs. hundreds of nanoseconds). Optimization of code is always great, but if the code only takes nanoseconds to start with, gains will be negligible. Think of it this way: you are optimizing pairs of handler-calling-routines (eg. 3 ns) and handlers themselves (eg. 300 ns). At best you'll speed up the combined process by 1% (1 ns + 300 ns). Your attitude of seeking optimized solutions is fantastic, but I'd keep an eye on where optimization is useful. \$\endgroup\$ – Jelle van Campen Aug 16 '17 at 19:37
  • 2
    \$\begingroup\$ @JellevanCampen To be fair, a cache miss - which is what the OP worries about - can easily hit up to 100 ns stackoverflow.com/a/29188516/1612743 But these 0.0001 milliseconds need to be looked at not relative to the total time of the button handler but relative to the time available to provide user feedback (100 ms), and relative to the CPU time available in the current frame (15 ms). Your point remains, it's merely the numbers that change. \$\endgroup\$ – Peter - Unban Robert Harvey Aug 16 '17 at 20:21
1
\$\begingroup\$

Create a ButtonBehaviour abstract class with a virtual execute(Button& b)

Then your button can have several behaviours:

class Button
{
public:
    void parseMouse(Mouse& m){
        if(isInside(m.pos)) {
            if(m.clicked) {
              m_on_click->execute(*this);
            }
            else {
                if (!m_mouse_inside) {
                    m_mouse_inside = true;
                    m_mouse_enter->execute(*this);
                }
            }
            else {
                if (m_mouse_inside) {
                   m_mouse_inside = false;
                   m_mouse_leave->execute(*this);
                }
            }
        }

    }
private:
    ButtonBehaviour* m_on_mouse_enter;
    ButtonBehaviour* m_on_mouse_leave;
    ButtonBehaviour* m_on_click;
    bool m_mouseinside;
};

The mouse then calls the appropriate one depending on the mouse state.

In such a way the ButtonBehaviour concrete classes can do whatever you want: call other, wrapped up functions, or interact with other modules in your code directly.

\$\endgroup\$
  • \$\begingroup\$ I think it's a good idea to treat the behaviour as something separate from a button, this way I can relink behaviours to different buttons. Though i still really dislike the idea of virtual functions and derived classes. I am not sure why but I think it's because it takes away from the transparency of things and i feel like it is a trade off of performance for easy usuage \$\endgroup\$ – stimulate Aug 16 '17 at 10:44
  • \$\begingroup\$ It's not just about ease of use. It's also about re-usability and flexibility. Also, the performance cost of clicking a button in a UI is negligible. Calling a wrapped function pointer via a virtual function will still happen faster than you can click the button, unless the button click triggers something cpu intensive, in which case, that is what should be optimised, not the button. \$\endgroup\$ – Ian Young Aug 16 '17 at 10:54
1
\$\begingroup\$

You could use templates and hide the function type under an interface:

class ICallback
{
public:
    virtual void Execute() = 0;
protected:
private:
}

template <typename T> ButtonCallback : public ICallback
{
public:
    ButtonCallback(T c_) : c(c_) {}

    void Execute() override
    {
        c();
    }
protected:
private:
    const T c;
}

class Button
{
public:
    Button() 
    {
        clickCallback = new ButtonCallback(/* any function pointer/std::function here. */);
    }
private:
    ICallback* clickCallback;
}

All variables to callbacks need to be of the type ICallback, which means that you need to "double" any interface you want with the callbacks there.

\$\endgroup\$
0
\$\begingroup\$

Make Button an abstract class with a virtual function click.

Then create a derived class for each button which implements click by doing what the button is supposed to do on a click-event.

\$\endgroup\$
  • \$\begingroup\$ I would like to avoid virtual functions and derived classes because i want to stick to a concrete, functional programming style. Especially in this part of my program which has to be highly responsive. \$\endgroup\$ – stimulate Aug 16 '17 at 10:00
  • 3
    \$\begingroup\$ @stimulate Why did you tag the question C++ if you want a C solution? And low-level performance considerations are unlikely to matter here. The performance overhead of calling a virtual method won't matter unless you process thousands of click events per second. \$\endgroup\$ – Philipp Aug 16 '17 at 10:53
0
\$\begingroup\$

You can turn things around by looping over your gui every frame and doing

if(button(quadIndex)){
    // handle click
}

where button(int) will look up the quad and see if the player clicked in that area. At the same time it will also schedule it to be drawn.

For checkboxes you pass a pointer to a bool that represents the state

if(checkBox(quadIndex, &selected)){
    // handle that selected changed
}

if(selected){
    //...
}

This pattern is sometimes called an immediate mode GUI.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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