RTS C++ Setup – The Inputs Part 3: El Finalmente

Ok, first of all, I have to tell you something: I lied. I said this tutorial is for creating a basic RTS setup, but the truth is, I actually wanted this tutorial series to be a bit more than that.

In fact, this part will be going through some stuff that you may not see in an RTS, such as rotating the screen, pitch, and zoom type functionality. Stuff that is in other types of top-down games though.

I’m sorry I lied. It’s honestly the worse thing I’ve ever done in my life. All those life accomplishments.. this undoes them all. It’s like when your wife finds out you forgot to take out the garbage.

The Choices

There are a few ways to handle rotation, and we must choose… wisely.

In most bird eye view games, we do not rotate the camera around itself. In FPS, we can merely rotate the camera, and it rotates the character, or just the head.

With top-down, we are usually focusing on some point in the game, and rotating/zooming on that point. Kinda like playing totally accurate battle simulator. The camera rotates around something.

1. The mathematical hell way. One way we could do this is to do a raycast from the camera, and find what we run in to. Then we can do some crazy vector math to rotate the camera around what we hit. I’m not a fan of this, because I don’t know how to do it 😦 and I think there is a simpler way with Lumberyard.

2. Create a new focal point entity. Another way to handle this is to create a focal point entity in the editor, that will track what the camera is currently looking at. Then when we rotate that tracking entity, it will rotate the camera with it, and around it.

I like this method, because it makes the math simpler, and whenever we need to focus on something like say a military unit, we can move the position of the tracking entity to the military unit. So it’s basically an easy way of manipulating the camera by letting us focus on what the camera is looking at.

So without further bloggering…

First we Must Refactor our Entities

So we need to refactor our entities somewhat so that when we “rotate the camera” it is actually rotating the camera around a new tracking object entity.

Open up ze editor, because we got some things to break.

First create a new entity called TrackingObject, and make the camera a child of that object.
tut_birdeye3_entitysetup

Now when we rotate/move the tracking object, the camera will merely follow it around.

Next, we need to fix the transforms for this. Keep in mind the camera transform is kinda a dummy and just follows the TrackingObject. (Yes, followers are dummies, like dumb terminals.)
tut_birdeye3_trackingObject
tut_birdeye3_camera

Notice that we moved our custom component from the dummy to the TrackingObject.

Now roll that beautiful bean footage (ie: save, export, try running to make sure things still work.)

Rotation Time!

In this tutorial, we are going to use Q and E to control rotation, but it should be pretty simple to change that to whatever you want (also totally not mimicking Xcom… like, totally.)

Capturing the keys

Lets start by capturing when the new keys are being pressed. We do it like all the other keys. See lines 21 through 27.

BECameraComponent.cpp:

void BECameraComponent::OnKeyboardEvent(const InputChannel& inputChannel)
{
    auto input_type = inputChannel.GetInputChannelId();
    if (input_type == InputDeviceKeyboard::Key::AlphanumericW)
    {
        movingUp = !!inputChannel.GetValue();
    }
    else if (input_type == InputDeviceKeyboard::Key::AlphanumericS)
    {
        movingDown = !!inputChannel.GetValue();
    }
    else if (input_type == InputDeviceKeyboard::Key::AlphanumericA)
    {
        movingLeft = !!inputChannel.GetValue();
    }
    else if (input_type == InputDeviceKeyboard::Key::AlphanumericD)
    {
        movingRight = !!inputChannel.GetValue();
    }
    else if (input_type == InputDeviceKeyboard::Key::AlphanumericQ)
    {
        rotateLeft = !!inputChannel.GetValue();
    }
    else if (input_type == InputDeviceKeyboard::Key::AlphanumericE)
    {
        rotateRight = !!inputChannel.GetValue();
    }
}

You’ll also need to add rotateLeft and rotateRight to the private variable section of the header. So go do dat.

Rotating your mother.. I mean the Camera

First we need to add some more private variables. Including the new rotateRight/Left variables, we need:

BECameraComponent.h:

        float rotationScale = 5.f;
        float currentRotation = 0.f;
        float currentPitch = 0.35f;
        bool rotateLeft = false;
        bool rotateRight = false;

Now we need to actually perform the rotation in the OnTick function.

