Thomas Winged

hero image

Buttons, weather system and minimap navigation


Task description

Create a simple but effective system for controlling a dynamic sky (Ultra Dynamic Sky) and a simple mini-map system (Map Quest PRO) in a game using Unreal Engine 5.1.

The player should be able to control the weather by pressing the appropriate triggers in the game world. Above each trigger, there should be a UI in the game world telling what weather change is "hidden" under a given trigger.

Triggers should be visible as points on the mini-map as points of interest. The player should be able to see a navigation point that shows the remaining distance to that location.

Preface

I am fully aware that this task can be solved using direct coupling between game objects using almost no abstractions and C++ code. This approach for sure can be enough for quick prototyping. But it would not be an exciting topic for a blog post, would it? Because the task description requires the system to be effective, and I prefer (or at least always try) to write solid code, I decided to implement some patterns to create simple but extendable architecture.

Button controlling weather

Action

First, I wanted to ensure that a button's interaction is decoupled from an action it executes. I did not want to create many child classes of the button, as nothing would change in the button mechanism except for the triggered activity. For this reason, I created an UAction abstract class to implement a command pattern. I defined it as follows:

Action.h - a brief definition
UCLASS(Abstract, Blueprintable, BlueprintType)
class TASKPROJECT_API UAction : public UObject
{
    ...
public:
    UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Action")
    void Init();  // For internal variables initialization step

    UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Action")
    void Execute(const AActor* Subject);

protected:
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Action")
    FText Description; // Going to be visible above the button
};

This allowed me to encapsulate the very essence of requested action, by creating for example BP_Action_Weather_ToggleWeather Blueprint child class. Inside I created an array of UDS Weather Settings and iterated over it to fetch the next one in the queue.

1673425924-1tHwrYv4Z7.webp

BP_Action_Weather_ToggleWeather for switching weathers inside Ultra_Dynamic_Weather actor

To test this functionality I created a BP_ActionTester which constructed the action object and using in-editor button I was able to execute given activity.

1673426191-Q02CwlzXnX.webp

BP_ActionTester - for validating the idea of encapsulating the action of the button in a separate object

And so I created a base C++ class for my future button, and defined the following members inside:

InteractableAction.h - a base class for the button with fields allowing to choose in-editor the action to perform.
public:
    UPROPERTY(EditAnywhere, Category="Action")
    TSubclassOf<UAction> ActionType;

private:
    UPROPERTY()
    UAction* Action;

On BeginPlay I construct the action appropriate object and assign it to Action member:

InteractableAction.cpp - constructing the action from a given subclass
void AInteractableAction::BeginPlay()
{
    Super::BeginPlay();

    if (ActionType)
    {
        Action = NewObject<UAction>(this, ActionType);
        Action->Init();
    }
    else
        UE_LOG(LogTemp, Error, TEXT("%s >> No action specified!"), ANSI_TO_TCHAR(__FUNCTION__))
}

Deriving a Blueprint class out of it and adding an AActor* Subject field allowed me to attach chosen action right inside the editor:

1673426898-I1MrnGdWJM.webp

BP_Button - setting action and its subject in the editor

1673427198-1USGsdY7X5.webp

BP_Button - using given fields to execute assigned action

So I got an actor that can execute a given action on a given subject. Let's interact with it using something different than an in-editor button.

Interactor and interactable

Next, the button needs to be somehow pressed. There needs to be some connection between a player clicking a mouse and triggering Execute Action method declared in BP_Button. And so I quickly bound input action with placeholder InteractWithWorld() method declared inside my pawn.

1673427542-7WtTQpg65V.webp

Input settings in the project
MyPlayerController.cpp - binding left mouse click
void AMyPlayerController::SetupInputComponent()
{
    Super::SetupInputComponent();
    InputComponent->BindAction("LMB", IE_Pressed, this, &AMyPlayerController::HandleLMBClick);
}

void AMyPlayerController::HandleLMBClick()
{
    if (MyCharacter) // Fetched in BeginPlay using = Cast<AMyCharacter>(GetPawn())
        MyCharacter->InteractWithWorld(); // For now, does nothing except printing a log
}

I can sense code smell creeping around already - now AMyPlayerController is coupled to AMyCharacter class. I don't want to propagate this approach further, so I plan to introduce some decoupling. And so following the "D" in SOLID principles - the Dependency Inversion Principle, which states that:

"High-level modules shouldn't depend on low-level modules, but both should depend on shared abstractions."

I created two interfaces: IInteractable and IInteractor. The first one is meant to be implemented by the parent button class.

