MVVM Framework¶
Summary: The MVVM framework uses
UBaseViewModel(extendsUMVVMViewModelBase) for data binding andUBaseUIControlleras mediator between game systems and ViewModels. Properties use theFieldNotifymetadata 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
- Data Flow
- ViewModel Pattern
- Property Binding
- Controller-ViewModel Relationship
- Widget Binding
- Static vs Dynamic Data
- API Reference
- Source References
- Related Systems
- Recent Changes
Why MVVM¶
Design Goals¶
- Testable UI Logic: ViewModels can be tested without spawning widgets
- Decoupled Systems: Game systems don't know about UI; controllers mediate
- Native UE Integration: Uses Unreal's FieldNotification system, not custom binding
- Two-Way Flow: Data flows down to widgets, commands flow up to systems
- 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¶
- ViewModels never call game systems directly - Controllers mediate all communication
- Widgets never modify game state directly - They call controller methods
- Game systems broadcast events - Controllers subscribe and update ViewModels
- 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:
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 |
Related Systems¶
- UI Architecture - Controller and subsystem setup
- CommonUI Integration - Widget base classes
- Widget Library - Concrete widget implementations
- GAS Overview - Attribute binding
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 |