Skip to content

MVVM Framework

Summary: The MVVM framework uses UBaseViewModel (extends UMVVMViewModelBase) for data binding and UBaseUIController as mediator between game systems and ViewModels. Properties use the FieldNotify metadata for automatic change notifications. Controllers register ViewModels with Unreal's MVVM subsystem, allowing widgets to bind via UMG Designer or C++.

Table of Contents


Why MVVM

Design Goals

  1. Testable UI Logic: ViewModels can be tested without spawning widgets
  2. Decoupled Systems: Game systems don't know about UI; controllers mediate
  3. Native UE Integration: Uses Unreal's FieldNotification system, not custom binding
  4. Two-Way Flow: Data flows down to widgets, commands flow up to systems
  5. Reactive Updates: Property changes automatically notify bound widgets

Key Tradeoffs

Decision Benefit Cost
ViewModels as Data Containers Simple, testable, focused responsibility Controllers handle all logic
FieldNotify Pattern Native UE5 support, designer-friendly Verbose property declarations
Named Registration Widgets find VMs by name, loosely coupled String-based lookup
Controller Mediation Clear data flow, single update point Extra layer to maintain

Data Flow

+-------------------+
|   Game Systems    |   Combat, Inventory, Equipment, Abilities
+-------------------+
         |
         | Events / Callbacks
         | (OnEquipmentChanged, OnAttributeChanged, etc.)
         v
+-------------------+
|    Controller     |   UBaseUIController subclass
+-------------------+
         |
         | Update properties via setters
         | (SetItemLevel, SetHealthPercent, etc.)
         v
+-------------------+
|    ViewModel      |   UBaseViewModel subclass
+-------------------+
         |
         | FieldNotify broadcast
         | (UE_MVVM_BROADCAST_FIELD_VALUE_CHANGED)
         v
+-------------------+
|     Widget        |   UUserWidget subclass
+-------------------+
         |
         | Display data to player
         v
     [Screen]

         ^
         | User Input (clicks, keypresses)
         |
+-------------------+
|     Widget        |   Calls controller methods
+-------------------+
         |
         v
+-------------------+
|    Controller     |   Calls game system methods
+-------------------+
         |
         v
+-------------------+
|   Game Systems    |   Execute game logic
+-------------------+

Data Flow Rules

  1. ViewModels never call game systems directly - Controllers mediate all communication
  2. Widgets never modify game state directly - They call controller methods
  3. Game systems broadcast events - Controllers subscribe and update ViewModels
  4. Property changes broadcast automatically - Widgets react to FieldNotify

ViewModel Pattern

UBaseViewModel

Base class for all ViewModels. Provides lifecycle hooks and initialization tracking.

+---------------------------+
|      UBaseViewModel       |
+---------------------------+
| - bIsInitialized : bool   |
+---------------------------+
| + Initialize()            |  Call once after creation
| + Reset()                 |  Clear all state
| + IsInitialized()         |  Check if ready
| # OnInitialize()          |  Override for setup
| # OnReset()               |  Override for cleanup
+---------------------------+

Property Declaration Pattern

Each bindable property requires: 1. Private member with FieldNotify and Getter metadata 2. Public getter marked BlueprintPure and FieldNotify 3. Public setter that broadcasts change

Property Declaration Structure:
+------------------------------------+
|  UPROPERTY(FieldNotify, Getter)   |  Private member
|  Type PropertyName;               |
+------------------------------------+
           |
           v
+------------------------------------+
|  UFUNCTION(BlueprintPure,         |  Getter function
|            FieldNotify)           |
|  Type GetPropertyName() const;    |
+------------------------------------+
           |
           v
+------------------------------------+
|  void SetPropertyName(Type Val)   |  Setter function
|  {                                |
|    if (PropertyName != Val) {     |
|      PropertyName = Val;          |
|      BROADCAST_CHANGE();          |
|    }                              |
|  }                                |
+------------------------------------+

Property Binding

FieldNotify Metadata Requirements

Element Metadata Purpose
Property FieldNotify, Getter="GetX", AllowPrivateAccess Marks for binding, specifies getter
Getter BlueprintPure, FieldNotify Exposed to widgets, bindable
Setter None required Just call broadcast macro

Broadcasting Changes

Use UE_MVVM_BROADCAST_FIELD_VALUE_CHANGED(PropertyName) after modifying a property.

Single Property:

SetHealth(NewValue)
  -> HealthPercent = NewValue
  -> BROADCAST(HealthPercent)

Multiple Dependent Properties:

SetItem(NewItem)
  -> ItemIcon = NewItem->GetIcon()
  -> StackCount = NewItem->GetStackCount()
  -> BROADCAST(ItemIcon)
  -> BROADCAST(StackCount)

Change Detection

Always check for actual changes before broadcasting to avoid unnecessary widget updates:

Setter Pattern:
  1. Check if value actually changed
  2. Update internal state
  3. Update derived values
  4. Broadcast all changed properties

Controller-ViewModel Relationship

Controller Responsibilities

Controller Lifecycle:
+-------------------------------------------+
| OnInitialize()                            |
|   1. Create ViewModel (NewObject)         |
|   2. Initialize ViewModel                 |
|   3. Register with MVVM Subsystem         |
|   4. Create Widget from DefaultWidgetClass|
+-------------------------------------------+
           |
           v
+-------------------------------------------+
| BindToPlayer() / BindToComponent()        |
|   1. Cache component references           |
|   2. Subscribe to component events        |
|   3. Initial ViewModel update             |
+-------------------------------------------+
           |
           v
