
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:
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.
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.
And so I created a base C++ class for my future button, and defined the following members inside:
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:
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:
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.
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.
...
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.
...
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:
...
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:
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.
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.
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.
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:
for (const auto Component : GetComponentsByInterface(UInteractor::StaticClass()))
{
IInteractor::Execute_TriggerInteractables(Component);
}
Up until now, this is a relational graph of the current setup:
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.
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.
And now the beautiful moment of wiring up interaction triggered by the IInteractable
interface 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!
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.
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
.
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.
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.
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.
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:
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".
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.
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:
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:
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.
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:
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:
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:
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 UObservableEvent
s, 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