IInteractable.h - basic functionality for interactibles
...
class TASKPROJECT_API IInteractible
{
    ...
    UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="Interaction")
    void Interact();

    UFUNCTION(BlueprintNativeEvent, Category="Interaction")
    void OnRangeEnter(const TScriptInterface<IInteractor>& Interactor);

    UFUNCTION(BlueprintNativeEvent, Category="Interaction")
    void OnRangeExit(const TScriptInterface<IInteractor>& Interactor);
};

The latter has just one method, which is meant to be called by a pawn or a player controller and be implemented by kind of some interactor.

IInteractor.h - basic functionality for interactor
...
class TASKPROJECT_API IInteractor
{
    ...
    UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="Interaction")
    void TriggerInteractables();
};

To keep things simple, I decided to utilize a USphereComponent. It detected whether an actor with the IInteractible interface overlapped its area. The SphereInteractor, as I named it, was defined as follows:

SphereInteractor.h - its basic form in a brief
...
class TASKPROJECT_API USphereInteractor : public USphereComponent, public IInteractor
{
    ...

public:
    virtual void TriggerInteractables_Implementation() override;

private:
    UPROPERTY(VisibleInstanceOnly)
    TArray<TScriptInterface<IInteractable>> InRange = TArray<TScriptInterface<IInteractable>>();

    UFUNCTION()
    void OnBeginOverlap(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, ...);

    UFUNCTION()
    void OnEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, ...);
};

In its constructor, I bound USphereComponent delegates to my custom overlap-handling methods:

SphereInteractor.cpp - bounding delegates of USphereComponent
USphereInteractor::USphereInteractor()
{
    ...
    OnComponentBeginOverlap.AddDynamic(this, &USphereInteractor::OnBeginOverlap);
    OnComponentEndOverlap.AddDynamic(this, &USphereInteractor::OnEndOverlap);
}

Note: OnBeginOverlap and OnEndOverlap methods needs to have UFUNCTION() above them in order to be able to bind them using AddDynamic() method.

Once the sphere of the component overlaps a new actor, it checks whether it implements IInteractable interface. If it is, it executes interface methods on that object.

SphereInteractor.cpp - handling actors overlap
void USphereInteractor::OnBeginOverlap(UPrimitiveComponent* OverlappedComp, AActor* Other, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
    if (!InRange.Contains(Other))
    {
        if (Other->GetClass()->ImplementsInterface(UInteractable::StaticClass()))
        {
            InRange.Add(Other);
            IInteractable::Execute_OnRangeEnter(Other, GetOwner());
        }
    }
}

Note that using UClass::ImplementsInterface(UClass* SomeInterface) instead of Cast<UClass* SomeInterface> assures that this check works correctly, regardless of whether an object is a Blueprint that implements the class already implementing the interface, or it implements the interface by itself. Check out >> this article << for more info.

1673431665-Zpo0GDgUcK.webp

Sphere interactor detecting overlapped actors implementing IInteractable interface

I got InRange array populated by interactables. Now, I can handle requests for triggering interaction following the same logic of executing the interface method on them.

SphereInteractor.cpp - interacting with interactables
void USphereInteractor::TriggerInteractables_Implementation()
{
    for (const auto Interactable : InRange)
    {
        IInteractable::Execute_Interact(Interactable.GetObject());
    }
}

After I attach this (or actually any component implementing IInteractor interface), I can quickly iterate over all components with IInteractor interface and trigger the interaction after the left mouse button is clicked:

MyCharacter.cpp - using interactor components
for (const auto Component : GetComponentsByInterface(UInteractor::StaticClass()))
{
    IInteractor::Execute_TriggerInteractables(Component);
}

Up until now, this is a relational graph of the current setup:

1673441782-U5XN2K39Fb.webp

UML diagram of interactors and interactables

A visual part of the button

Next I created a simple model of a button in Cinema4D. It consisted of three meshes - stand, body, and "pushable red mushroom(?)" (I wonder what this part is called... maybe "button"?). I assembled it in a Blueprint (BP_Button) derived from AInteractableAction. I also added a text component containing a description of the action attached to the button.

1673432081-pW611UdEql.webp

Visual representation of a button

Using a timeline component in combination with modification of the relative location of a pushable part I created a simple push animation that will be played after a player interacts with the button.

1673432296-PrTpnttUhU.webp

BP_Button - defining the animation of the button press

And now the beautiful moment of wiring up interaction triggered by the IInteractable interface with the animation:

1673432517-qF22CoF1ue.webp

BP_Button - wiring up the interaction request with the animation

After adding an UpdateDescription method which takes the description from the UAction object and puts it inside UTextComponent + creating some smooth animation for the description, finally, when I click on the button, besides triggering an action specified by TSubclassOf<UAction> I also receive visual feedback from the button itself. Cool!

animated-button.webp

Animation of a button triggered by a left mouse button click

