Starting with Lumberyard Input – Part 1

This tutorial will focus on implementing some basic input controls in a simple fps type game.

This will not cover how to start a new project and gem. We will assume that is set up already and all we need to do now is add components as needed. If you need help setting up your environment, then go here.

The project and gem for this is called TutorialSeries.

Basic Setup

So first we need to make a level in the editor. You probably already have this but it’s pretty straight forward in the editor. Once we have that we are going to make a player entity and a camera entity.

tut_input_2entities

We need the following components

tut_input_player1

tut_input_camera1

Note that the parent of the camera is the player entity. This it’s so when we move the player it will move the camera. So the entity overview should now look like this.

tut_input_entity_after_parent

Ok we are ready to start coding now. Export and save the level, and close the editor.

Setup for our New Component

If you haven’t already, let’s first make our level be auto loaded. A simple way to do this is to add the console command to our startup method.

int GameStartup::Run(const char* autoStartLevelName)
{
    gEnv->pConsole->ExecuteString("exec autoexec.cfg");
    gEnv->pConsole->ExecuteString("map TutorialLevel");

Next we are going to add a component called TutorialSeriesCharacterComponent. Let’s first add it to the waf file.

tutorialseries.waf_files:

"auto": {
    ...
    "Source/Compnonets": [
        "Source/Components/TutCharacterComp.h",
        "Source/Components/TutCharacterComp.cpp"
    ],
    ...

Next add the header and source file.

tut_input_adding_comp_file

Note that if you are using Visual Studio (rather than vscode like me) the best way to add files is:

  1. Update the waf file with the new file.
  2. Close visual studio
  3. Add the files with console, ps, or explorer.
  4. Run lmbr_waf configure
  5. Open visual studio back up

Next let’s put a skeleton structure into our new component.

TutCharacterComp.h

#pragma once
#include <AzCore/Component/Component.h>

namespace TutorialSeries
{
    class TutorialSeriesCharacterComponent
        : public AZ::Component
    {
    public:
        AZ_COMPONENT(TutorialSeriesCharacterComponent, "{F52E6197-C72B-4BEF-99CB-FE41C36CF882}");

        ~TutorialSeriesCharacterComponent() override = default;

        static void Reflect(AZ::ReflectContext* reflection);

        void Init() override;
        void Activate() override;
        void Deactivate() override;
    };
}

TutCharacterComp.cpp

#include "StdAfx.h"

#include "Components/TutCharacterComp.h"

#include <AzCore/Serialization/EditContext.h>

using namespace TutorialSeries;
using namespace AzFramework;

void TutorialSeriesCharacterComponent::Reflect(AZ::ReflectContext *reflection)
{
    if (auto serializationContext = azrtti_cast<AZ::SerializeContext *>(reflection))
    {
        serializationContext->Class<TutorialSeriesCharacterComponent>()
            ->Version(1);

        if (auto editContext = serializationContext->GetEditContext())
        {
            editContext->Class<TutorialSeriesCharacterComponent>("TutorialSeriesCharacterComponent", "Main controller component")
                ->ClassElement(AZ::Edit::ClassElements::EditorData, "")
                ->Attribute(AZ::Edit::Attributes::Category, "TutorialSeries")
                ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC("Game"));
        }
    }
}

void TutorialSeriesCharacterComponent::Init()
{
}

void TutorialSeriesCharacterComponent::Activate()
{
}

void TutorialSeriesCharacterComponent::Deactivate()
{
}

Next we need to register our component in our module.cpp.

TutorialSeriesModule.cpp

#include "StdAfx.h"
#include <platform_impl.h>

#include <AzCore/Memory/SystemAllocator.h>

#include "TutorialSeriesSystemComponent.h"
#include "Components/TutCharacterComp.h"

#include <IGem.h>

namespace TutorialSeries
{
    class TutorialSeriesModule
        : public CryHooksModule
    {
    public:
        AZ_RTTI(TutorialSeriesModule, "{33A3448B-6DF2-421F-A106-26FEADC7CBE2}", CryHooksModule);
        AZ_CLASS_ALLOCATOR(TutorialSeriesModule, AZ::SystemAllocator, 0);

        TutorialSeriesModule()
            : CryHooksModule()
        {
            // Push results of [MyComponent]::CreateDescriptor() into m_descriptors here.
            m_descriptors.insert(m_descriptors.end(), {
                TutorialSeriesSystemComponent::CreateDescriptor(),
                TutorialSeriesCharacterComponent::CreateDescriptor()
            });
        }

        /**
         * Add required SystemComponents to the SystemEntity.
         */
        AZ::ComponentTypeList GetRequiredSystemComponents() const override
        {
            return AZ::ComponentTypeList{
                azrtti_typeid<TutorialSeriesSystemComponent>(),
            };
        }
    };
}

// DO NOT MODIFY THIS LINE UNLESS YOU RENAME THE GEM
// The first parameter should be GemName_GemIdLower
// The second should be the fully qualified name of the class above
AZ_DECLARE_MODULE_CLASS(TutorialSeries_7a17c44ce76744e1b646e6d8556372c8, TutorialSeries::TutorialSeriesModule)

Let’s compile and ensure everything is a-okay.

Assign our New Component

Time to go back to the editor.

Note: I had to “lmbr_waf.bat build_win_x64_vs2015_debug -p all” and run the editor from the debug folder for our TutorialSeriesCharacterComponent to show up.

In the editor, load our level and select our player entity. Now add our new component.

tut_input_add_custom_comp

Boom. Now we are ready to start. Export, save, close.

Time to Start the Input

Oh yeah, wasn’t this whole thing about implementing basic input? Har har. Before we go into how things work, lets first get some code under our belt so you can have a context for what I’m talking about.

TutCharacterComp.h:

#pragma once
#include "AzCore/Component/Component.h"
#include "AzFramework/Input/Events/InputChannelEventListener.h"

namespace TutorialSeries
{
    struct Vector3;

    class TutorialSeriesCharacterComponent
        : public AZ::Component,
          public AzFramework::InputChannelEventListener
    {
    public:
        AZ_COMPONENT(TutorialSeriesCharacterComponent, "{F52E6197-C72B-4BEF-99CB-FE41C36CF882}");

        ~TutorialSeriesCharacterComponent() override = default;

        static void Reflect(AZ::ReflectContext* reflection);

        void Init() override;
        void Activate() override;
        void Deactivate() override;

        bool OnInputChannelEventFiltered(const AzFramework::InputChannel& inputChannel) override;

    private:
        void OnKeyboardEvent(const AzFramework::InputChannel& inputChannel);
        void OnMouseEvent(const AzFramework::InputChannel& inputChannel);

        bool movingForward = false;
        bool movingBack = false;
        bool strafingLeft = false;
        bool strafingRight = false;
    };
}

Lets go over the things we added here. InputChannelEventListener.h is a nice wrapper around Lumberyard’s input notification system. Whenever an input is made by the player, it will send that input in the form of an InputChannel to the OnInputChannelEventFiltered function.

All InputChannels have an InputChannelId that defines which input it is (w key, right mouse, controller A button, etc.) Furthermore, all inputs have a “value” and a “state.” For keyboard events, value is 1 for pressed, 0 for not pressed, and the states are Idle, Begin, Updated, End. Depending on the type of input, what the value is and what the states mean are pretty obvious.

We’ll go deeper later.

OnKeyboardEvent and OnMouseEvent are helper functions I made for later use. The bools are going to be used to keep track of whether the player is moving or not.

Time to Handle the Keyboard Events

It’s time to detect when the player hits the WASD.

TutCharacterComp.cpp

#include "StdAfx.h"

#include "Components/TutCharacterComp.h"

#include <AzFramework/Input/Devices/Keyboard/InputDeviceKeyboard.h>
#include <AzCore/Serialization/EditContext.h>
#include <AzFramework/Input/Events/InputChannelEventListener.h>

using namespace TutorialSeries;
using namespace AzFramework;

void TutorialSeriesCharacterComponent::Reflect(AZ::ReflectContext *reflection)
{
    if (auto serializationContext = azrtti_cast<AZ::SerializeContext *>(reflection))
    {
        serializationContext->Class<TutorialSeriesCharacterComponent>()
            ->Version(1);

        if (auto editContext = serializationContext->GetEditContext())
        {
            editContext->Class<TutorialSeriesCharacterComponent>("TutorialSeriesCharacterComponent", "Main controller component")
                ->ClassElement(AZ::Edit::ClassElements::EditorData, "")
                ->Attribute(AZ::Edit::Attributes::Category, "TutorialSeries")
                ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC("Game"));
        }
    }
}

