This will be the final part of the Input series. If you haven’t already, be sure to check out part 1, part 2, and part 3
This tutorial will focus on the ability to look up and down as the player. It will probably be the most complex part since it is the camera that must rotate up and down, not the player entity.
The Choice
Lumberyard gives us some design choices here.
The most straightforward choice is to somehow get the Entity Id of the Camera into our TutorialSeriesCharacterComponent so that we can modify it’s transform (ie, rotate it) like we do the character entity. This will probably involve passing the entity in as a property from the editor.
I don’t really like this way because it doesn’t work well for multiplayer games (since other player objects wouldn’t have a camera entity.)
The most complex way would be to create a new camera component that handles inputs. This means a new custom component that inherits the InputChannelEventListener interface, just like our character component. Then whenever the player moves the mouse, both the character component and our new camera component would handle the inputs.
This way is alright, but you may have an ordering problem on who processes first, and you would have to track the mouse separately on each component (or create an EBus to share it.)
Another way is to make our own EBus for the camera, and have the character component rotate the camera by sending an event to that bus.
I like this solution for a few reasons, because it makes it so all character movement input is still controlled in one spot. Also, it’s a great way to fit making one of the two types of EBus into this tutorial.
Creating the Camera Component
Its time to create a new component. Lets start by adding the shell of our two components to our components folder. As a note, this is really easy on vscode if you are using my snippets extension. Even if I used visual studio, I’d probably still use vscode for adding files and updating the waf files.
TutCameraComp.h
#pragma once #include "AzCore/Component/Component.h" #include "Buses/CameraRotationRequest.h" namespace TutorialSeries { class TutorialSeriesCameraComponent : public AZ::Component { public: AZ_COMPONENT(TutorialSeriesCameraComponent, "{9d5a7b4c-ee6a-4237-99ad-cdb86bc6a598}") ~TutorialSeriesCameraComponent() override {}; static void Reflect(AZ::ReflectContext* reflection); void Init() override; void Activate() override; void Deactivate() override; }; }
TutCameraComp.cpp
#include "TutCameraComp.h" #include "AzCore/Serialization/EditContext.h" #include "AzCore/Math/Transform.h" #include "AzCore/Component/TransformBus.h" using namespace TutorialSeries; void TutorialSeriesCameraComponent::Reflect(AZ::ReflectContext* reflection) { if (auto serializationContext = azrtti_cast<AZ::SerializeContext*>(reflection)) { serializationContext->Class<TutorialSeriesCameraComponent>() ->Version(1); if (auto editContext = serializationContext->GetEditContext()) { editContext->Class<TutorialSeriesCameraComponent>("TutorialSeriesCameraComponent", "Camera Component to handle Camera stuffs") ->ClassElement(AZ::Edit::ClassElements::EditorData, "") ->Attribute(AZ::Edit::Attributes::Category, "TutorialSeries") ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC("Game")); } } } void TutorialSeriesCameraComponent::Init() { } void TutorialSeriesCameraComponent::Activate() { } void TutorialSeriesCameraComponent::Deactivate() { }
Be sure to update the components section of the waf file to include our two new files
tutorialseries.waf_files
... "Source/Compnonets": [ "Source/Components/TutCharacterComp.h", "Source/Components/TutCharacterComp.cpp", "Source/Components/TutCameraComp.h", "Source/Components/TutCameraComp.cpp" ], ...
And lastly, we need to register the new files in our module source file
TutorialSeriesModule.cpp
m_descriptors.insert(m_descriptors.end(), { TutorialSeriesSystemComponent::CreateDescriptor(), TutorialSeriesCharacterComponent::CreateDescriptor(), TutorialSeriesCameraComponent::CreateDescriptor() });
Be sure to also include TutCameraComp.h at the top.
If you are using visual studio, now is a good time to close it. Run lmbr_waf configure, and reopen visual studio. Then compile to make sure everything is okay.
Run the editor!
Select the camera entity, and add our new component. Your entity setup should now look like this
<img src="https://wordpress.com/1d751ba9-2928-42a4-8c76-51a9e5663435" alt="tut_input_part4_camera_comp.png" class="alignnone size-full wp-image-media-3" />
Export and save, then close the editor.
Creating the EBus
About EBusi
The idea behind Ebus is that of a basic pub/sub system. Someone publishes an event, and someone else subscribes to that event to do something when it's published.
Our input system is an EBus behind the scenes. When input happens, the Device objects "publish" the event with it's InputChannel, and then everything that inherits InputChannelEventListener is subscribed to that event and will run when this happens.
Subscribers are called handlers, and there are a lot of macros to publish events (EBUS_EVENT_ID_RESULT, EBUS_EVENT_ID, EBUS_EVENT, EBUS_EVENT_RESULT, etc).
From what I've seen, there are two main types of EBus. There are component EBus, and there are non-component EBus (or sometimes called system ebus.)
Component EBus will inherit from AZ::ComponentBus. Whenever you call this bus, you will need to provide it some id that goes with it (often EntityId). In fact we are already using one of these to get and set the transform of our player entity via our Character Component.
Component Ebuses are generally used to communicate between components on a given entity. You could also communicate with other entitys' Component EBuses, but you would have to get their entityId to do so.
Non-Component EBus (for lack of better words) does not take an ID to call. These inherit from AZ::EBusTraits, which is actually the base class of ComponentBus as well. They are kinda sorta (but not really) similar to singletons, except you can have multiple handlers for them.
We will be inheriting from the EBusTraits, since we don't want to have to pass the EntityId, and there should generally be one camera for the player at any moment that needs to handle these events.
Creating the EBus
In our gem source, lets create a new folder called Buses, where we can put our ebus. Lets call it CameraRotationRequest.h
CameraRotationRequest.h
#pragma once #include "AzCore/EBus/EBus.h" #include "AzCore/Math/Quaternion.h" namespace TutorialSeries { class CameraRotationRequest : public AZ::EBusTraits { public: virtual void SetCameraRotation(AZ::Quaternion) = 0; }; using CameraRotationRequestBus = AZ::EBus<CameraRotationRequest>; }
The class that inherits EBusTraits/ComponentBus generally contain only pure virtual functions, and don't need a source file. The AZ::EBus<CameraRotationRequestBus> is really what creates all the functionality for the EBus.
Lets add that to our waf file.
tutorialseries.waf_file
... ], "Source/Buses": [ "Source/Buses/CameraRotationRequest.h" ], "Source/Compnonets": [ ...
If in vscode, run the configure task. If in Visual studio, close it, run lmbr_waf configure, and reopen visual studio.
Using our EBus
Now we need to inherit our EBus in our Camera Component.
TutCameraComp.h
#pragma once #include "AzCore/Component/Component.h" #include "Buses/CameraRotationRequest.h" namespace TutorialSeries { class TutorialSeriesCameraComponent : public AZ::Component, public CameraRotationRequestBus::Handler { public: AZ_COMPONENT(TutorialSeriesCameraComponent, "{9d5a7b4c-ee6a-4237-99ad-cdb86bc6a598}"); ~TutorialSeriesCameraComponent() override {}; static void Reflect(AZ::ReflectContext* reflection); void Init() override; void Activate() override; void Deactivate() override; void SetCameraRotation(AZ::Quaternion rotation); }; }
Some important notes here. We are inheriting our buses Handler. That's because TutorialSeriesComeraComponent is now officially a handler of our EBus. So if someone publishes an event for this bus, this class will have it's "SetCameraRotation" function ran.
Now we go back to our Character Component and update the OnTick function to call our EBus to update the camera's rotation.
TutCharacterComp.cpp
void TutorialSeriesCharacterComponent::OnTick(float deltaTime, AZ::ScriptTimePoint time) { AZ::Transform entityTransform; EBUS_EVENT_ID_RESULT(entityTransform, GetEntityId(), AZ::TransformBus, GetWorldTM); auto x_rotation = GetCurrentXOrientation(); auto z_rotation = GetCurrentZOrientation(); entityTransform.SetRotationPartFromQuaternion(z_rotation); EBUS_EVENT_ID(GetEntityId(), AZ::TransformBus, SetWorldTM, entityTransform); EBUS_EVENT(CameraRotationRequestBus, SetCameraRotation, z_rotation * x_rotation); auto desiredVelocity = AZ::Vector3::CreateZero(); if (movingForward || movingBack || strafingLeft || strafingRight) { HandleForwardBackwardMovement(desiredVelocity); HandleStrafing(desiredVelocity); desiredVelocity = z_rotation * desiredVelocity; } EBUS_EVENT_ID(GetEntityId(), LmbrCentral::CryCharacterPhysicsRequestBus, RequestVelocity, desiredVelocity, 0); } const AZ::Quaternion TutorialSeriesCharacterComponent::GetCurrentZOrientation() { auto z_rotation = AZ::Quaternion::CreateRotationZ(m_mouseChangeAggregate.GetX() * RotationSpeed); return z_rotation; } const AZ::Quaternion TutorialSeriesCharacterComponent::GetCurrentXOrientation() { auto x_rotation = AZ::Quaternion::CreateRotationX(m_mouseChangeAggregate.GetY() * RotationSpeed); return x_rotation; }
So we added two helper functions GetCurrentZOrientation
and GetCurrentXOrientation
to deal with the mouse being moved side to side and up and down, respectively. We got rid of the GetCurrentOrientation
helper function. So be sure to update the header with these changes.
On line 10 we set the current transform of our player entity, like before, only using the z axis rotation. But for the camera, we need to turn based on both the z axis and the x axis.
Be careful here, because z_rotation * x_rotation is an entirely different result than x_rotation * z_rotation. If you rotate it by the x and then by the z axis, you will get the wrong result. Try it out with an object around you.
Compile and run. You should now be able to look around and walk any direction!
We still have a problem though. You can look up so much you break your next looking behind you. It’s enough to make our player nauseous.
TutCharacterComp.cpp
void TutorialSeriesCharacterComponent::TrackMouseMovement(const InputChannel::PositionData2D *position_data) { auto deltaMousePosition = m_lastMousePosition - position_data->m_normalizedPosition; auto new_y = (m_mouseChangeAggregate + deltaMousePosition).GetY() * RotationSpeed; if (new_y > 1.8f || new_y < -1.8f) { m_mouseChangeAggregate.SetX(m_mouseChangeAggregate.GetX() + deltaMousePosition.GetX()); } else { m_mouseChangeAggregate += deltaMousePosition; } }
This will limit the amount they can look up and down. 1.8 is just a value I got to from trial and error.
The Final Everything
This concludes this tutorial. We will have a tutorial on raycasting which will be a good follow up to this.
Here are all the final files to ensure you have everything correct.
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); void PerformRotation(const AzFramework::InputChannel& inputChannel); void TrackMouseMovement(const AzFramework::InputChannel::PositionData2D* position_data); void CenterCursorPosition(); const AZ::Quaternion GetCurrentZOrientation(); const AZ::Quaternion GetCurrentXOrientation(); AZ::Vector2 m_lastMousePosition{.5f, .5f}; AZ::Vector2 m_mouseChangeAggregate{0, 0}; float RotationSpeed = 5.f; float MovementScale = 5.0f; bool movingForward = false; bool movingBack = false; bool strafingLeft = false; bool strafingRight = false; }; }
TutCharacterComp.cpp
#include "StdAfx.h" #include "Components/TutCharacterComp.h" #include "Buses/CameraRotationRequest.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" 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) ->Field("Rotation Speed", &TutorialSeriesCharacterComponent::RotationSpeed); 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") ->DataElement(nullptr, &TutorialSeriesCharacterComponent::RotationSpeed, "Rotation Speed", "The speed multiplier to apply to mouse rotation"); } } } 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 x_rotation = GetCurrentXOrientation(); auto z_rotation = GetCurrentZOrientation(); entityTransform.SetRotationPartFromQuaternion(z_rotation); EBUS_EVENT_ID(GetEntityId(), AZ::TransformBus, SetWorldTM, entityTransform); EBUS_EVENT(CameraRotationRequestBus, SetCameraRotation, z_rotation * x_rotation); auto desiredVelocity = AZ::Vector3::CreateZero(); if (movingForward || movingBack || strafingLeft || strafingRight) { HandleForwardBackwardMovement(desiredVelocity); HandleStrafing(desiredVelocity); desiredVelocity = z_rotation * desiredVelocity; } EBUS_EVENT_ID(GetEntityId(), LmbrCentral::CryCharacterPhysicsRequestBus, RequestVelocity, desiredVelocity, 0); } const AZ::Quaternion TutorialSeriesCharacterComponent::GetCurrentZOrientation() { auto z_rotation = AZ::Quaternion::CreateRotationZ(m_mouseChangeAggregate.GetX() * RotationSpeed); return z_rotation; } const AZ::Quaternion TutorialSeriesCharacterComponent::GetCurrentXOrientation() { auto x_rotation = AZ::Quaternion::CreateRotationX(m_mouseChangeAggregate.GetY() * RotationSpeed); return x_rotation; } 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) { auto input_type = inputChannel.GetInputChannelId(); if (input_type == InputDeviceMouse::SystemCursorPosition) { PerformRotation(inputChannel); } } void TutorialSeriesCharacterComponent::CenterCursorPosition() { EBUS_EVENT(InputSystemCursorRequestBus, SetSystemCursorPositionNormalized, AZ::Vector2{.5f, .5f}); m_lastMousePosition = AZ::Vector2{.5f, .5f}; } void TutorialSeriesCharacterComponent::PerformRotation(const InputChannel &inputChannel) { auto position_data = inputChannel.GetCustomData<InputChannel::PositionData2D>(); TrackMouseMovement(position_data); CenterCursorPosition(); } void TutorialSeriesCharacterComponent::TrackMouseMovement(const InputChannel::PositionData2D *position_data) { auto deltaMousePosition = m_lastMousePosition - position_data->m_normalizedPosition; auto new_y = (m_mouseChangeAggregate + deltaMousePosition).GetY() * RotationSpeed; if (new_y > 1.8f || new_y Class<TutorialSeriesCameraComponent>() ->Version(1); if (auto editContext = serializationContext->GetEditContext()) { editContext->Class<TutorialSeriesCameraComponent>("TutorialSeriesCameraComponent", "Camera Component to handle Camera stuffs") ->ClassElement(AZ::Edit::ClassElements::EditorData, "") ->Attribute(AZ::Edit::Attributes::Category, "TutorialSeries") ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC("Game")); } } } void TutorialSeriesCameraComponent::Init() { } void TutorialSeriesCameraComponent::Activate() { CameraRotationRequestBus::Handler::BusConnect(); } void TutorialSeriesCameraComponent::Deactivate() { CameraRotationRequestBus::Handler::BusDisconnect(); } void TutorialSeriesCameraComponent::SetCameraRotation(AZ::Quaternion rotation) { AZ::Transform entityTransform; EBUS_EVENT_ID_RESULT(entityTransform, GetEntityId(), AZ::TransformBus, GetWorldTM); entityTransform.SetRotationPartFromQuaternion(rotation); EBUS_EVENT_ID(GetEntityId(), AZ::TransformBus, SetWorldTM, entityTransform); }
CameraRotationRequest.h
#pragma once #include "AzCore/EBus/EBus.h" #include "AzCore/Math/Quaternion.h" namespace TutorialSeries { class CameraRotationRequest : public AZ::EBusTraits { public: virtual void SetCameraRotation(AZ::Quaternion) = 0; }; using CameraRotationRequestBus = AZ::EBus<CameraRotationRequest>; }
TutorialSeriesModule.cpp
#include "StdAfx.h" #include "platform_impl.h" #include "AzCore/Memory/SystemAllocator.h" #include "TutorialSeriesSystemComponent.h" #include "Components/TutCharacterComp.h" #include "Components/TutCameraComp.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() { m_descriptors.insert(m_descriptors.end(), { TutorialSeriesSystemComponent::CreateDescriptor(), TutorialSeriesCharacterComponent::CreateDescriptor(), TutorialSeriesCameraComponent::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)
Note, your AZ_DECLARE_MODULE_CLASS will have a different guid than mine. That's fine.
tutorialseries.waf_files
{ "none": { "Source": [ "Source/StdAfx.cpp", "Source/StdAfx.h" ] }, "auto": { "Include": [ "Include/TutorialSeries/TutorialSeriesBus.h" ], "Source": [ "Source/TutorialSeriesModule.cpp", "Source/TutorialSeriesSystemComponent.cpp", "Source/TutorialSeriesSystemComponent.h" ], "Source/Buses": [ "Source/Buses/CameraRotationRequest.h" ], "Source/Compnonets": [ "Source/Components/TutCharacterComp.h", "Source/Components/TutCharacterComp.cpp", "Source/Components/TutCameraComp.h", "Source/Components/TutCameraComp.cpp" ], "Source/Core": [ "Source/Core/EditorGame.cpp", "Source/Core/EditorGame.h", "Source/Core/TutorialSeriesGame.cpp", "Source/Core/TutorialSeriesGame.h", "Source/Core/TutorialSeriesGameRules.cpp", "Source/Core/TutorialSeriesGameRules.h" ], "Source/Game": [ "Source/Game/Actor.cpp", "Source/Game/Actor.h" ], "Source/System": [ "Source/System/GameStartup.cpp", "Source/System/GameStartup.h" ] } }
If you have any questions, you can reach out to me on the lumberyard slack. Feel free to leave a comment or email me at lumberyard.tutorials@gmail.com
Written by Greg Horvay
Hello, great tutorials, I have a problem !! Up to now everything works fine (Vers.1.16) when I go into game in the “Launcher” engine. In the editor unfortunately the mouse hangs in the middle and I can not move it anymore, I managed with the keys to go ingame from the editor and it works. some idea? Thank you
LikeLike
Hi, Great tutorial, i have a problem! When open the editor!
The mouse cursor stays locked in the middle and I can not move it! Some idea?
LikeLike