BECameraComponent.cpp:

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

    float x = 0.f;
    float y = 0.f;

    if (movingUp || mouseEdgeUp)
        y += movementScale * deltaTime;
    if (movingDown || mouseEdgeDown)
        y -= movementScale * deltaTime;
    if (movingRight || mouseEdgeRight)
        x += movementScale * deltaTime;
    if (movingLeft || mouseEdgeLeft)
        x -= movementScale * deltaTime;

    if (y != 0.f || x != 0.f)
    {
        auto new_position = entityTransform.GetPosition() + AZ::Vector3{x, y, 0.f};
        entityTransform.SetPosition(new_position);
    }

    auto oldRotation = currentRotation;

    if (rotateLeft)
        currentRotation -= rotationScale * deltaTime;
    if (rotateRight)
        currentRotation += rotationScale * deltaTime;

    if (currentRotation - oldRotation != 0)
    {
        AZ::Quaternion xRotation = AZ::Quaternion::CreateRotationX(currentPitch);
        AZ::Quaternion zRotation = AZ::Quaternion::CreateRotationZ(currentRotation);
        entityTransform.SetRotationPartFromQuaternion(zRotation * xRotation);
    }

    EBUS_EVENT_ID(GetEntityId(), AZ::TransformBus, SetWorldTM, entityTransform);
}

You can see that we keep track of the currentRotation, and each time they change it, we recalculate the z and x angles, and use those to set the new rotation.

One important function here is AZ::Quaternion::CreateRotationX/Y, as it allows us to use a normal angle to determine the quaternion. There is trigonometry involved to go from vector + angle to quaternion, and you can check out the implementation of that function if you are curious about that.

Honestly though, quaternions are somewhat confusing, and many people go on using them without really understanding them. The important thing is to know their useful functions to control them. It’s kinda like quantum mechanics. We have no idea why it behaves like it does, but we can make mathematical equations to predict it and use it (wow, that was deep).

(Also, have you heard about the QM speeding joke? So Heisenberg was speeding down the highway and got pulled over by the police. The officer asked, “do you know how fast you were going?” Heisenberg replied, “no, but I know where I am!” el-oh-el oh el-oh-el)

We got 99 Problems, but our Code is 1

So we created a problem. Namely, after rotating, our WASD and mouse edge movements do not account for how we have rotated. So we need to fix that. Here is my solution:

BECameraComponent.cpp:

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

    float x = 0.f;
    float y = 0.f;

    if (movingUp || mouseEdgeUp)
        y += movementScale * deltaTime;
    if (movingDown || mouseEdgeDown)
        y -= movementScale * deltaTime;
    if (movingRight || mouseEdgeRight)
        x += movementScale * deltaTime;
    if (movingLeft || mouseEdgeLeft)
        x -= movementScale * deltaTime;

    auto oldRotation = currentRotation;

    if (rotateLeft)
        currentRotation -= rotationScale * deltaTime;
    if (rotateRight)
        currentRotation += rotationScale * deltaTime;

    if (currentRotation - oldRotation != 0)
    {
        AZ::Quaternion xRotation = AZ::Quaternion::CreateRotationX(currentPitch);
        AZ::Quaternion zRotation = AZ::Quaternion::CreateRotationZ(currentRotation);

        entityTransform.SetRotationPartFromQuaternion(zRotation * xRotation);
    }

    if (y != 0.f || x != 0.f)
    {
        auto new_position = entityTransform.GetPosition() + AZ::Quaternion::CreateRotationZ(currentRotation) * AZ::Vector3{x, y, 0.f};
        entityTransform.SetPosition(new_position);
    }

    EBUS_EVENT_ID(GetEntityId(), AZ::TransformBus, SetWorldTM, entityTransform);
}

I flipped the handling of the position and the rotation, is case they are doing both at once, I want the position to take into account the new rotation.

Lumberyard gives us some operation overloads that allow us to take our position vector and merely * with our rotation (yay Lumberyard (actually all game engines have this.)) Note that we only use the Z axis rotation, because that’s all we care about when using WASD/mouse.

The Final Rotation

The amount that the camera looks up and down, I’m calling the pitch. Pitch is a great word because it’s totally unambiguous. Otherwise sentences like the following would make no sense: “I pitched a ball of pitch which had a slight downward pitch, towards the singer who had horrible pitch, at my wife’s business pitch meeting.”

Anyhoo, here is the pitch for handling pitch.

