Actions Tutorial

Welcome to the tutorial about the Input module. Here, you will learn how to build up an object-oriented event system. From SFML, you know the procedural way of event handling, namely polling:

while (window.pollEvent(event))
{
    // switch on event type
}

However, this approach has a few drawbacks. You have to differentiate the types explicitly, and your code reacting to the events is gathered in one place. The single events are hardwired as compile-time constants after each case mark. That makes it complicated to change controls according to the user's request.

The Action system of Thor's Input module has been designed with the following thoughts in mind:

Actions

The class thor::Action represents an action. It can be used in the following situations:

We say that an action is active if the event or input state specified in the constructor has been triggered. In the following example, we define some actions. The comments explain when each action is active.

thor::Action a(sf::Keyboard::X, thor::Action::PressOnce); // Key X is pressed once
thor::Action b(sf::Mouse::Left, thor::Action::Hold);      // Left mouse button is currently held down
thor::Action c(sf::Event::Closed);                        // SFML Window is closed
We can even combine multiple actions! The following creates an action which is active when both a and b are active:
thor::Action both = a && b;
The same works if at least one of them must be active in order to activate the combined action:
thor::Action atLeastOne = a || b;
Of course, you can build arbitrary complex expressions using the overloaded AND and OR operators.

Action maps

Now we have defined some actions, but how can we use them? Here, the class template thor::ActionMap comes into play.

This class template maps IDs to actions. IDs can be of any comparable type, for example std::string or a user-defined enumeration. This type is specified in the template parameter.

thor::ActionMap<std::string> map;

We have defined an action map that stores std::string identifiers associated with thor::Action instances. Now let's introduce meaningful IDs and associate them with the actions above:

map["run"] = a;
map["shoot"] = b;
map["quit"] = c;
Now the action map knows our actions. We can check if an action is currently active by writing
if (map.isActive("run"))
     // React to "run" action
if (map.isActive("shoot"))
     // React to "shoot" action
if (map.isActive("quit"))
     // React to "quit" action
How does the action map know if an action is active? In order to make this possible, we need to feed it with the SFML events occurred every frame. This is automatically done if we call the thor::ActionMap::update() method. We pass the SFML window from which events are polled. Here is an example of a main loop:
while (window.isOpened())
{
    // Poll the window for new events, update actions
    map.update(window);

    // React to different action types
    if (map.isActive("quit"))
        window.close();

    // Update window
    window.display();
}

As an alternative to update(), thor::ActionMap provides the methods pushEvent() to add an SFML event and clearEvents() to remove old events. These methods exist for more flexibility, if you want to poll the window yourself and decide which events are forwarded to the action map. Using this approach, you can write the following code:

while (window.isOpened())
{
    // Clear events from last frame
    map.clearEvents();

    // Forward all events to the action map
    sf::Event event;
    while (window.pollEvent(event))
        map.pushEvent(event);

    // React to different action types
    if (map.isActive("quit"))
        window.close();

    // Update window
    window.display();
}

Connecting callbacks

Actions can act as triggers for function invocations. Let’s call the functions associated to the different event types “listeners” (other terms are “callbacks”, “slots”, “event handlers” or “observers”).

What we need now are the listeners that are invoked if an action is active. Listener functions return void and take a parameter of type thor::ActionContext with your ID type as template argument. This parameter contains information about the context in which an action was fired (for example the original sf::Event that triggered the action).

Define a listener for actions with the ID "shoot":

void onShoot(thor::ActionContext<MyAction> context)
{
    // context.window is a pointer to a sf::Window. It can be used
    // for mouse input relative to a window, as follows:
    sf::Vector2i mousePosition = sf::Mouse::getPosition(*context.window);

    // Do something with the mouse position...
}

Using the class template thor::EventSystem, you can associate different action types with different functions. thor::ActionMap contains a member typedef CallbackSystem for the correct type. Let's instantiate such a callback system and connect the ID "shoot"with the just defined callback onShoot():

thor::ActionMap<std::string>::CallbackSystem system;
system.connect("shoot", &onShoot);

The callback system is ready to call the correct functions if an action is triggered. We only need to forward the actions from thor::ActionMap to the CallbackSystem. We can do this in the main loop, instead of the isActive() calls:

map.invokeCallbacks(system, &window);

The second parameter is the window, which is assigned to the pointer in thor::ActionContext. If you don't need the window inside the callbacks, you can also pass a null pointer. In this case, the callback will have no information about the window, so the function onShoot() would not work.

map.invokeCallbacks(system, nullptr);

The callback mechanism is quite powerful. The second parameter of connect() can not only be a pointer to function like in the above example, rather any compatible callable is allowed. That is, you can also pass function objects, especially instances of std::function. Besides, you are able to connect any number of listeners to one action type, just invoke connect()once for each listener.

Another example is a listener that moves a sprite when the player is running:

void moveSprite(sf::Sprite& sprite)
{
    sprite.move(10.f, 0.f);
}

Here, we can’t register MoveSprite() directly at the event system, because the parameter list doesn’t match (sf::Sprite& vs.thor::ActionContext<std::string>). But we can build a function object:

struct SpriteMover
{
    explicit SpriteMover(sf::Sprite& sprite)
    : sprite(sprite)
    {
    }

    void operator() (thor::ActionContext<std::string>)
    {
        // ignore parameter
        sprite.move(10.f, 0.f);
    }

    sf::Sprite& sprite;
};

Then the registration looks like this:

sf::Sprite playerSprite;
system.connect("run", SpriteMover(playerSprite));

Binders [advanced]

This is a chapter that requires rather advanced C++ knowledge, so depending on your skills, you might want to skip it. You can use the whole functionality of the event systems in Thor without binders, however they can simplify things a lot.

You may think, the functor SpriteMover needs quite a lot code to achieve a single-line functionality. You are right. Fortunately, C++ offers other ways, namely binders. The library Boost.Bind (which has made it into the standard library) contains a bunch of tools to compose new functions on-the-fly. If you are unfamiliar to binders, you should have a look at the Boost documentation – you don’t need Boost though. Using the global function moveSprite() from above, the registration looks like the following. You need std::ref to pass the sprite as reference.

sf::Sprite playerSprite;
system.connect("run", std::bind(&moveSprite, std::ref(playerSprite)));

We got rid of the class and all the constructor/member/initialization stuff. But honestly, that annoying function does nothing more than forwarding a call to a member. Since binders support member functions, why not use them to call sf::Sprite::move() directly?

sf::Sprite playerSprite;
system.connect("run", std::bind(&sf::Sprite::move, &playerSprite, 10.f, 0.f));

Now all the functionality is contained in a single line, and we are relieved from writing boilerplate code. The same principle applies to C++11 lambda expressions.

Disconnecting callbacks

With the function thor::EventSystem::connect(), you have the possibility to add event-listener connections. But you don't know how to remove them from the system as soon as they aren’t needed anymore. The member function Connect() returns an object of type thor::Connection which exists exactly for this purpose. You can store it and call its method disconnect() later to disconnect the listener from the event.

thor::Connection connection = system.connect("shoot", &onShoot);
connection.disconnect();

A connection automatically becomes invalid when the referenced listener is disconnected, so you can’t disconnect a listener accidentally more than once. To disconnect all listeners associated with a given event type, call thor::EventSystem::clearConnections().