void TutorialSeriesCharacterComponent::Init()
{
}

void TutorialSeriesCharacterComponent::Activate()
{
    InputChannelEventListener::Connect();
}

void TutorialSeriesCharacterComponent::Deactivate()
{
    InputChannelEventListener::Disconnect();
}


bool TutorialSeriesCharacterComponent::OnInputChannelEventFiltered(const AzFramework::InputChannel &inputChannel)
{
    auto device_id = inputChannel.GetInputDevice().GetInputDeviceId();
    if (device_id == InputDeviceMouse::Id)
    {
        OnMouseEvent(inputChannel);
    }

    if (device_id == InputDeviceKeyboard::Id)
    {
        OnKeyboardEvent(inputChannel);
    }

    return false;
}

void TutorialSeriesCharacterComponent::OnKeyboardEvent(const InputChannel &inputChannel)
{
    auto input_type = inputChannel.GetInputChannelId();
    if (input_type == InputDeviceKeyboard::Key::AlphanumericW)
    {
        movingForward = !!inputChannel.GetValue();
    }
    else if (input_type == InputDeviceKeyboard::Key::AlphanumericS)
    {
        movingBack = !!inputChannel.GetValue();
    }
    else if (input_type == InputDeviceKeyboard::Key::AlphanumericA)
    {
        strafingLeft = !!inputChannel.GetValue();
    }
    else if (input_type == InputDeviceKeyboard::Key::AlphanumericD)
    {
        strafingRight = !!inputChannel.GetValue();
    }
}

