HardDriverz
An arcade kart racing game built in Unreal Engine 5.6 on a 53-person team, where I designed and implemented the 7-stage outgame menu pipeline with state machine architecture and hierarchical widget system.
Overview
HardDriverz is an arcade kart racing game where players customize characters, karts, and gear before racing through gravity-defying futuristic tracks. On a 53-person team at SMU Guildhall, I was the Game UI Programmer responsible for the entire outgame menu flow — a 7-stage data pipeline from the attract screen through gear selection and into the loading screen. Each screen validates selections, passes state forward, and supports backtracking without losing progress. I built this using a state machine architecture (E_OutGamePipelineState) and a hierarchical widget system (UMG_OutGameButtonBase) to handle the complexity of tracking player selections across screens while supporting keyboard, mouse, and gamepad input with seamless device switching.
This was a single-semester project (February – May 2025) following milestone-driven production:
Architecture
The outgame menu flow is driven by a state machine enum (E_OutGamePipelineState) that tracks which screen the player is on and controls forward/backward navigation. Each screen is a UMG widget that inherits from UMG_MainMenuBaseLayer, which provides the navigation bar, back button handling, and state transition logic. The state machine ensures that players can’t advance until all required selections are made.
// E_OutGamePipelineState.h — State machine enum for menu navigation
UENUM(BlueprintType)
enum class E_OutGamePipelineState : uint8
{
Attract UMETA(DisplayName = "Attract Screen"),
PlayerModeSelection UMETA(DisplayName = "Player Mode Selection"),
GameModeSelection UMETA(DisplayName = "Game Mode Selection"),
TrackSelection UMETA(DisplayName = "Track Selection"),
CharacterSelection UMETA(DisplayName = "Character Selection"),
KartSelection UMETA(DisplayName = "Kart Selection"),
GearSelection UMETA(DisplayName = "Gear Selection"),
LoadingScreen UMETA(DisplayName = "Loading Screen")
};
| Screen | Purpose | Key Components |
|---|---|---|
| Attract | Idle screen with “Press Any Button” | Auto-loops until input detected |
| PlayerModeSelection | Single-player vs Multiplayer | Mode selection buttons |
| GameModeSelection | Race type selection | Time Trial, Grand Prix, Battle Mode |
| TrackSelection | Map selection with preview | UMG_TrackImageAndText component |
| CharacterSelection | Character + color customization | UMG_CharacterComponent, UMG_CharacterColorPanel |
| KartSelection | Kart + color customization | UMG_KartComponent, UMG_KartColorPanel |
| GearSelection | Equipment selection with stats | UMG_GearPropertiesPanel, UMG_GearProgressBar |
| LoadingScreen | Transition to gameplay | Async level loading |
The widget hierarchy uses inheritance to reduce duplication. UMG_OutGameButtonBase is the parent class for all buttons (UMG_OutGameButton_Square, UMG_OutGameButton_Wide), providing hover/click/focus states, gamepad navigation support, and audio feedback.
Character and kart selection screens use a component-based approach: UMG_CharacterComponent handles the 3D preview, UMG_CharacterColorPanel handles color swatches, and UMG_CharacterDescriptionComponent displays stats — all composed into UMG_CharacterSelection. The KartSelection screen reuses the same component pattern rather than reimplementing it.
Here’s an example Blueprint showing the state machine logic in UMG_MainMenuBaseLayer:
State Machines as UI Orchestration
The pipeline pattern here — a state machine driving a sequence of validated screens with forward/backward navigation and persistent selections — isn’t specific to game menus. It’s the same structure that appears in multi-step checkout flows, onboarding wizards, form pipelines, and any UI where users move through stages that depend on previous choices. The state machine centralizes transition logic, validation rules, and backtracking behavior in one place rather than scattering it across individual screens. Each screen only knows how to render itself and report whether its selections are valid; the orchestrator decides what happens next.
Design Decisions
Why I Used a State Machine for Menu Flow Instead of Direct Widget Transitions
The initial approach was to have each screen directly call the next screen’s widget when the player clicked “Next”. This broke down fast because state was scattered: the TrackSelection widget needed to know if the player had selected a character yet (to show the correct preview), the GearSelection widget needed to know which kart was selected (to show compatible gear), and the back button needed to restore previous selections rather than resetting to defaults.
I centralized this into a state machine enum (E_OutGamePipelineState) with values for each screen. A single UMG_MainMenuBaseLayer parent class manages state transitions: when the player clicks “Next”, it checks if the current screen’s selections are valid, updates the state enum, and spawns the next screen’s widget. When the player clicks “Back”, it decrements the state and restores the previous screen with saved selections. This made the flow predictable and debuggable — I could log the state transitions and immediately see where navigation was breaking.
Why I Built a Hierarchical Widget System Instead of Duplicating Blueprints
Midway through development, I noticed that every button widget had nearly identical Blueprint logic: hover effects, click sounds, gamepad focus handling, input device switching (showing keyboard prompts vs gamepad button icons). The CharacterSelection and KartSelection screens were 80% identical. Duplicating this logic meant that fixing a bug in one place required fixing it in five other places.
I refactored the buttons into a parent-child hierarchy: UMG_OutGameButtonBase contains all the shared logic (hover/click states, audio, input device detection), and child classes (UMG_OutGameButton_Square, UMG_OutGameButton_Wide) only override visual layout.
For the selection screens, I extracted reusable components: UMG_CharacterComponent for the 3D preview, UMG_CharacterColorPanel for color swatches, UMG_CharacterDescriptionComponent for stats. CharacterSelection and KartSelection compose these components rather than reimplementing them. This reduced Blueprint graph size by roughly 60%. When I needed to add controller disconnect handling, I added it to UMG_OutGameButtonBase once and every button inherited it.
Why I Wrote a C++ Utility Library for Widget Functions
Unreal’s UMG system is Blueprint-first, but some operations are awkward or impossible in Blueprint. I needed to get the project version string from DefaultGame.ini for the main menu, and fetch localized text from string tables for multi-language support. Blueprint’s GetProjectVersion node doesn’t exist, and string table lookups require hardcoded namespace/key strings that can’t be validated at compile time.
I wrote UKartWidgetUtils, a Blueprint Function Library in C++ with three static functions: GetProjectVersionFromProjectSettings(), GetProjectNameFromProjectSettings(), and GetLocalizedProjectName(). These read from GConfig (Unreal’s config system) and string tables, exposing the results as Blueprint-callable nodes.
This also solved a build pipeline issue — our Perforce build machine needed to read version info from C++ for Steam SDK integration, and Blueprint-only solutions weren’t accessible to the build scripts.
// KartWidgetUtils.cpp — Blueprint-callable utility functions for project info and localization
FString UKartWidgetUtils::GetProjectVersionFromProjectSettings()
{
FString ProjectVersion;
GConfig->GetString(
TEXT("/Script/EngineSettings.GeneralProjectSettings"),
TEXT("ProjectVersion"),
ProjectVersion,
GGameIni
);
return ProjectVersion;
}
FText UKartWidgetUtils::GetLocalizedProjectName()
{
FString const Namespace = TEXT("ST_MenuSystem");
FString const Key = TEXT("RacerGameTitle");
return FText::FromStringTable(*Namespace, Key);
}
Challenges
Working Within the Constraints of a Third-Party Menu Framework
The project used a third-party Unreal Engine menu framework called AMS (Advanced Menu System) as a foundation. AMS provides pre-built widgets for settings panels, lobby systems, and server browsers — features designed for multiplayer shooters and RPGs. Our game needed character customization, kart customization, and gear selection with real-time 3D previews and stat visualization, which aren’t standard menu operations.
The UI team identified early that AMS wasn’t designed for our use case. The framework’s Blueprint codebase was large and tightly coupled, making it difficult to extend without breaking existing functionality. The AMS developer confirmed it was built for traditional menu navigation (settings, lobbies, server lists) rather than data-heavy customization flows with 3D previews.
The constraint forced me to build the outgame pipeline as a parallel system alongside AMS rather than on top of it. I used AMS for settings panels and system popups (where it worked well), but built the 7-stage customization flow from scratch. A tool can be well-engineered and still be the wrong fit for your problem.
Bridging Unreal’s C++-Only Input Events to Blueprint
Unreal Engine has built-in support for input device switching — when the player switches from keyboard to gamepad, the engine fires an OnInputDeviceChanged event. The problem: this event is only exposed in C++, not Blueprint. Our entire UI system was Blueprint-based, and we needed to react to device changes to swap button prompts (show “Press A” for gamepad vs “Press Enter” for keyboard).
The workaround was a Blueprint-callable C++ actor component that listens to the C++ event and broadcasts a Blueprint event dispatcher. Every widget that needed device change notifications subscribed to this dispatcher. This worked, but it added a layer of indirection — instead of binding directly to an engine event, widgets bound to a custom event on a component that might or might not exist.
What I’d Do Differently
- Prototype the hardest screen before committing to a framework. AMS was well-engineered but wrong for our use case. If we’d prototyped CharacterSelection (3D preview + color customization + stat display) in AMS during the first week, we’d have discovered the mismatch before building on top of it.
- Build the C++ → Blueprint bridge as a reusable plugin from day one. The input device switching bridge was a one-off workaround. A thin C++ layer that exposes engine-level events (input device changes, window focus, platform notifications) as Blueprint event dispatchers would be reusable across every UE project with Blueprint UI.
- Extract the hierarchical widget system into a standalone module.
UMG_OutGameButtonBaseand the component-based selection screens are generic enough to reuse. Packaging them as a plugin with configurable styles would save weeks on the next project with multi-stage menu flows. - Push for C++ earlier in the UI pipeline. We started Blueprint-only and retrofitted C++ when we hit Blueprint’s limitations. Starting with a C++ base layer and Blueprint for layout/visuals would have avoided the bridging workarounds entirely.
Technical Specifications
| Component | Technology |
|---|---|
| Engine | Unreal Engine 5.6 |
| Language | Blueprint / C++ |
| UI Framework | UMG (Unreal Motion Graphics) |
| Input | Enhanced Input System (keyboard, mouse, gamepad) |
| Localization | String Tables (7 languages: en, ar-PS, zh-Hans, zh-Hant, es-419, ja-JP, ko-KR) |
| Third-Party | AMS (Advanced Menu System) for settings/popups |
| Online | Steam SDK, OnlineSubsystemSteam |
| Version Control | Perforce |
| Team | 53 members (SMU Guildhall) |
| Role | Game UI Programmer (Outgame Menu Flow) |
| Timeline | February – May 2025 (milestone-driven agile) |
| Platform | PC (Steam) |
Related Projects
Game Dev
Corrupted Hollow
A third-person puzzle-platformer built in Unreal Engine 5.7 on a 24-person team, where I designed the multi-module C++ architecture, built the Ghosty companion system, and explored MassEntity for swarm behavior.
Game Dev
Daemon Chess
A 3D chess game with procedural piece geometry, Blinn-Phong shading, and raycast-based interaction, built on a custom C++ engine.
Game Dev
DaemonCraft
A Minecraft-inspired voxel game with 6-phase procedural terrain generation, multithreaded chunk system, and day/night lighting, built on a custom C++ engine.