Mini-map and navigation

As the task requires, I implemented the Map Quest PRO plugin inside my player controller. Following a wiki page of this plugin, very quickly I received working mini-map and compass.

1673435589-hrx0S7iPbq.webp

Map Quest PRO compass and mini-map

I dig a little bit inside the Demo map that this plugin is bundled with and found that in order to attach a navigation point to some Actor, I need to use AddQuest method defined inside QuestMapManager component wired to my PlayerController.

1673435864-AfqTqLzl0B.webp

RnD in a level blueprint, adding a navigation point to the world.

1673435720-XAZfya6oTc.webp

Navigation point attached to the button

And so I thought, "Ok, I don't want to specify the whole mechanism of tracking the progress of pressed buttons" and I created a new class called AQuestManager, which was going to do just that. I created an Objectives array of actors inside so I could populate it from within the editor, take the first button and spawn the navigation point in its location.

QuestManager.h - the array with buttons to be clicked
UPROPERTY(EditInstanceOnly, BlueprintReadWrite, Category="Quest Manager")
TArray<AActor*> Objectives;

But I was still missing one important piece in this puzzle - some callback with information about whether a given button has been pressed, so I could move the navigation point to the next location.

Observer and observable

To let the AQuestManager and the button communicate with each other without direct coupling, I needed to implement another set of interfaces to utilize an observer pattern. Hence I created IObserver and IObservable. And as you can guess, the quest manager will implement the first and the button the latter.

IObserver.h - the single important method
UFUNCTION(BlueprintCallable, BlueprintNativeEvent)
void OnObservableEvent(const TScriptInterface<IObservable>& Subject, TSubclassOf<UObservableEvent> Event);

And so when the player performs the interaction with the button, it can call the OnObservableEvent on the listening quest manager. To distinct different types of events, as I noticed usually programmers use an enum with entries like Killed, Entered, Jumped. But I dislike the idea of constantly expanding the enum when new types of events are needed. Instead, I introduced an additional abstract class UObservableEvent and created derived classes with names expressing events' essence. Using their types allowed me to mimic the enum-like way of passing the type of the event.

UObservableEvent class and its derivatives
UCLASS(Abstract, BlueprintType)
class UObservableEvent : public UObject { GENERATED_BODY() };

UCLASS(BlueprintType)
class UInteractedEvent : public UObservableEvent { GENERATED_BODY() };

UCLASS(BlueprintType)
class UEnteredRangeEvent : public UObservableEvent { GENERATED_BODY() };

UCLASS(BlueprintType)
class UExitedRangeEvent : public UObservableEvent { GENERATED_BODY() };

Now, since I can differentiate whether the player clicked the button on merely got close to it, I created a struct defining the objectives needed to be accomplished:

Utils.h - structure containing information about the objective
USTRUCT(BlueprintType)
struct FQuestObjective
{
    GENERATED_BODY()

    UPROPERTY(BlueprintReadOnly)
    int32 ID; // Assigned by QuestManager

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    AActor* Subject;

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    TSubclassOf<UObservableEvent> EventType;

    UPROPERTY(VisibleAnywhere, BlueprintReadWrite)
    bool Completed = false; // Assigned by QuestManager
};

And so I changed the signature of Objectives array inside the AQuestManager to TArray<FQuestObjective> and implemented methods like GetNextObjective or SetObjectiveCompleted to manipulate the progress of the "quest".

1673439310-MmFjUGabDt.webp

An array of struct objectives inside BP_QuestManager actor

In this form, the observer interface via the OnObservableEvent method allows me to mark the active objective as completed and inform the derived Blueprint class about it.

QuestManager.cpp - handling event send by listeners
void AQuestManager::OnObservableEvent_Implementation(const TScriptInterface<IObservable>& Subject, TSubclassOf<UObservableEvent> Event)
{
    FQuestObjective NextObjective;
    const bool bNextObjectiveFound = GetNextObjective(NextObjective);
    if (bNextObjectiveFound && Subject == NextObjective.Subject && Event == NextObjective.EventType)
    {
        SetObjectiveCompleted(NextObjective.ID, true);
        SetEnabledObjectiveListener(NextObjective.ID, false);

        // BlueprintImplementableEvent for handling mechanisms in Blueprint child class
        OnObjectiveCompleted(NextObjective);
    }
}

And now highlighting the next objective in the game world is as simple as this:

1673439432-3vYsG8mFQm.webp

Moving the navigation point to the next objective location.
QuestManager.h - final definition of the class
class TASKPROJECT_API AQuestManager : public AActor, public IObserver
{
    GENERATED_BODY()

public:
    AQuestManager();

