Examples » Engine » Input

Using the Input plugin.

This example shows how the scene-input plugin can be used to handle user input.

The plugin function is included from the engine/input/plugin.hpp header.

    cubos.plugin(settingsPlugin);
    cubos.plugin(windowPlugin);
    cubos.plugin(assetsPlugin);
    cubos.plugin(inputPlugin);

The Input plugin requires a InputBindings asset to be set. Let's take a look at the file the sample uses.

{
    "actions": {
        "next-showcase": [
            {"keys": ["Return"]},
            {"gamepadButtons": ["RBumper"]}
        ],
        "x-or-z": [
            {"keys": ["X"]},
            {"keys": ["Z"]}
        ],
        "shift-space": [
            {"keys": ["LShift", "Space"]},
            {"keys": ["RShift", "Space"]}
        ],
        "ctrl-shift-space": [
            {"keys": ["LControl", "LShift", "Space"]},
            {"keys": ["LControl", "RShift", "Space"]},
            {"keys": ["RControl", "LShift", "Space"]},
            {"keys": ["RControl", "RShift", "Space"]}
        ],
        "left-mb": [
            {"mouseButtons": ["Left"]}
        ],
        "right-mb": [
            {"mouseButtons": ["Right"]}
        ],
        "middle-mb": [
            {"mouseButtons": ["Middle"]}
        ],
        "extra-mb": [
            {"mouseButtons": ["Extra1"]},
            {"mouseButtons": ["Extra2"]}
        ]
    },
    "axes": {
        "vertical": {
            "positive": [
                {"keys": ["W"]},
                {"keys": ["Up"]}
            ],
            "negative": [
                {"keys": ["S"]},
                {"keys": ["Down"]}
            ],
            "gamepadAxes": [
                "LY"
            ]
        },
        "horizontal": {
            "positive": [
                {"keys": ["D"]},
                {"keys": ["Right"]}
            ],
            "negative": [
                {"keys": ["A"]},
                {"keys": ["Left"]}
            ],
            "gamepadAxes": [
                "LX"
            ]
        },
        "shift-vertical": {
            "positive": [
                {"keys": ["LShift", "W"]},
                {"keys": ["LShift", "Up"]},
                {"keys": ["RShift", "W"]},
                {"keys": ["RShift", "Up"]}
            ],
            "negative": [
                {"keys": ["LShift", "S"]},
                {"keys": ["LShift", "Down"]},
                {"keys": ["RShift", "S"]},
                {"keys": ["RShift", "Down"]}
            ]
        },
        "shift-horizontal": {
            "positive": [
                {"keys": ["LShift", "D"]},
                {"keys": ["LShift", "Right"]},
                {"keys": ["RShift", "D"]},
                {"keys": ["RShift", "Right"]}
            ],
            "negative": [
                {"keys": ["LShift", "A"]},
                {"keys": ["LShift", "Left"]},
                {"keys": ["RShift", "A"]},
                {"keys": ["RShift", "Left"]}
            ]
        }
    }
}

There are two types of bindings: actions and axes. An action is an input that only has two states: pressed or not pressed. This would be most keys on a keyboard. An axe is an input that has a numeric value. For example, the joysticks on a controller can go from -1 to 1, depending on how much they are tilt in which direction. Using axes can also be useful for keys with symmetric behaviour. For example, in this sample, W sets the vertical axe to 1, while S sets it to -1.

To define an action or an axe, you simply have to add it to the respective list, giving it a name. The very first action in the file is called next-showcase. Then, if it's an action, you simply have to define which keys trigger it. You can also define key combinations by using a -. To check which strings map to which keys, you check the names of the variants of the enums Key and Modifier on this file.

Now that we have our bindings file, let's get our application to do something with it. The first thing we're going to need is a reference to the bindings asset. For the purposes of this sample we can simply use an hardcoded reference to the asset.

static const Asset<InputBindings> BindingsAsset = AnyAsset("bf49ba61-5103-41bc-92e0-8a442d7842c3");

To utilize the bindings, loading them is essential. This can be accomplished by establishing a startup systems which reads from the asset and sets the required bindings.

    cubos.startupSystem("load and set the Input Bindings")
        .tagged(assetsTag)
        .call([](const Assets& assets, Input& input) {
            auto bindings = assets.read<InputBindings>(BindingsAsset);
            input.bind(*bindings);
            CUBOS_INFO("Loaded bindings: {}", input.bindings().at(0));
        });