void TutorialSeriesCharacterComponent::OnMouseEvent(const InputChannel &inputChannel)
{
}

First, notice that we had to connect/disconnect the InputChannelEventListener in the activate/deactivate functions. This sets up the EBus stuff involving inputs for us.

Lets look at the OnInputChannelEventFiltered function. You will see a new class called InputDevice. There are many types, but we care about the keyboard and mouse one. Each have an Id which we are using here to determine how to handle the input. These devices are also in charge of setting up all the different input channels related to them.

Looking at OnKeyboardEvent, you will see that we can determine what inputChannel was sent to us by comparing its InputChannelId to the channelIds set up on the InputDeviceKeyboard class.

Something to keep in mind is that InputChannel is a base class, and there are many different derived classes. For keyboard keys, it uses the InputChannelDigital derived class. For that, the value returned from GetValue is 1 or 0 (key up or key down.) So we employee the ghetto !! trick to turn that into a true/false bool.

For now, we don’t do anything with the mouse.

Compile to make sure we have no errors. Run the game to make sure nothing crashes… though obviously you won’t be able to move yet.

Time to Move the Player!

Honestly, the above section explains the keyboard input system, and moving the player is a different topic. So this next part I’m not going to explain in detail but will highlight on specific Lumberyard stuff.

TutCharacterComp.h:

#pragma once
#include "AzCore/Component/TransformBus.h"
#include "AzCore/Component/Component.h"
#include "AzFramework/Input/Events/InputChannelEventListener.h"
#include "AzCore/Component/TickBus.h"

namespace TutorialSeries
{
    struct Vector3;