    virtual void OnObservableEvent_Implementation(const TScriptInterface<IObservable>& Subject, TSubclassOf<UObservableEvent> Event) override;

    UFUNCTION(BlueprintPure, Category="Quest Manager")
    bool GetNextObjective(FQuestObjective& OutObjective) const;

protected:
    virtual void BeginPlay() override;

    UPROPERTY(EditInstanceOnly, BlueprintReadWrite, Category="Quest Manager")
    TArray<FQuestObjective> Objectives;

    UFUNCTION(BlueprintImplementableEvent, Category="Quest Manager")
    void OnObjectiveCompleted(FQuestObjective Objective);

private:
    void InitializeObjectives(); // Assign IDs based on array sequence

    void SetEnabledObjectiveListener(int32 ID, bool bEnabled);

    void SetObjectiveCompleted(int32 ID, bool bCompleted);
};

My observer observes. Now it's time to make my subjects to be observable. Here comes the IObservable interface:

IObservable.h - it's basic form
UFUNCTION(BlueprintCallable, BlueprintNativeEvent)
void AddListener(const TScriptInterface<IObserver>& Listener);

UFUNCTION(BlueprintCallable, BlueprintNativeEvent)
void RemoveListener(const TScriptInterface<IObserver>& Listener);

Making the button implement that interface, the observing quest manager can request to be added to the button's newsletter (I perform this operation inside BeginPlay()), which informs every interested entity about interactions performed on them.

QuestManager.cpp - subscribing to listeners
void AQuestManager::SetEnabledObjectiveListener(int32 ID, bool bEnabled)
{
    for (int32 i = 0; i < Objectives.Num(); i++)
    {
        if (const auto Subject = Objectives[ID].Subject)
        {
            if (Subject->GetClass()->ImplementsInterface(UObservable::StaticClass()))
            {
                if (bEnabled)
                    IObservable::Execute_AddListener(Subject, this);
                else
                    IObservable::Execute_RemoveListener(Subject, this);
            }
        }
    }
}

And so for example the button, when the Interact interface method is called, can notify all interested observers about this event:

InteractableAction.cpp - informing listeners about interaction with the button
void AInteractableAction::Interact_Implementation()
{
    NotifyListeners(UInteractedEvent::StaticClass());
}

void AInteractableAction::NotifyListeners(TSubclassOf<UObservableEvent> Event)
{
    // Iterate over indices instead using for-each to avoid "Array has changed during ranged-for iteration!" error
    for (int32 i = 0 ; i < Listeners.Num() ; i++)
    {
        IObserver::Execute_OnObservableEvent(Listeners[i].GetObject(), this, Event);
    }
}

And so this is the final form of AInteractableAction definition implementing both IInteractable and IObservable interfaces:

InteractableAction.h - it's final form
class TASKPROJECT_API AInteractableAction : public AActor, public IInteractable, public IObservable
{
    GENERATED_BODY()

public:
    AInteractableAction();

    virtual void Interact_Implementation() override;
    virtual void OnRangeEnter_Implementation(const TScriptInterface<IInteractor>& Interactor) override;
    virtual void OnRangeExit_Implementation(const TScriptInterface<IInteractor>& Interactor) override;

    UPROPERTY(EditAnywhere, Category="Action")
    TSubclassOf<UAction> ActionType;

    virtual void AddListener_Implementation(const TScriptInterface<IObserver>& Observer) override;
    virtual void RemoveListener_Implementation(const TScriptInterface<IObserver>& Observer) override;

protected:
    virtual void BeginPlay() override;

    UFUNCTION(BlueprintPure)
    UAction* GetAction() const;

    UPROPERTY(VisibleAnywhere)
    TArray<const TScriptInterface<IObserver>> Listeners;

private:
    UPROPERTY()
    UAction* Action;

    void NotifyListeners(TSubclassOf<UObservableEvent> Event);
};

In the end, this is a relational diagram of classes and abstractions that I implemented during this task. I hope it does not make everything even more complicated, as I am just learning how to create such graphs:

1673442302-8ov7AZ75vo.webp

Somehow-correct UML diagram of the final architecture

Final effect + conclusions

Creating this kind of architecture was really an enjoyable experience. Below I present to you a short video depicting the final result:

When I think about things I could make differently, I would like to try to make a SubSystem that keeps track of all UObservableEvents, so each actor could bind to the subsystem instead of directly connecting to actors implementing IObservable interface. But that's an idea for another time.

Thank you for taking the time and reading my post. I hope you enjoyed it! If so, check out my other posts about Unreal Engine - there are some other pretty neet experiments described inside : )

Links and files

GitHub repository: https://github.com/thomaswinged/ue-recruitment-task-1

Windows Build: ue-recruitment_task_1-thomas_winged-build_win.zip