Tutorial2.5 (An alternative method for user input)

|

Background

In [Tutorial2]http://keithgrant.co.uk/tutorials/Tutorial2/ I outlined how to add basic input handling to an application, in particular one using the OpenGLContext from [Tutorial1]http://keithgrant.co.uk/tutorials/Tutorial1/. This method is fine and you can pass the event that is generated down into whatever you application you create very easily, the main loop would look something like:

#!C++
while(my_app.run())
{
    // Process Inputs
    InputEvent e;
    while (test_window.popEvent(e))
    {
        my_app->processInputs(e);
    }
    my_app->update();
    my_app->draw();
}

this is fine I suppose, but it does impose some restrictions. Any place you want to interact, you have to wire the event down through your heirarchy to get to it. So I prefer to use a listener based system.

Create a ‘Handler’

To start with we are going to create a class that will handle any InputEvent that our window produces. For this tutorial we’ll just go ahead and declare it in the same file as the InputEvent itself.

#!c++
class InputEventHandler
{
public: // Internal (only used within the context, not used by the application)
    static InputEventHandler& getEventHandler();
    ~InputEventHandler() = default;
    void handleEvent(InputEvent e);

public: // External (This is the only thing an application would care about)
    void addListener(event_type e, WindowsInputDelegate listener);
    void removeListener(event_type e, WindowsInputDelegate listener);

private: // Implementation details only
    InputEventHandler() = default;
    WindowsEventMap m_windows_event_map;
};

This is made up two main sections; the first Internal section contains a default constructor and destructor as well as one method that is called to handle a specific event type and once set up will not be needed by the application developer, the second section would be the “public” api of the class. By this I mean that it would be the only section that day to day users should be interested in.

Essentially what this class does is it store a multi-map of methods to event types, this will store a list of methods(listeners) that should be called for each event type (mouse or keyboard). This will be updated by the addListener(...) and removeListener(…) methods, when each event is generated, all methods associated with that event will be triggered.

Details

As you can see the class depends on a couple of other types, namely WindowsInputDelegate and delegate_entry, so I’ll give you a quick rundown on what they are.
WindowsInputDelagate is an alias for templated type Delegate<void, InputEvent>, The delegate type is my own custom type that has similar functionality to std::function, it’s not as fully-featured (for instance it doesn’t support lambdas), but for free, static and class methods it works well. The reason I use this rather than std::function is that it does implement the == operator. This allows us to remove listeners more simply. I’m not going to go into the detail of how it works here. but I’ll include the full source for it at the end. Essentially you can assign any method with the signature void myMethod(InputEvent param) to it. This is done using various overloads of Delegate<>::create().
delegate_entry is a simple struct that stores a WindowsInputDelegate and a bool.

#!c++
using WindowsInputDelegate = Delegate<void, InputEvent>;
struct delegate_entry
{
    WindowsInputDelegate delegate;
    bool active;
};

To implement the InputEventHandler we add a new file, InputEvent.cpp and populate it as follows:

#!c++
#include "stdafx.h"
#include "InputEvent.h"
#include <utility>

InputEventHandler& InputEventHandler::getEventHandler() {
    static InputEventHandler instance;
    return instance;
}

void InputEventHandler::handleEvent(InputEvent e) {
    auto& range = m_windows_event_map.equal_range(e.m_event_type);
    for(auto& it = range.first; it != range.second; )
    {
        if(it->second.active)
        {
            it->second.delegate(e);
            ++it;
        }
        else
        {
            it = m_windows_event_map.erase(it);
        }
    }
}

void InputEventHandler::addListener(event_type e, WindowsInputDelegate listener) {
    auto entry = std::pair<event_type, delegate_entry>(e, {listener, true});
    m_windows_event_map.insert(entry);
}

void InputEventHandler::removeListener(event_type e, WindowsInputDelegate listener) {
    auto& range = m_windows_event_map.equal_range(e);
    for(auto& it = range.first; it != range.second;)
    {
        if(it->second.delegate == listener)
        {
            it->second.active = false;
        }
        ++it;
    }
}

This is fairly straightforward, basically InputEventHandler::addListener(...) and InputEventHandler::removeListener(...) are used to add/remove methods to the m_windows_event_map, and the InputEventHandler::handleEvent(InputEvent e) takes the event e and calls all listeners associated with it.

You may notice this InputEventHandler is a singleton, before anyone gets all stroppy about this, remember this is just a small example program, in a bigger system I might try to avoid this.

Also note the way that removeListener doesn’t actually remove the listener, rather it marks it inactive, this mitigates the case where one of the listeners that is registered might execute some code that would cause a seperate listener to be removed. For example a method might destroy an instance of a class, and that instance may have had one of it’s members registered as a listener, if it was removed straight away then the maps iterators would get all screwy and nobody wants that, rather we mark it as inactive so thatr next time we try and access it we see its inactive and remove it from the list then.

Update our GLWindow class

In order to use our new system we need to make some minor changes to the GLWindow class. Basically we remove the GLWindow::popEvent() method, and also the m_events member.
Then in the GLWindow::windowsProcedure method we replace the calls to m_events.push_back(e), with InputEventHandler::getEventHandler().handleEvent().

