Raycasting with Lumberyard

One of the fundamentals of any FPS game is raycasting. It’s what tells us what the player hit when they click somewhere.

Before you start this tutorial, you should really have a character that can move around the level. If you need help with that, you can see our other tutorial to get that going. Also, I use the Gem that I created in the creating a gem tutorial.

important: There is a better method to do a raycast using a Lumberyard Ebus. I would suggest finishing this tutorial, and then updating it to use the Lumberyard Ebus in part 2.

The Choices

Choice 1: Put the raycast logic in the character component.

If you followed the tutorials before this one, then you may be noticing that our character component is getting quite large fast. If we keep putting all player logic in there, it will be unmanageable soon.

Choice 2: Put the raycast logic in a helper class

This isn’t bad. We could then make an instance of this class on the character. However, I still don’t like that our character has to “care” about it. It would be nice if it could just request it, and that was it.

Choice 3: The EBus!

We may as well use what Lumberyard is good at, and utilize the EBus. This way the character can just call the bus, and get the results, and not care about the implementation in any way. This also helps for testing by allowing us to completely segregate our character comp unit test from the raycast unit test.

One note: Choice 3 also uses Choice 2, but we don’t have to instantiate it in the character component anymore.

Gem, so many Gems!

What we will be creating is a wrapper around some CryEngine stuff since Lumberyard hasn’t touched physics too much. Therefore, it makes sense for us to put this physics wrapper stuff in a gem.

If you are unfamiliar with creating gems, I have a quick 5 minute tutorial on it here!

At this point, I’ll assume you have a gem called PhysicsWrapper, as created in the tutorial above.

Create the EBus Entry Point

So lets create the ebus entry point that our game gem will call. For now, we can just use the main ebus for our physics wrapper located at Code\Include\PhysicsWrapper\PhysicsWrapperBus.h in our PhysicsWrapper gem.

PhysicsWrapperBus.h:

#pragma once

#include <AzCore/EBus/EBus.h>
#include "AzCore/Math/Vector3.h"

namespace PhysicsWrapper
{
    struct Hit
    {
        AZ::EntityId entity;
        bool hit = false;
        AZ::Vector3 position{ 0, 0, 0 };
        AZ::Vector3 normal{ 0, 0,0 };
    };

    class PhysicsWrapperRequests
        : public AZ::EBusTraits
    {
    public:
        static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single;
        static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;

        virtual Hit PerformRayCastWithParam(float distance, int colisionFlags) = 0;
        virtual Hit PerformRayCast() = 0;

        // Public functions
    };

    using PhysicsWrapperRequestBus = AZ::EBus<PhysicsWrapperRequests>;
} // namespace PhysicsWrapper

Half of this code was generated for us by Lumberyard. We are only adding the Hit struct, and the pure virtual PerformRayCastWithParam/PerformRayCast functions.

When I first did this, I tried to make the functions have the same name with different parameters. However, the EBus system does not really support function overloading. While you can technically get it to work, you can’t use the macros and it gets very ugly and hard to read.

Therefore, we make two functions. One with defaults and the other with parameters.

Also, our struct “Hit” will just be a more simplified versions of the CryEngine’s hit object.

Implementing our EBus

Lumberyard also made a system component for us called PhysicsWrapperSystemComponent under the Code\Source folder. We will use that to implement our ebus.

PhysicsWrapperSystemComponent.h:

#pragma once

#include "AzCore/Component/Component.h"

#include "PhysicsWrapper/PhysicsWrapperBus.h"

namespace PhysicsWrapper
{
    class PhysicsWrapperSystemComponent
        : public AZ::Component
        , protected PhysicsWrapperRequestBus::Handler
    {
    public:
        AZ_COMPONENT(PhysicsWrapperSystemComponent, "{85082C5C-C2D7-49C4-8C6A-6CD00AFF0760}");

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

        static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided);
        static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible);
        static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required);
        static void GetDependentServices(AZ::ComponentDescriptor::DependencyArrayType& dependent);

    protected:
        ////////////////////////////////////////////////////////////////////////
        // PhysicsWrapperRequestBus interface implementation
        Hit PerformRayCastWithParam(float distance, int colisionFlags);
        Hit PerformRayCast();
        ////////////////////////////////////////////////////////////////////////

        ////////////////////////////////////////////////////////////////////////
        // AZ::Component interface implementation
        void Init() override;
        void Activate() override;
        void Deactivate() override;
        ////////////////////////////////////////////////////////////////////////
    };
}