BECameraComponent.cpp:

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

    float x = 0.f;
    float y = 0.f;

    if (movingUp || mouseEdgeUp)
        y += movementScale * deltaTime;
    if (movingDown || mouseEdgeDown)
        y -= movementScale * deltaTime;
    if (movingRight || mouseEdgeRight)
        x += movementScale * deltaTime;
    if (movingLeft || mouseEdgeLeft)
        x -= movementScale * deltaTime;

    auto oldRotation = currentRotation;

    if (rotateLeft)
        currentRotation -= rotationScale * deltaTime;
    if (rotateRight)
        currentRotation += rotationScale * deltaTime;

    if (pitchUp)
        currentPitch -= rotationScale * deltaTime;
    if (pitchDown)
        currentPitch += rotationScale * deltaTime;

    if (currentPitch < .10f) currentPitch = .10f;
    if (currentPitch > 1.05f) currentPitch = 1.05f;

    if (rotateLeft || rotateRight || pitchUp || pitchDown)
    {
        AZ::Quaternion xRotation = AZ::Quaternion::CreateRotationX(currentPitch);
        AZ::Quaternion zRotation = AZ::Quaternion::CreateRotationZ(currentRotation);

        entityTransform.SetRotationPartFromQuaternion(zRotation * xRotation);
    }

    if (y != 0.f || x != 0.f)
    {
        auto new_position = entityTransform.GetPosition() + AZ::Quaternion::CreateRotationZ(currentRotation) * AZ::Vector3{x, y, 0.f};
        entityTransform.SetPosition(new_position);
    }

    EBUS_EVENT_ID(GetEntityId(), AZ::TransformBus, SetWorldTM, entityTransform);
}

You can see we clamp the pitch between .10f and 1.05f. We already rotated based on the currentPitch, so that part is fine. It’s really just lines 25 through 31 that is new here.

Compile and roll that beautiful bean footage. We should now have all the rotational control we want for the camera.

Adding Some Editor Values

You can add the rotationScale to the editor reflection if you want. But you can do this yourself. Talk about BOOOOOORRRIIIIING.

Conclusion

This concludes season 3 of RTS(++) camera. In this season, we successfully killed off all the males, so only females remain, which is actually what we all wanted anyways.

Honestly, this basic camera could be used for any top down type game, including something like League of Legends type deal.

We’ll see where this tutorial will take us, as I’d also like to implement selecting things and centering on it, perhaps a mini-map, and stuff like that.

So until next time, keep powering up.

Here is the final result for both the header and source for our BECameraComponent, btw.

BECameraComponent.h

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

namespace BirdEye
{
    class BECameraComponent
        : public AZ::Component,
          public AzFramework::InputChannelEventListener,
          public AZ::TickBus::Handler
    {
    public:
        AZ_COMPONENT(BECameraComponent, "{49a33c8d-4df0-4f27-9cd2-65c2ccd91355}")

        ~BECameraComponent() override{};

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

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

    protected:
        bool OnInputChannelEventFiltered(const AzFramework::InputChannel& inputChannel) override;
        void OnTick(float deltaTime, AZ::ScriptTimePoint time) override;

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

    private:
        bool movingUp = false;
        bool movingDown = false;
        bool movingLeft = false;
        bool movingRight = false;

        bool mouseEdgeUp = false;
        bool mouseEdgeDown = false;
        bool mouseEdgeLeft = false;
        bool mouseEdgeRight = false;

        float movementScale = 5.f;

        float rotationScale = 5.f;
        float currentRotation = 0.f;
        float currentPitch = 0.35f;
        bool rotateLeft = false;
        bool rotateRight = false;

        bool pitchUp = false;
        bool pitchDown = false;
    };
}

BECameraComponent.cpp

#include "StdAfx.h"

#include "BECameraComponent.h"

#include "AzCore/Component/TransformBus.h"
#include "AzCore/Math/Transform.h"
#include "AzCore/Serialization/EditContext.h"
#include "AzFramework/Input/Channels/InputChannel.h"
#include "AzFramework/Input/Devices/Keyboard/InputDeviceKeyboard.h"
#include "AzFramework/Input/Devices/Mouse/InputDeviceMouse.h"
#include "Cry_Math.h"

using namespace BirdEye;
using namespace AzFramework;

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

        if (auto editContext = serializationContext->GetEditContext())
        {
            editContext->Class<BECameraComponent>("BECameraComponent", "Camera component from bird's eye view")
                ->ClassElement(AZ::Edit::ClassElements::EditorData, "")
                ->Attribute(AZ::Edit::Attributes::Category, "BirdEye")
                ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC("Game"))
                ->DataElement(nullptr, &BECameraComponent::movementScale, "Movement scale", "How fast the camera moves")
                ->DataElement(nullptr, &BECameraComponent::rotationScale, "Rotation scale", "How fast the camera rotates");
        }
    }
}