Getting the input is done through the cubos::engine::Input resource. What this sample does is show in order, a series of prompt to showcase the different functionalities of the Input plugin. For this, it keeps a state integer that indicates the current prompt. Whenever the action next-showcase is triggered, it advances to the next prompt. However, as the plugin currently does not have events, we have to manually check whether the key has just been pressed, is being pressed continuously, or was just released.

    cubos.system("detect input")
        .after(inputUpdateTag)
        .call([](const Input& input, const Window& window, State& state, ShouldQuit& shouldQuit) {
            if (input.justPressed("next-showcase"))
            {
                state.explained = false;
                state.showcase++;
            }

What this does is only advance the state when the return key is released. This avoids the state advancing more than once if the user presses it for more than one frame.

Now let's see each of the prompt, to understand the full breadth of the plugin's functionalities.

static void showcaseXZ(const Input& input, bool& explained)
{
    if (!explained)
    {
        CUBOS_WARN("This showcase will print `X or Z` when either X or Z is pressed. Press Enter to advance to the "
                   "next showcase.");
        explained = true;
    }

    if (input.pressed("x-or-z"))
    {
        CUBOS_INFO("X or Z");
    }
}

static void showcaseJustXZ(const Input& input, bool& explained)
{
    if (!explained)
    {
        CUBOS_WARN("This showcase will print `justPressed X or Z` when either X or Z is justPressed and `justReleased "
                   "X or Z when either X or Z is justReleased`. Press Enter to advance to the "
                   "next showcase.");
        explained = true;
    }

    if (input.justPressed("x-or-z"))
    {
        CUBOS_INFO("justPressed X or Z");
    }
    if (input.justReleased("x-or-z"))
    {
        CUBOS_INFO("justReleased X or Z");
    }
}

Finding out whether the user is pressing a key is checked by a simple call to Input::pressed.

static void showcaseModifiers(const Input& input, bool& explained)
{
    if (!explained)
    {
        CUBOS_WARN("Modifiers are supported. This showcase will print `Shift` when Shift+Space is pressed. "
                   "Press Enter to advance to the next showcase.");
        explained = true;
    }

    if (input.pressed("shift-space"))
    {
        CUBOS_INFO("Shift");
    }
}

Getting modified input (such as with a Control or a Shift hold) is no different from getting non-modified input, just make sure the binding for it is defined in the Bindings asset.

static void showcaseMultipleModifiers(const Input& input, bool& explained)
{
    if (!explained)
    {
        CUBOS_WARN("Multiple modifiers are supported. This showcase will print `Ctrl Shift` when Ctrl+Shift+Space is "
                   "pressed. Press Enter to advance to the next showcase.");
        explained = true;
    }

    if (input.pressed("ctrl-shift-space"))
    {
        CUBOS_INFO("Ctrl Shift");
    }
}

You can have more than one modifier.

static void showcaseAxis(const Input& input, bool& explained)
{
    if (!explained)
    {
        CUBOS_WARN("Axis are supported. This showcase will print the value of the `horizontal` and `vertical` axis "
                   "when their values is different to 0. Use the arrows and WASD to move the axis. Press Enter to "
                   "advance to the next showcase.");
        explained = true;
    }

    if (input.axis("horizontal") != 0.0F || input.axis("vertical") != 0.0F)
    {
        CUBOS_INFO("horizontal: {}, vertical: {}", input.axis("horizontal"), input.axis("vertical"));
    }
}

Getting axis is very similar to actions, by calling Input::axis. The difference is that this funtion returns a float instead of a boolean value.

static void showcaseModifierAxis(const Input& input, bool& explained)
{
    if (!explained)
    {
        CUBOS_WARN("Modifiers are supported with axis. This showcase will print the value of the `shift-vertical` axis "
                   "when its value is different to 0. Use Shift+arrows and Shift+WASD to move the axis. Press Enter to "
                   "advance to the next showcase.");
        explained = true;
    }

    if (input.axis("shift-horizontal") != 0.0F || input.axis("shift-vertical") != 0.0F)
    {
        CUBOS_INFO("shift-horizontal: {}, shift-vertical: {}", input.axis("shift-horizontal"),
                   input.axis("shift-vertical"));
    }
}

Modifier keys work with axis too.

static void showcaseUnbound(const Window& window, bool& explained)
{
    if (!explained)
    {
        CUBOS_WARN("Direct access is supported. This showcase will print `Unbound` when Y is pressed. Note "
                   "that Y is not bound in sample.bind. Press Enter to advance to the next showcase.");
        explained = true;
    }

    if (window->pressed(Input::Key::Y))
    {
        CUBOS_INFO("Unbound");
    }
}

If, for any reason, you want to read an input that is not defined in the Bindings asset, you cannot use the Input plugin for it. Instead, you will have to call the Window::pressed function.

static void showcaseMouseButtons(const Input& input, bool& explained)
{
    if (!explained)
    {
        CUBOS_WARN("This showcase will print the name of a mouse button when it is pressed. Press Enter to advance to "
                   "the next showcase.");
        explained = true;
    }

    if (input.pressed("left-mb"))
    {
        CUBOS_INFO("Left");
    }
    if (input.pressed("right-mb"))
    {
        CUBOS_INFO("Right");
    }
    if (input.pressed("middle-mb"))
    {
        CUBOS_INFO("Middle");
    }
    if (input.pressed("extra-mb"))
    {
        CUBOS_INFO("Extra1 or Extra2");
    }
}

Reading mouse buttons is also supported, just bind them to an action, and then call Input::pressed as usual. To check which strings map to which buttons, you check the names of the variants of the enum MouseButton on this file.