Lets talk about system components in general. System components are somewhat like singletons, which instantiate without any link to a real entity.

However, when you have multiple, the question becomes, which gets instantiated first. That’s where GetProvidedServices, GetRequiredServices, and GetDependentServices come into play. These provide a means to determine which system component comes first and who depends on whom. Amazon has documentation on this, which you can check out if you need it later.

So you can see that this system component is already inheriting our PhysicsWrapperRequestBus. Lumberyard’s gem creator did this for us. We added line 26 and 27.

Now lets go define their source.

PhysicsWrapperSystemComponent.cpp:

#include "StdAfx.h"

#include "AzCore/Serialization/SerializeContext.h"
#include "AzCore/Serialization/EditContext.h"
#include "AzCore/std/smart_ptr/unique_ptr.h"
#include "physinterface.h"
#include "IPhysics.h"
#include "MathConversion.h"
#include "CryAction.h"
#include "Cry_Camera.h"

#include "PhysicsWrapperSystemComponent.h"

namespace PhysicsWrapper
{
void PhysicsWrapperSystemComponent::Reflect(AZ::ReflectContext *context)
{
    if (AZ::SerializeContext *serialize = azrtti_cast<AZ::SerializeContext *>(context))
    {
        serialize->Class<PhysicsWrapperSystemComponent, AZ::Component>()
            ->Version(0)
            ->SerializerForEmptyClass();

        if (AZ::EditContext *ec = serialize->GetEditContext())
        {
            ec->Class<PhysicsWrapperSystemComponent>("PhysicsWrapper", "[Description of functionality provided by this System Component]")
                ->ClassElement(AZ::Edit::ClassElements::EditorData, "")
                ->Attribute(AZ::Edit::Attributes::Category, "PhysicsWrapper")
                ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC("System"))
                ->Attribute(AZ::Edit::Attributes::AutoExpand, true);
        }
    }
}

Hit PhysicsWrapperSystemComponent::PerformRayCastWithParam(float distance, int colisionFlags)
{
    auto hit = AZStd::make_unique<ray_hit>();

    auto &cam = GetISystem()->GetViewCamera();

    auto direction = cam.GetViewdir();
    auto start = cam.GetPosition() + direction;

    auto pWorld = gEnv->pPhysicalWorld;
    auto numHits = pWorld->RayWorldIntersection(start, direction * distance, ent_all, colisionFlags, hit.get(), 1);

    Hit phit;

    if (numHits > 0)
    {
        phit.position = LYVec3ToAZVec3(hit->pt);
        phit.normal = LYVec3ToAZVec3(hit->n);

        if (hit->pCollider)
        {
            phit.hit = true;
            phit.entity = static_cast<AZ::EntityId>(hit->pCollider->GetForeignData(PHYS_FOREIGN_ID_COMPONENT_ENTITY));
        }
    }

    return phit;
}

Hit PhysicsWrapperSystemComponent::PerformRayCast()
{
    return PerformRayCastWithParam(100.f, rwi_stop_at_pierceable | rwi_colltype_any);
}

void PhysicsWrapperSystemComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType &provided)
{
    provided.push_back(AZ_CRC("PhysicsWrapperService"));
}

void PhysicsWrapperSystemComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType &incompatible)
{
    incompatible.push_back(AZ_CRC("PhysicsWrapperService"));
}

void PhysicsWrapperSystemComponent::GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType &required)
{
    (void)required;
}

void PhysicsWrapperSystemComponent::GetDependentServices(AZ::ComponentDescriptor::DependencyArrayType &dependent)
{
    (void)dependent;
}

void PhysicsWrapperSystemComponent::Init()
{
}

void PhysicsWrapperSystemComponent::Activate()
{
    PhysicsWrapperRequestBus::Handler::BusConnect();
}

void PhysicsWrapperSystemComponent::Deactivate()
{
    PhysicsWrapperRequestBus::Handler::BusDisconnect();
}
}

Other than some include headers, our code starts on line 35. The CryEngine’s raycast function is basically RayWorldIntersection which has lots of overloads.

To get the direction the player is facing in terms CryEngine understands, we are using the CryEngine camera object, which comes from the gEnv (which is what the GetISystem function uses.)

Other than that, we call the raycast with the parameters we got from the camera, and fill in our Hit object if it returns any hits.

Lets compile to make sure there are no errors.

Calling the RayCast from our Game Gem

Be sure to set up this new PhysicsWrapper gem as a dependency of our game gem. See the linked tutorial about creating a gem above.

