RTS C++ Setup – The Inputs Part 3.2

So apparently in my previous tutorial, I said that we would implement zooming with the RTS camera. Whoops! Knew I forgot something.

So I called part 3 the finale, so obviously I’ll just have to call this one 3.2. Why not 3.1 you might ask? Apparently, it’s cool these days to skip versions.

In this tutorial addendum, we’ll add the zoom feature. The plan is to use the mouse wheel, which will be interesting since the wheel is not a buttons which is either pressed or not. Where as the button is a true/false (pressed/not-pressed), the scroll wheel is merely an amount scrolled.

The Choice

Up until now, we have been transforming the tracking object entity that our camera is looking at. However, this time, we are going to change the distance that the camera entity is to the tracking object entity. That means changing the transform of the camera, not the tracking object entity.

1. Get the entity ID of the camera, and change it’s transform with the rest of our camera manipulation logic. This involves creating a new bus that we provide the camera’s entity id, and a new component that we will put on the camera entity itself to implement that bus.

I actually like this method because it allows us to keep all the main camera logic in the one component that handles input. It helps keep things simple.

2. Add a method on the camera to adjust it’s zoom. This also involves a bus and a new component on the camera entity that implements that bus. Then when the tracking object component (BECameraComponent) wants to change the zoom, it calls the bus and the new component on the camera entity will change it’s transform.

I also like this method, because it allows the camera entity itself to change it’s own transform, and we don’t have to deal with one component updating multiple entities’ transforms.

Well shoot, two good methods for different reasons, which do we do?

LONG DRAMATIC PAUSE!!!!!!

I guess we’ll have to implement them both. You can pick which one you keep. (Also, Angelica Hale totally should have won.)

Method 2

Create the Bus

We are going to start with Method 2, because as of this time writing the post, I already implemented it.

So we need to create a folder in our source called “buses” and a new header file for our bus, which we can call CameraController.h

CameraController.h:

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

namespace BirdEye
{
    class CameraController
        : public AZ::EBusTraits
    {
    public:
        virtual void SetZoom(float change_amount) = 0;
    };

    using CameraControllerBus = AZ::EBus<CameraController>;
}

As always, if you use Visual Code to add files, some derpderp wrote this Lumberyard Snippets extension that can help create these files a bit faster. But you are probably copying and pasting the above, anyway.

Create the Camera entity’s Component

Next we need a new component that we will attach to the camera entity itself, which both methods require. This component is pretty simple, so here is the header and source for it:

BECameraControllerComponent.h:

#pragma once
#include "AzCore/Component/Component.h"

#include "Buses/CameraController.h"

namespace BirdEye
{
    class BECameraControllerComponent
        : public AZ::Component
        , protected CameraControllerBus::Handler
    {
    public:
        AZ_COMPONENT(BECameraControllerComponent, "{e95a671a-14e5-4137-9776-ef55dde86b03}")

        ~BECameraControllerComponent() override {};

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

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

    protected:
        void SetZoom(float change_amount) override;

    private:
        float maxZoom = 1.f;
        float minZoom = 200.f;
    };
}

BECameraControllerComponent.cpp

#include "StdAfx.h"

#include "BECameraControllerComponent.h"

#include "AzCore/Component/TransformBus.h"
#include "AzCore/Math/Transform.h"
#include "AzCore/Serialization/EditContext.h"

using namespace BirdEye;

void BECameraControllerComponent::Reflect(AZ::ReflectContext* reflection)
{
    if (auto serializationContext = azrtti_cast<AZ::SerializeContext*>(reflection))
    {
        serializationContext->Class<BECameraControllerComponent>()
            ->Version(1)
            ->Field("Min Zoom", &BECameraControllerComponent::minZoom)
            ->Field("Max Zoom", &BECameraControllerComponent::maxZoom);

        if (auto editContext = serializationContext->GetEditContext())
        {
            editContext->Class<BECameraControllerComponent>("BECameraControllerComponent", "Controller for the camera. This goes on the actual camera.")
                ->ClassElement(AZ::Edit::ClassElements::EditorData, "")
                ->Attribute(AZ::Edit::Attributes::Category, "BirdEye")
                ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC("Game"))
                ->DataElement(nullptr, &BECameraControllerComponent::minZoom, "Min Zoom", "The minimum zoom possible (how far back can it go?)")
                ->DataElement(nullptr, &BECameraControllerComponent::maxZoom, "Max Zoom", "The maximum zoom possible (how far forward can it go?)");
        }
    }
}