void BECameraComponent::Init()
{
}

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

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

bool BECameraComponent::OnInputChannelEventFiltered(const AzFramework::InputChannel& inputChannel)
{
    OnMouseEvent(inputChannel);
    OnKeyboardEvent(inputChannel);

    return false;
}

void BECameraComponent::OnKeyboardEvent(const InputChannel& inputChannel)
{
    auto input_type = inputChannel.GetInputChannelId();
    if (input_type == InputDeviceKeyboard::Key::AlphanumericW)
    {
        movingUp = !!inputChannel.GetValue();
    }
    else if (input_type == InputDeviceKeyboard::Key::AlphanumericS)
    {
        movingDown = !!inputChannel.GetValue();
    }
    else if (input_type == InputDeviceKeyboard::Key::AlphanumericA)
    {
        movingLeft = !!inputChannel.GetValue();
    }
    else if (input_type == InputDeviceKeyboard::Key::AlphanumericD)
    {
        movingRight = !!inputChannel.GetValue();
    }
    else if (input_type == InputDeviceKeyboard::Key::AlphanumericQ)
    {
        rotateLeft = !!inputChannel.GetValue();
    }
    else if (input_type == InputDeviceKeyboard::Key::AlphanumericE)
    {
        rotateRight = !!inputChannel.GetValue();
    }
    else if (input_type == InputDeviceKeyboard::Key::NavigationPageUp)
    {
        pitchDown = !!inputChannel.GetValue();
    }
    else if (input_type == InputDeviceKeyboard::Key::NavigationPageDown)
    {
        pitchUp = !!inputChannel.GetValue();
    }
}

void BECameraComponent::OnMouseEvent(const InputChannel& inputChannel)
{
    auto input_type = inputChannel.GetInputChannelId();
    if (input_type == InputDeviceMouse::Button::Left || input_type == InputDeviceMouse::Button::Right)
    {
    }
    else if (input_type == InputDeviceMouse::SystemCursorPosition)
    {
        mouseEdgeDown = mouseEdgeLeft = mouseEdgeRight = mouseEdgeUp = false;

        if (auto position_data = inputChannel.GetCustomData<InputChannel::PositionData2D>())
        {
            auto position = position_data->m_normalizedPosition;
            auto x = position.GetX();
            auto y = position.GetY();

            mouseEdgeLeft = x <= .01;
            mouseEdgeRight = x >= .99;
            mouseEdgeUp = y <= .01;
            mouseEdgeDown = y >= .99;
        }
    }
}

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

    float x = 0.f;
    float y = 0.f;

    if (movingUp || mouseEdgeUp)
        y += movementScale * deltaTime;
    if (movingDown || mouseEdgeDown)
        y -= movementScale * deltaTime;
    if (movingRight || mouseEdgeRight)
        x += movementScale * deltaTime;
    if (movingLeft || mouseEdgeLeft)
        x -= movementScale * deltaTime;

    auto oldRotation = currentRotation;

    if (rotateLeft)
        currentRotation -= rotationScale * deltaTime;
    if (rotateRight)
        currentRotation += rotationScale * deltaTime;

    if (pitchUp)
        currentPitch -= rotationScale * deltaTime;
    if (pitchDown)
        currentPitch += rotationScale * deltaTime;

    if (currentPitch < .10f) currentPitch = .10f;
    if (currentPitch > 1.05f) currentPitch = 1.05f;

    if (rotateLeft || rotateRight || pitchUp || pitchDown)
    {
        AZ::Quaternion xRotation = AZ::Quaternion::CreateRotationX(currentPitch);
        AZ::Quaternion zRotation = AZ::Quaternion::CreateRotationZ(currentRotation);

        entityTransform.SetRotationPartFromQuaternion(zRotation * xRotation);
    }

    if (y != 0.f || x != 0.f)
    {
        auto new_position = entityTransform.GetPosition() + AZ::Quaternion::CreateRotationZ(currentRotation) * AZ::Vector3{x, y, 0.f};
        entityTransform.SetPosition(new_position);
    }

    EBUS_EVENT_ID(GetEntityId(), AZ::TransformBus, SetWorldTM, entityTransform);
}

One thought on “RTS C++ Setup – The Inputs Part 3: El Finalmente

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 )

Twitter picture

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

Facebook photo

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

Connecting to %s