Lets make it so when the user click the left mouse button it does the ray cast.

To do this, lets change the OnMouseEvent function to be like this:

TutCharacterComp.cpp:

void TutorialSeriesCharacterComponent::OnMouseEvent(const InputChannel &inputChannel)
{
    auto input_type = inputChannel.GetInputChannelId();
    if (input_type == InputDeviceMouse::SystemCursorPosition)
    {
        PerformRotation(inputChannel);
    }
    else if (input_type == InputDeviceMouse::Button::Left && !!inputChannel.GetValue())
    {
        Hit hit;
        EBUS_EVENT_RESULT(hit, PhysicsWrapperRequestBus, PerformRayCast);
    }
}

Also be sure to include it #include "PhysicsWrapper/PhysicsWrapperBus.h"

Since I use vscode, I had to add the new include for it to work, but the waf system probably does it for you if you use Visual studio.

For vscode users, add: "${workspaceRoot}/../../../Gems/PhysicsWrapper/Code/include", to your IncludePath.

So you can see how we call our new EBus. Pretty simple.

Compile and run.

Now when you left click… nothing happens! Woohoo! Actually it does the RayCast but we didn’t do anything to show it. If you put a breakpoint on EBUS_EVENT_RESULT(hit, PhysicsWrapperRequestBus, PerformRayCast); and click at the ground, it will return results

Adding a Debug Line Where we Cast

So I looked around in the engine for awhile and finally came across gEnv-&gt;pGame-&gt;GetIGameFramework()-&gt;GetIPersistentDebug() which we can use for our debug line.

PhysicsWrapperSystemComponent.cpp:

Hit PhysicsWrapperSystemComponent::PerformRayCastWithParam(float distance, int colisionFlags)
{
    auto hit = AZStd::make_unique<ray_hit>();

    auto &cam = GetISystem()->GetViewCamera();

    auto direction = cam.GetViewdir();
    auto start = cam.GetPosition() + direction;

    auto pWorld = gEnv->pPhysicalWorld;
    auto numHits = pWorld->RayWorldIntersection(start, direction * distance, ent_all, colisionFlags, hit.get(), 1);

    Hit phit;

    if (numHits > 0)
    {
        phit.position = LYVec3ToAZVec3(hit->pt);
        phit.normal = LYVec3ToAZVec3(hit->n);

        if (hit->pCollider)
        {
            phit.hit = true;
            phit.entity = static_cast<AZ::EntityId>(hit->pCollider->GetForeignData(PHYS_FOREIGN_ID_COMPONENT_ENTITY));
        }
    }

#if !defined(_RELEASE)
    if (auto *pPersistentDebug = gEnv->pGame->GetIGameFramework()->GetIPersistentDebug())
    {
        const ColorF green(0.000f, 1.000f, 0.000f);
        const ColorF red(1.000f, 0.000f, 0.000f);

        pPersistentDebug->Begin("FG_Line", true);

        auto end = start + direction * distance;

        if (!!phit.hit && phit.entity.IsValid())
        {
            pPersistentDebug->AddLine(start, end, green, 500);
        }
        else
        {
            pPersistentDebug->AddLine(start, end, red, 500);
        }
    }
#endif

    return phit;
}

We use the precompiler to only run this when not in release mode.

If we hit something, it will be green, otherwise we’ll use red. What we have right now is a start, a direction, and a distance, and we need to get that into a start and end. So we have to do some math to get the “end” which you can see.

When we call this debug tool, you’ll see some options: pPersistentDebug-&gt;Begin("FG_Line", true);

FG_Line is just the type of thing we are trying. The second parameter determines if the draw buffer is cleared. If you want to see the line stick around when clicking, make it false. If you just want to see one line at a time, make it true so it clears the old lines.

Compile and run it.

Now when you click, you still won’t see anything :(:(:(. This is actually because you are staying right at the line so it’s just a point. If you move to the side, you should see a green or red line.

Conclusion

Unfortunately for now, when we deal with physics, using the EBUS and simple design of Lumberyard isn’t really an option. So going forward, we will probably put all physics related stuff in this PhysicsWrapper Gem to make it more like the rest of the Lumberyard’ish type code.

Don’t be afraid to break anything out into a Gem. Even Lumberyard is actively tearing things apart and putting them in module Gems.

In the next tutorial, we will try to interact with something using the RayCast we built.

One thought on “Raycasting with Lumberyard

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 )

Google+ photo

You are commenting using your Google+ 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