void BECameraControllerComponent::Init()
{
}

void BECameraControllerComponent::Activate()
{
    CameraControllerBus::Handler::BusConnect();
}

void BECameraControllerComponent::Deactivate()
{
    CameraControllerBus::Handler::BusDisconnect();
}

void BECameraControllerComponent::SetZoom(float change_amount)
{
    AZ::Transform entityTransform;
    EBUS_EVENT_ID_RESULT(entityTransform, GetEntityId(), AZ::TransformBus, GetLocalTM);

    const auto& old_position = entityTransform.GetPosition();

    auto newZ = old_position.GetZ() + change_amount;
    auto new_position = AZ::Vector3{0.f, 0.f, newZ.GetClamp(maxZoom, minZoom)};

    entityTransform.SetPosition(new_position);
    EBUS_EVENT_ID(GetEntityId(), AZ::TransformBus, SetLocalTM, entityTransform);
}

We are allowing the maxZoom and minZoom to be passed to the editor to modify. As you can see, because of our entity setup, we really only care about the Z value of the position, so we only set that. Also note that we need to clamp the zoom to what is passed in from the editor.

One last thing to note is that this is the first time in the tutorials you will have seen SetLocalTM and GetLocalTM. These methods are relative to the parent entity (the TrackingObject entity), rather than the absolute (world) position.

Oh yeah, Those waf_files

I always forget those waf files. If you too have these type of problems, consider drugs. If that doesn’t work, maybe try out my post on it: Why Won’t My Components Show in the Damn Editor!!!11one

Anyways, be sure to add these to the waf_file:

birdeye.waf_files:

...
        "Source/Buses": [
            "Source/Buses/CameraController.h"
        ],
        "Source/Compnonets": [
            "Source/Components/BECameraComponent.h",
            "Source/Components/BECameraComponent.cpp",
            "Source/Components/BECameraControllerComponent.h",
            "Source/Components/BECameraControllerComponent.cpp"
        ],
...

Finally, we do do the Zoom in our First Component

Yes, that’s right. I spent a lot of time figuring out how to say “do do” in this confusing header. Important.

So for BECameraComponent, we first need to add a few more private variables.

BECameraComponent.h:

        float zoomDelta = 0.f;
        float zoomScale = 1.f;

zoomScale will determine how fast we zoom in or out. The zoomDelta will be tracking the amount we need to adjust the zoom for any given tick. It’ll be reset back to 0 every time we tick and change zoom.

Next we need to add the following to the end of the OnTick function. Also be sure to #include "Buses/CameraController.h".

BECameraComponents.cpp:

if (zoomDelta != 0)
{
    EBUS_EVENT(CameraControllerBus, SetZoom, zoomDelta * zoomScale * deltaTime);
    zoomDelta = 0.f;
}

We also need to add the zoomScale to the reflect function, so that we can modify it in the editor. But I’ll leave that for you to do. Go for it! (Connect four).

Add our New Component In the Editor

Compile and run the editor. Click the child camera entity. The components should now look like this:
tut_rts32_cameracomps
And the tracking object like this:
tut_rts32_trackingcomps

Save, export, close and run the launcher (or run it in the editor.) Zoom should now be working

Method 2 COMPLETED! BOOM! ZOOM!

Method 1

Ok, time to get super Saiyan level lazy on the next method

Create the Bus

CameraController.h:

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

namespace BirdEye
{
    class CameraController
        : public AZ::EBusTraits
    {
    public:
        virtual const AZ::EntityId GetCameraEntityId() = 0;
    };