    class TutorialSeriesCharacterComponent
        : public AZ::Component,
          public AzFramework::InputChannelEventListener,
          public AZ::TickBus::Handler,
          public AZ::TransformNotificationBus::Handler
    {
    public:
        AZ_COMPONENT(TutorialSeriesCharacterComponent, "{F52E6197-C72B-4BEF-99CB-FE41C36CF882}");

        ~TutorialSeriesCharacterComponent() override = default;

        static void Reflect(AZ::ReflectContext* reflection);

        void Init() override;
        void Activate() override;
        void Deactivate() override;

        void OnTick(float deltaTime, AZ::ScriptTimePoint time) override;

        bool OnInputChannelEventFiltered(const AzFramework::InputChannel& inputChannel) override;

    private:
        void OnKeyboardEvent(const AzFramework::InputChannel& inputChannel);
        void OnMouseEvent(const AzFramework::InputChannel& inputChannel);

        void HandleForwardBackwardMovement(AZ::Vector3& desiredVelocity);
        void HandleStrafing(AZ::Vector3& desiredVelocity);

        float MovementScale = 5.0f;

        bool movingForward = false;
        bool movingBack = false;
        bool strafingLeft = false;
        bool strafingRight = false;
    };
}

TutCharacter.cpp:

#include "StdAfx.h"

#include "Components/TutCharacterComp.h"

#include "AzFramework/Input/Devices/Keyboard/InputDeviceKeyboard.h"
#include "AzFramework/Input/Devices/Mouse/InputDeviceMouse.h"
#include <AzCore/Component/ComponentApplicationBus.h>
#include <AzCore/EBus/EBus.h>
#include <AzCore/Math/Transform.h>
#include <AzCore/Serialization/EditContext.h>
#include <AzFramework/Entity/GameEntityContextBus.h>
#include <AzFramework/Input/Channels/InputChannel.h>
#include <LmbrCentral/Physics/CryCharacterPhysicsBus.h>
#include <AzFramework/Network/NetBindingHandlerBus.h>

using namespace TutorialSeries;
using namespace AzFramework;

void TutorialSeriesCharacterComponent::Reflect(AZ::ReflectContext *reflection)
{
    if (auto serializationContext = azrtti_cast<AZ::SerializeContext *>(reflection))
    {
        serializationContext->Class<TutorialSeriesCharacterComponent>()
            ->Version(1)
            ->Field("Movement scale", &TutorialSeriesCharacterComponent::MovementScale);

        if (auto editContext = serializationContext->GetEditContext())
        {
            editContext->Class<TutorialSeriesCharacterComponent>("TutorialSeriesCharacterComponent", "Main controller component")
                ->ClassElement(AZ::Edit::ClassElements::EditorData, "")
                ->Attribute(AZ::Edit::Attributes::Category, "TutorialSeries")
                ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC("Game"))
                ->DataElement(nullptr, &TutorialSeriesCharacterComponent::MovementScale, "Movement scale", "How fast the character moves");
        }
    }
}

void TutorialSeriesCharacterComponent::Init()
{
}

void TutorialSeriesCharacterComponent::Activate()
{
    AZ::TickBus::Handler::BusConnect();
    InputChannelEventListener::Connect();

    AZ::TransformNotificationBus::Handler::BusConnect(GetEntityId());
}

void TutorialSeriesCharacterComponent::Deactivate()
{
    AZ::TickBus::Handler::BusDisconnect();
    InputChannelEventListener::Disconnect();
    AZ::TransformNotificationBus::Handler::BusDisconnect();
}

bool TutorialSeriesCharacterComponent::OnInputChannelEventFiltered(const AzFramework::InputChannel &inputChannel)
{
    auto device_id = inputChannel.GetInputDevice().GetInputDeviceId();
    if (device_id == InputDeviceMouse::Id)
    {
        OnMouseEvent(inputChannel);
    }

    if (device_id == InputDeviceKeyboard::Id)
    {
        OnKeyboardEvent(inputChannel);
    }

    return false;
}

void TutorialSeriesCharacterComponent::OnKeyboardEvent(const InputChannel &inputChannel)
{
    auto input_type = inputChannel.GetInputChannelId();
    if (input_type == InputDeviceKeyboard::Key::AlphanumericW)
    {
        movingForward = !!inputChannel.GetValue();
    }
    else if (input_type == InputDeviceKeyboard::Key::AlphanumericS)
    {
        movingBack = !!inputChannel.GetValue();
    }
    else if (input_type == InputDeviceKeyboard::Key::AlphanumericA)
    {
        strafingLeft = !!inputChannel.GetValue();
    }
    else if (input_type == InputDeviceKeyboard::Key::AlphanumericD)
    {
        strafingRight = !!inputChannel.GetValue();
    }
}