Finally in order to test this functionality we’ll replace the various methods in main.cpp so that main.cpp looks like

#!c++
// main.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include "GLWindow.h"
#include <string>

class test_app
{
public:

    test_app(
    ) {
        auto k = WindowsInputDelegate::create<test_app, &test_app::onKey>(this);
        InputEventHandler::getEventHandler().addListener(event_type::e_key_event, k);

        auto m = WindowsInputDelegate::create<test_app, &test_app::onMouse>(this);
        InputEventHandler::getEventHandler().addListener(event_type::e_mouse_event, m);
    }

    ~test_app(
    ) {

        auto k = WindowsInputDelegate::create<test_app, &test_app::onKey>(this);
        InputEventHandler::getEventHandler().removeListener(event_type::e_key_event, k);

        auto m = WindowsInputDelegate::create<test_app, &test_app::onMouse>(this);
        InputEventHandler::getEventHandler().removeListener(event_type::e_mouse_event, m);
    }

    void onKey(
       InputEvent e
    ) {
        if(e.m_key_args.key == key_code::A)
        {
            std::cout << "test_app key event" << std::endl;
        }
    }


    void onMouse(
        InputEvent e
    ) {
        std::cout << "test_app mouse event" << std::endl;
    }

    static void onStaticKey(
        InputEvent e
    ) {
        if(e.m_key_args.key == key_code::S)
        {
            std::cout << "test_app static key event" << std::endl;
        }  
    }
};



test_app* t;


void createTestApp(
    InputEvent e
) {
    if(e.m_key_args.key == key_code::O)
    {
        if(!t)
        {
            std::cout << "Creating test app" << std::endl;
            t = new test_app();
        }
    }
}

void deleteTestApp(
    InputEvent e
) {
    if(e.m_key_args.key == key_code::P)
    {
        if(t)
        {
            std::cout << "deleting test app" << std::endl;
            delete t;
            t = nullptr;
        }
    }
}

void onFreeKey(
    InputEvent e
) {
    if(e.m_key_args.key == key_code::D)
    {
        std::cout << "free function key event" << std::endl;
    }
}

int main()
{
    GLWindow test_window;

    auto s_k = WindowsInputDelegate::create<&test_app::onStaticKey>();
    InputEventHandler::getEventHandler().addListener(event_type::e_key_event, s_k);

    auto f_k = WindowsInputDelegate::create<&onFreeKey>();
    InputEventHandler::getEventHandler().addListener(event_type::e_key_event, f_k);

    InputEventHandler::getEventHandler().addListener(event_type::e_key_event, WindowsInputDelegate::create<&createTestApp>());
    InputEventHandler::getEventHandler().addListener(event_type::e_key_event, WindowsInputDelegate::create<&deleteTestApp>());

    while(test_window.run())
    {
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        glClearColor(1.0f, 1.0f, 0.0f, 1.0f);
        // Your OpenGLCode here...
        test_window.swapBuffers();
    }
    return 0;
}

If we run the program now we can use the various keys to generate events, as well as using ‘o’ and ‘p’ to create and delete an object which in turn registers and unregisters additional listeners.

Final Thoughts

This is a very basic add-on to [Tutorial2]http://keithgrant.co.uk/tutorials/Tutorial2/, and should be seen more as a starting point rather than a reccomended way of doing anything, in particular I would like to use std::function rather than my own delegate type.
There are areas to look at as well, the range of event types could be expanded so that there was a seperate event for each key-state (pressed, repeat and released) rather than one event type with additional parameters.
The InputEventHandler could also queue the events for handling later rather than just dealing with them instantly, this may allow you to have more fine-grained control on the order or speed that things happen. This can be important when you’re trying to run a game-loop on a tight timing budget.

Source for delegate.cpp

#!c++
#pragma once
template<typename ret_type, typename... param_types>
class Delegate
{
    using Type = ret_type(*)(void*, param_types...);
public:

    Delegate(void* inst, Type function):
        m_instance(inst),
        m_func(function)
    {}

    template<typename T, ret_type(T::*TMethod)(param_types...)>
    static Delegate create(T* inst = nullptr)
    {
        Delegate ret_val(inst, &callMethod<T, TMethod>);
        return ret_val;
    }

    template<ret_type(TMethod)(param_types...)>
    static Delegate create()
    {
        Delegate ret_val(nullptr, &callMethod<TMethod>);
        return ret_val;
    }

    ret_type operator() (param_types... args)
    {
        return (*m_func)(m_instance, args...);
    }

    bool Delegate::operator==(const Delegate &other) const {
        return (m_instance == other.m_instance) &&
               (m_func == other.m_func);
    }

private:
    void* m_instance;
    Type m_func;

    template <class T, ret_type(T::*TMethod)(param_types...)>
    static ret_type callMethod(void* inst, param_types... args)
    {
        T* p = static_cast<T*>(inst);
        return (p->*TMethod)(args...);
    }

    template <ret_type(TMethod)(param_types...)>
    static ret_type callMethod(void* inst, param_types... args)
    {
        return (*TMethod)(args...);
    }
};