Back to Projects
Game Dev · Created at May 12, 2025 · Last updated at May 12, 2025 · 7 mins read

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.

Unreal Engine UMG Blueprint C++ Perforce Steam SDK Localization

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")
};
ScreenPurposeKey Components
AttractIdle screen with “Press Any Button”Auto-loops until input detected
PlayerModeSelectionSingle-player vs MultiplayerMode selection buttons
GameModeSelectionRace type selectionTime Trial, Grand Prix, Battle Mode
TrackSelectionMap selection with previewUMG_TrackImageAndText component
CharacterSelectionCharacter + color customizationUMG_CharacterComponent, UMG_CharacterColorPanel
KartSelectionKart + color customizationUMG_KartComponent, UMG_KartColorPanel
GearSelectionEquipment selection with statsUMG_GearPropertiesPanel, UMG_GearProgressBar
LoadingScreenTransition to gameplayAsync 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:

MainMenuBaseLayer Blueprint — state machine handles forward/backward navigation and validates selections before advancing

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_OutGameButtonBase and 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

ComponentTechnology
EngineUnreal Engine 5.6
LanguageBlueprint / C++
UI FrameworkUMG (Unreal Motion Graphics)
InputEnhanced Input System (keyboard, mouse, gamepad)
LocalizationString Tables (7 languages: en, ar-PS, zh-Hans, zh-Hant, es-419, ja-JP, ko-KR)
Third-PartyAMS (Advanced Menu System) for settings/popups
OnlineSteam SDK, OnlineSubsystemSteam
Version ControlPerforce
Team53 members (SMU Guildhall)
RoleGame UI Programmer (Outgame Menu Flow)
TimelineFebruary – May 2025 (milestone-driven agile)
PlatformPC (Steam)