+-------------------------------------------+
| Event Handlers (OnEquipmentChanged, etc.) |
|   1. Calculate new values                 |
|   2. Call ViewModel setters               |
+-------------------------------------------+

ViewModel Registration

Controllers register ViewModels with Unreal's MVVM subsystem for widget lookup:

Registration Flow:
  Controller -> MVVMGameSubsystem -> ViewModelCollection
      |                                    |
      +-- Context.ContextClass = VM->GetClass()
      +-- Context.ContextName = "InventoryViewModel"
                                           |
                                           v
                                 AddViewModelInstance()

Widgets retrieve ViewModels by class and name:

Widget Lookup:
  Widget -> MVVMGameSubsystem -> ViewModelCollection
      |                                    |
      +-- Context.ContextClass = UInventoryViewModel
      +-- Context.ContextName = "InventoryViewModel"
                                           |
                                           v
                                 FindViewModelInstance()

Widget Binding

Native C++ Binding

Widgets bind to ViewModel property changes using FieldNotification delegates:

Widget Binding Flow:
+-------------------------+
| NativeConstruct()       |
|   1. Find ViewModel     |
|   2. BindViewModel()    |
+-------------------------+
           |
           v
+-------------------------+
| BindViewModel()         |
|   AddFieldValueChanged  |
|   Delegate for each     |
|   property of interest  |
+-------------------------+
           |
           v
+-------------------------+
| OnPropertyChanged()     |
|   Update widget display |
+-------------------------+
           |
           v
+-------------------------+
| NativeDestruct()        |
|   UnbindViewModel()     |
|   Remove delegates      |
+-------------------------+

Delegate Binding Pattern

Step Purpose
AddFieldValueChangedDelegate Subscribe to property changes
Store FDelegateHandle Keep reference for cleanup
Implement handler Update widget on change
RemoveFieldValueChangedDelegate Clean up in NativeDestruct

Static vs Dynamic Data

The Split Pattern

SkillTooltipViewModel demonstrates splitting static and dynamic data:

+-----------------------------------+
|     UpdateFromAbility()           |   Called once on hover
+-----------------------------------+
|  Cache static data:               |
|    - AbilityName                  |
|    - AbilityIcon                  |
|    - DescriptionTemplate          |
|    - SkillType                    |
|                                   |
|  Cache references for later:      |
|    - CachedAttributeSet           |
|    - CachedCombatComponent        |
|    - CachedEquipmentComponent     |
+-----------------------------------+
           |
           v
+-----------------------------------+
|     RefreshDynamicValues()        |   Called when tooltip shown
+-----------------------------------+
|  Recalculate from current state:  |
|    - Damage (uses current stats)  |
|    - Costs (uses current stamina) |
|    - Combo info (current weapon)  |
|    - Charge info (current bonuses)|
|                                   |
|  Format description with values   |
+-----------------------------------+

Why This Matters

Data Type When Set Example
Static On ability assignment Name, icon, base description
Dynamic On tooltip display Damage range (affected by buffs), stamina cost

Benefits: - Hover doesn't recalculate everything - Tooltip refresh is cheap - Values stay current as player stats change


API Reference

UBaseViewModel

Method Return Description
Initialize() void Set up ViewModel, call OnInitialize()
Reset() void Clear state, call OnReset()
IsInitialized() bool Check if Initialize() was called

USkillTooltipViewModel

Method Return Description
UpdateFromAbility(Ability, Options, AttrSet, Combat, Equipment) void Cache static data and references
RefreshDynamicValues() void Recalculate damage, costs, combos from current state
SetDetailedMode(bDetailed) void Toggle detailed info display
ClearData() void Reset all properties to defaults

Common ViewModel Properties

Property Type Description
AbilityName FText Display name
AbilityIcon UTexture2D* Icon texture
CostInfo FAbilityCostDisplayInfo Stamina, resonance, cooldown
EffectInfos TArray Status effects applied
ComboChains TArray Combo progression info

UBaseUIController

Method Return Description
RegisterViewModel(VM, Name) void Register with MVVM subsystem
UnregisterViewModel(VM, Name) void Remove from MVVM subsystem

Source References

Class File Line
UBaseViewModel Source/ProjectEternal/Public/UI/ViewModels/BaseViewModel.h 1
USkillTooltipViewModel Source/ProjectEternal/Public/UI/ViewModels/Tooltip/SkillTooltipViewModel.h 1
UInventoryViewModel Source/ProjectEternal/Public/UI/ViewModels/Inventory/InventoryViewModel.h 1
UHUDViewModel Source/ProjectEternal/Public/UI/ViewModels/HUD/HUDViewModel.h 1
UItemViewModel Source/ProjectEternal/Public/UI/ViewModels/Inventory/ItemViewModel.h 1


Recent Changes

Date Change Impact
2024-12 SkillTooltipViewModel splits static/dynamic data UpdateFromAbility() caches static data; RefreshDynamicValues() recalculates damage/costs
2024-12 ViewModel caches component references CachedAttributeSet, CachedCombatComponent, CachedEquipmentComponent stored for on-demand recalc
2024-12 Ability display types moved to AbilityDisplayTypes.h FAbilityDamageDisplayInfo, FAbilityCostDisplayInfo, etc. now in Abilities module
2024-12 SkillTooltipTypes.h now includes AbilityDisplayTypes.h UI-specific types (FAbilityEffectDisplayInfo, FSkillTooltipDisplayOptions) remain in UI module