Unreal UMG Exercise – Friends HUD


📺 Video demo on YouTube · 💻 Source code on GitHub


This project started as a structured UMG exercise from a Udemy course, but the interesting part was never the widget layout — it was the architectural problem underneath: how do you build a friends list that reacts to live connection events without coupling your UI to your data layer?

The answer I landed on was a combination of a UGameInstanceSubsystem as the data service, dynamic multicast delegates as the communication channel, and a Mediator actor as the explicit bridge between the two. Here’s how those pieces fit together.


The Problem Worth Solving

A friends HUD needs to do several things at once: display an initial list populated from a DataTable, move friend widgets between online and offline lists when connection state changes, and show a brief notification overlay on each transition. The naive approach — having the widget subscribe directly to the service — works for small projects but creates a tight coupling that makes both sides harder to test, modify, or reuse.

The architectural goal here was: UFriendsService must have zero knowledge of any widget class. The UI should be entirely replaceable without touching the data layer.


Architecture

The system is organized into three layers that communicate in one direction only:

UFriendsService → (delegates) → AFriendsHUDMediator → (method calls) → UFriendsHUD

UFriendsService (UGameInstanceSubsystem) owns the canonical TArray<FFriendData> and is responsible for two things: populating it from a UDataTable via InitFriendsData(), and simulating live activity via a FTimerHandle that randomly flips one friend’s bOnline flag every few seconds. When state changes, it broadcasts one of three dynamic multicast delegates — OnFriendCreationDelegate, OnFriendConnectionDelegate, or OnFriendDisconnectionDelegate. That’s all it does.

Using a UGameInstanceSubsystem here is a deliberate choice. The friends list is persistent social data — it should survive level transitions and be accessible from anywhere without manual lifecycle management. Subsystems are automatically instantiated by the engine when the UGameInstance starts, so there’s no singleton boilerplate to maintain and no risk of the object being garbage-collected mid-session.

AFriendsHUDMediator is an AActor placed in the level with FriendsHUDClass set in the Details panel. In BeginPlay it creates the HUD widget, adds it to the viewport, and binds three UFUNCTION callbacks to the service’s delegates. Its only job is to receive a delegate payload and forward it as a method call to UFriendsHUD. No logic lives here — it is pure routing. This is the Mediator pattern: it knows about both sides so neither side has to know about the other.

UFriendsHUD is the root UMG widget. It owns a TMap<FString, UFriendWidget*> for O(1) lookups when a connection state change arrives — iterating a TArray on every event would be O(n) and pointless given that the nickname is already the unique key in every delegate payload. The HUD manages two UFriendListWidget instances (one for online, one for offline) and a UConnectionStateWidget notification overlay. When OnFriendConnected is called, it retrieves the widget from the map, updates its state, removes it from the offline list, and adds it to the online list — all in a few lines.


The C++ / Blueprint Split

One of the explicit design goals of the original exercise was to keep critical logic in C++ and expose events to Blueprint for anything that involves animation or visual presentation. The split in this project follows that principle consistently:

  • UFriendListWidget::AddFriend and RemoveFriend are BlueprintImplementableEvents. The C++ side defines the contract (this must happen, with this widget pointer); the Blueprint subclass handles the actual UListView manipulation and any insertion animation.
  • UConnectionStateWidget::PlayFadeInFadeOutAnimation is a BlueprintImplementableEvent. C++ calls it after setting the text; the Blueprint owns the animation timeline, its duration, and its easing curves.
  • UFriendWidget exposes EPlatformType, Level, bOnline, and LastConnectionTime as BlueprintReadWrite so the Blueprint subclass can drive platform icons, formatted labels, and status indicators without needing additional C++ accessors.

This boundary matters in a real team context: a technical artist or UI designer can iterate on the animation feel and widget layout without ever touching the C++ files. The contracts are defined in C++; the craft is done in Blueprint.


Defensive Programming

Every UObject pointer access in this codebase uses ensureMsgf(IsValid(Ptr), TEXT("...")) with an early return rather than a bare check(). The distinction is meaningful: check() crashes the process unconditionally, which is appropriate for invariants that represent programmer error (a null pointer that should never be null given correct setup). ensureMsgf logs a callstack with a descriptive message in development builds and continues execution in shipping — better for widget pointer bindings where the failure mode is a missing Blueprint BindWidget assignment, not a logic bug.

Each class has its own log category (LogFriendsService, LogFriendsHUD, LogFriendsHUDMediator, etc.). LogTemp does not appear anywhere in the codebase. This matters when you’re reading the Output Log during a play session: filtering by LogFriendsService shows only the data layer events, filtering by LogFriendsHUDMediator shows only the routing, and so on.

All UPROPERTY class member pointers use TObjectPtr<T> instead of raw pointers. In UE5, TObjectPtr enables GC tracking and access checks in development builds that raw pointers silently skip.


What I’d Extend Next

The current implementation simulates connection state with a random timer, which is enough to drive the UI. A natural next step would be replacing UFriendsService with a real Online Subsystem integration — the delegate interface would stay identical, only the internals of InitFriendsData and the timer-based toggle would change. That’s the payoff of keeping the service decoupled: the entire UI layer is already written and wouldn’t need to be touched.

A tooltip widget showing detailed friend info on hover (platform, level, last seen timestamp) was listed as a “nice-to-have” in the original exercise spec and would be a straightforward addition given that UFriendWidget already holds all that data as BlueprintReadWrite properties.


💻 Full source on GitHub — ue5-friends-hud

Leave a comment

Create a website or blog at WordPress.com

Up ↑