A rock-solid Grabber class

I’ve just finished the section and, for inspiration and educational purposes, I thought I’d post my Grabber actor-component implementation for others to read, along with some commentary. I have a different style to Ben and think it might be enlightening to see the same stuff implemented slightly differently. (Arguing whose is superior is outside the scope of this thread.) (Comments mostly removed in favour of inline discussions in the thread.)

The constructor. Nothing to say, here.

UGrabber::UGrabber()
{
    bWantsBeginPlay = true;
    PrimaryComponentTick.bCanEverTick = true;
}

BeginPlay() is implemented and called because bWantsBeginPlay is set to true, above.

void UGrabber::BeginPlay()
{
    Super::BeginPlay();

I immediately stop the component from ticking because it doesn’t need to tick until something has actually been grabbed and ticking is expensive. Don’t tick unless you really have to. You can see, further on, how I toggle ticking on and off with SetTickFunctionEnable(). (I tried to use PrimaryComponentTick.bStartWithTickEnabled in the ctor. to have the component’s ticking disabled by default but I couldn’t seem to make it work as expected.)

    PrimaryComponentTick.SetTickFunctionEnable(false);

Now I store the world-pointer so I don’t need to call GetWorld() over and over. The underscore naming convention denotes that _world is a private data member (or field) of the current class. It is defined in the header (listed below) and always in scope in non-static member functions of Grabber.

    _world = GetWorld();

Next, I find the owner of the Grabber and use the templated IsA<T>() to make sure that the actor-component is actually attached to an APawn. It doesn’t make sense to have a Grabber on something that is not a pawn and having it cast to an APawn* will be useful, further down.

    AActor* owner = GetOwner();
    if (owner->IsA<APawn>())
    {
        _pawn = Cast<APawn>(owner);
    }
    else UE_LOG(LogTemp, Error, TEXT("Grabber components may only be attached to Pawns"));

I find the UInputComponent of the pawn and bind to the Grab action as normal. In real-world code, I’d probably pull these BindAction() calls into another method. I don’t bother logging if the input component is not found because it’s inherited from the default pawn and not something the developer has to add.

    UInputComponent* inputComponent = _pawn->FindComponentByClass<UInputComponent>();
    if (nullptr != inputComponent)
    {
        inputComponent->BindAction("Grab", IE_Pressed, this, &UGrabber::GrabPressed);
        inputComponent->BindAction("Grab", IE_Released, this, &UGrabber::GrabReleased);
    }

Finally, I find the UPhysicsHandleComponent and cache its memory address for later, bringing BeginPlay() to an end.

    _physicsHandle = _pawn->FindComponentByClass<UPhysicsHandleComponent>();
    if (nullptr == _physicsHandle) UE_LOG(LogTemp, Error, TEXT("%s missing Physics Handle"), *(_pawn->GetName()));
}

The relevant snippet of the header is as follows:

private:
    UWorld* _world;
    APawn* _pawn;
    UPhysicsHandleComponent* _physicsHandle;

public:    
    UGrabber();
    virtual void BeginPlay() override;
    virtual void TickComponent( float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction ) override;

Next up in the header, I define some design-time editable properties that I will use. The first defines the reach of my grabber and the second is used to decide whether a debug line should be drawn or not if a grab misses - allowing a designer to debug strange behaviour without having to edit or recompile any C++ code.

private:
    UPROPERTY(EditAnywhere, meta = (DisplayName = "Reach"))
    float _reach = 100.0f;

    UPROPERTY(EditAnywhere, meta = (DisplayName = "Debug Missed Grabs"))
    bool _debugMisses = false;

Note that I have used the DisplayName meta property to give my C++ class members friendly names for the designer while allowing me to stick to my underscore convention for private members. (There are considerations, here, with regard to blueprints but they are outside of the scope of this discussion.)

Also in the header, I have two utility functions:

private:
    bool TryGetGrabLine(FVector& start, FVector& end);
    bool TryGetGrabTarget(FHitResult& hitResult);

The first of these is an amalgamation of the methods named GetReachLineStart() and GetReachLineEnd() in the course content. By combining these two into one, I save one call to GetPlayerViewPoint() as shown in the implementation, lower down. The second is almost identical to GetFirstPhysicsBodyInReach() in the course content.