void TutorialSeriesCharacterComponent::OnTick(float deltaTime, AZ::ScriptTimePoint time)
{
    AZ::Transform entityTransform;
    EBUS_EVENT_ID_RESULT(entityTransform, GetEntityId(), AZ::TransformBus, GetWorldTM);

    auto desiredVelocity = AZ::Vector3::CreateZero();
    if (movingForward || movingBack || strafingLeft || strafingRight)
    {
        HandleForwardBackwardMovement(desiredVelocity);
        HandleStrafing(desiredVelocity);
    }

    // Apply relative translation to the character via physics.
    EBUS_EVENT_ID(GetEntityId(), LmbrCentral::CryCharacterPhysicsRequestBus, RequestVelocity, desiredVelocity, 0);
}

void TutorialSeriesCharacterComponent::HandleForwardBackwardMovement(AZ::Vector3 &desiredVelocity)
{
    if (movingBack || movingForward)
    {
        float forward_back_vel = 0;
        if (movingForward)
        {
            forward_back_vel += MovementScale;
        }
        if (movingBack)
        {
            forward_back_vel -= (MovementScale / 2.f);
        }

        desiredVelocity.SetY(forward_back_vel);
    }
}

void TutorialSeriesCharacterComponent::HandleStrafing(AZ::Vector3 &desiredVelocity)
{
    if (strafingLeft || strafingRight)
    {
        float left_right_vel = 0;
        if (strafingRight)
        {
            left_right_vel += MovementScale * .75f;
        }
        if (strafingLeft)
        {
            left_right_vel -= MovementScale * .75f;
        }

        desiredVelocity.SetX(left_right_vel);
    }
}

void TutorialSeriesCharacterComponent::OnMouseEvent(const InputChannel &inputChannel)
{
}

Okaaaaay.

First the Reflect function. We added a MovementScale variable that can be modified in the editor that defaults to 5. Change this value to be whatever you want your speed to be. Also, we make backwards be half of the MovementScale, and left and right be 3/4 of it.

We now inherit TickBus and TransformBus and connect/disconnect them in our activate/deactivate. We need these for obvious reasons.

OnTick is really where the movement magic happens. We use the Transform bus to get the current position of the player. Then we do some math to determine the rate at which we want to move forward (Y direction) and side-to-side (X direction). Last, we call the CryCharacterPhysicsRequestBus to set our entity’s velocity based on those calculations.

One last minor thing, you will see a return false on the OnInputChannelEventFiltered function. If we return true there, then nothing else will process input after this component. Also with the InputChannelEventListener, you can set priorities. As an example, if the player clicks, you may want to press a button or fire a gun. If a button exists, you can return true, and it won’t fire the gun which comes after.

Conclusion

This wraps up part 1. If you want to learn more about the input system, check out the current documentation (as of this post) here.

Part 2 of this guide is now available!

by Greg Horvay

12 thoughts on “Starting with Lumberyard Input – Part 1

  1. Hi, I am so glad I stumbled upon your site. It’s great but I have come across a problem that I cannot figure out. When trying to get the id of the mouse with InputDeviceMouse::Id I get an error that InputDeviceMouse is not a class or namespace. Which is weird because I was able to find the class declaration in InputSystemComponent.h as well as its definition in InputDeviceMouse.ccp and InputDeviceMouse.h. I compared this to the way InputDeviceKeyboard (which works) is setup and it seems to be the same. I am not sure where the issue lies but i am not able to compile due to it. Any ideas?

    Like

      1. Also I would like to make a note that in your TutCharacterComp.cpp in the “Time to Handle Keyboard Events” section you do not include “AzFramework/Input/Devices/Mouse/InputDeviceMouse.h” but try to use InputDeviceMouse::Id so it’s always gonna fail to compile. Maybe you could update that so others don’t get stuck like me 🙂

        Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s