    using CameraControllerBus = AZ::EBus<CameraController>;
}

Create the Camera entity’s Component

BECameraControllerComponent.h:

#pragma once
#include "AzCore/Component/Component.h"

#include "Buses/CameraController.h"

namespace BirdEye
{
    class BECameraControllerComponent
        : public AZ::Component
        , protected CameraControllerBus::Handler
    {
    public:
        AZ_COMPONENT(BECameraControllerComponent, "{e95a671a-14e5-4137-9776-ef55dde86b03}")

        ~BECameraControllerComponent() override {};

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

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

    protected:
        const AZ::EntityId GetCameraEntityId() override;
    };
}

BECameraControllerComponent.cpp:

#include "StdAfx.h"

#include "BECameraControllerComponent.h"

#include "AzCore/Component/TransformBus.h"
#include "AzCore/Math/Transform.h"
#include "AzCore/Serialization/EditContext.h"

using namespace BirdEye;

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

        if (auto editContext = serializationContext->GetEditContext())
        {
            editContext->Class<BECameraControllerComponent>("BECameraControllerComponent", "Controller for the camera. This goes on the actual camera.")
                ->ClassElement(AZ::Edit::ClassElements::EditorData, "")
                ->Attribute(AZ::Edit::Attributes::Category, "BirdEye")
                ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC("Game"));
        }
    }
}

void BECameraControllerComponent::Init()
{
}

void BECameraControllerComponent::Activate()
{
    CameraControllerBus::Handler::BusConnect();
}

void BECameraControllerComponent::Deactivate()
{
    CameraControllerBus::Handler::BusDisconnect();
}

const AZ::EntityId BECameraControllerComponent::GetCameraEntityId()
{
    return GetEntityId();
}

Oh yeah, Those waf_files

birdeye.waf_files:

...
        "Source/Buses": [
            "Source/Buses/CameraController.h"
        ],
        "Source/Compnonets": [
            "Source/Components/BECameraComponent.h",
            "Source/Components/BECameraComponent.cpp",
            "Source/Components/BECameraControllerComponent.h",
            "Source/Components/BECameraControllerComponent.cpp"
        ],
...

Finally, we do do the Zoom in our First Component

BECameraComponent.h:

        float zoomDelta = 0.f;
        float zoomScale = 1.f;
        float maxZoom = 1.f;
        float minZoom = 200.f;

And then at the end of the OnTick function:
BECameraComponent.cpp:

if (zoomDelta != 0)
{
    AZ::EntityId camera_entity;
    EBUS_EVENT_RESULT(camera_entity, CameraControllerBus, GetCameraEntityId);

    AZ::Transform entityTransform;
    EBUS_EVENT_ID_RESULT(entityTransform, camera_entity, AZ::TransformBus, GetLocalTM);

    const auto& old_position = entityTransform.GetPosition();

    auto newZ = old_position.GetZ() + zoomDelta * zoomScale * deltaTime;
    auto new_position = AZ::Vector3{0.f, 0.f, newZ.GetClamp(maxZoom, minZoom)};

    entityTransform.SetPosition(new_position);
    EBUS_EVENT_ID(camera_entity, AZ::TransformBus, SetLocalTM, entityTransform);

    zoomDelta = 0.f;
}

Also you’ll want to add max/minZoom and zoomScale to the reflect function, so you can modify them in the editor.

Editor stuff

Compile and run the editor

tut_rts32_trackingcompstut_rts32_cameracomps

Save, export, run.

Method 2 COMPLETED! BOOM! ZOOM!

Conclusion

I think I’ll move forward using Method 1 (where we merely get the camera entity id.) I think both are fine and valid. Probably there should be heated arguments over which is better, until one side calls the other side Hitler. Whoever invokes Hitler first obviously wins out.

So this is the for-realzies end to the RTS input series. There will be future tutorials to go over other aspects of RTS or perhaps other types of top down games.

Until then, keep juggling the cats. (Or not)

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