This is the first use of the Try...() convention in this code. Basically, the convention says that the method returns a bool indicating whether it succeeded or not and the actual data, in the case of success, is returned through reference parameters. The implementation of these two methods will reveal the reasoning behind this somewhat non-obvious idiom.

Here’s the first:

    bool UGrabber::TryGetGrabLine(FVector& grabStart, FVector& grabEnd)
    {
        if ((nullptr != _pawn) && (_pawn->IsControlled()))
        {
            FVector location;
            FRotator rotation;
            _pawn->GetController()->GetPlayerViewPoint(location, rotation);

            grabStart = location;
            grabEnd = grabStart + (rotation.Vector() * _reach);

            return true;
        }
        else UE_LOG(LogTemp, Warning, TEXT("Grab Line undefined (no possessed pawn)"));

        return false;
    }

First, note that I am not using GetFirstPlayerController() from the world but rather using IsControlled() and GetController() from the pawn to which the Grabber is actually attached - this is why I bothered to cast the grabber’s owner to an APawn earlier on. Also, see how start- and end-vectors are calculated with a single call to GetPlayerViewPoint().

The return value lets callers know whether the grab line existed or not. If the Grabber was not attached to a pawn or the player was ejected from the pawn and this code happened to be called, there would be no grab line. This simple pattern abstracts these reasons away from the caller - they simply know whether it succeeded or not and a tester can check the log to find out why it failed.

    bool UGrabber::TryGetGrabTarget(FHitResult& hitResult)
    {
        FVector grabStart;
        FVector grabEnd;
        if (TryGetGrabLine(grabStart, grabEnd))
        {
            FCollisionObjectQueryParams objectTypes(ECollisionChannel::ECC_PhysicsBody);
            FCollisionQueryParams queryParams(FName(TEXT("Grab")), false, _pawn);

            _world->LineTraceSingleByObjectType(hitResult, grabStart, grabEnd, objectTypes, queryParams);
            bool hit = (nullptr != hitResult.GetActor());

            if ((!hit) && (_debugMisses))
            {
                DrawDebugLine(_world, grabStart, grabEnd, FColor(255, 0, 0), true);
            }

            return hit;
        }

        return false;
    }

The code, above, is used in TryGetGrabTarget() and this use shows how nicely the pattern can be chained. Again, the caller of TryGetGrabTarget() does not need to know all the reasons why there might not be a grab target - either the grab line was undefined or the grab line didn’t hit a valid target. They only need to know whether a target was found or not.

And, pulling it all together, implementing GrabPressed() is now dead simple…

void UGrabber::GrabPressed()
{
    if (nullptr != _physicsHandle)
    {
        FHitResult hitResult;
        if (TryGetGrabTarget(hitResult))
        {
            _physicsHandle->GrabComponent(hitResult.GetComponent(), NAME_None, hitResult.Location, true);
            PrimaryComponentTick.SetTickFunctionEnable(true);
        }
    }
}

Notice how I start ticking if the grab succeeded. Also, I am using the Location of the FHitResult instead of the static mesh anchor - this stops the mesh from ‘jumping’ when you grab it.

GrabReleased() is also simple…

void UGrabber::GrabReleased()
{
    PrimaryComponentTick.SetTickFunctionEnable(false);

    if ((nullptr != _physicsHandle) && (nullptr != _physicsHandle->GrabbedComponent))
    {
        _physicsHandle->ReleaseComponent();
    }
}

It stops the ticking, regardless, and releases the component if one was grabbed.

And here is my TickComponent which uses TryGetGrabLine() for a second time.

void UGrabber::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
    Super::TickComponent(DeltaTime, TickType, ThisTickFunction);

    FVector grabStart;
    FVector grabEnd;
    if ((nullptr != _physicsHandle) && (nullptr != _physicsHandle->GrabbedComponent) && (TryGetGrabLine(grabStart, grabEnd)))
    {
        _physicsHandle->SetTargetLocation(grabEnd);
    }
}

Enjoy. If you have any questions about this, feel free to ask them. I hope that there are some ideas in this that provide food for thought.

What an awesome share, and so well formatted. Thank you

1 Like

Privacy & Terms