· Filip Iliescu · Game Development · 6 min read
Implementing MVVM Pattern in Unreal Engine
Learn how to adapt the Model-View-ViewModel pattern for Unreal Engine game development to create maintainable and testable UI systems.
Implementing MVVM in Unreal Engine with C++
The Model-View-ViewModel (MVVM) pattern has long been a staple in web and app development for creating maintainable UI systems. While Unreal Engine doesn’t include built-in support for MVVM, several community plugins have emerged to bring this powerful pattern to UE development. This tutorial will guide you through implementing MVVM in your Unreal Engine projects using C++.
Understanding MVVM in the Context of Unreal Engine
Before diving into implementation, let’s clarify how MVVM maps to Unreal Engine concepts:
- Model: The data layer, typically C++ classes that represent your game state (e.g., character stats, inventory)
- View: UMG widgets that display information to the player
- ViewModel: The intermediary that transforms Model data for the View and handles UI logic
The key benefit of MVVM in Unreal is the separation of concerns - your UI logic becomes independent from both your game logic and your UI layout.
Setting Up the MVVM Plugin
The first step is to add an MVVM plugin to your project. We’ll be using the popular “Unreal MVVM” plugin by Bozoskill for this tutorial.
1. Install the Plugin
You can either:
- Download from the GitHub repository and place in your Plugins folder
- Add as a submodule if your project uses Git
- Install from the Unreal Marketplace if available
Add this to your .uproject
file:
"Plugins": [
{
"Name": "MVVM",
"Enabled": true
}
]
2. Add Dependencies to Your Module
In your project’s Build.cs
file, add the MVVM module as a dependency:
PublicDependencyModuleNames.AddRange(
new string[]
{
"Core",
"CoreUObject",
"Engine",
"InputCore",
"UMG",
"MVVM" // Add this line
}
);
Creating Your First MVVM Components
Let’s implement a simple health display system using MVVM:
1. Create the Model
First, define your data model:
// CharacterStatsModel.h
#pragma once
#include "CoreMinimal.h"
#include "CharacterStatsModel.generated.h"
UCLASS()
class YOURGAME_API UCharacterStatsModel : public UObject
{
GENERATED_BODY()
public:
UCharacterStatsModel();
UPROPERTY()
float Health;
UPROPERTY()
float MaxHealth;
};
// CharacterStatsModel.cpp
#include "CharacterStatsModel.h"
UCharacterStatsModel::UCharacterStatsModel()
{
Health = 100.0f;
MaxHealth = 100.0f;
}
2. Create the ViewModel
Now, create a ViewModel that will prepare the Model data for display:
// CharacterStatsViewModel.h
#pragma once
#include "CoreMinimal.h"
#include "MVVMViewModelBase.h"
#include "CharacterStatsModel.h"
#include "CharacterStatsViewModel.generated.h"
UCLASS()
class YOURGAME_API UCharacterStatsViewModel : public UMVVMViewModelBase
{
GENERATED_BODY()
public:
UCharacterStatsViewModel();
// Bindable properties
UPROPERTY(BlueprintReadOnly, FieldNotify)
float HealthPercentage;
UPROPERTY(BlueprintReadOnly, FieldNotify)
FText HealthText;
// Reference to the model
UPROPERTY()
UCharacterStatsModel* StatsModel;
// Call this when model updates
UFUNCTION()
void UpdateFromModel();
// Bindable actions
UFUNCTION(BlueprintCallable)
void UseHealthKit();
};
// CharacterStatsViewModel.cpp
#include "CharacterStatsViewModel.h"
UCharacterStatsViewModel::UCharacterStatsViewModel()
{
StatsModel = CreateDefaultSubobject<UCharacterStatsModel>(TEXT("StatsModel"));
UpdateFromModel();
}
void UCharacterStatsViewModel::UpdateFromModel()
{
if (StatsModel)
{
HealthPercentage = StatsModel->Health / StatsModel->MaxHealth;
HealthText = FText::Format(NSLOCTEXT("CharacterStats", "HealthFormat", "{0}/{1}"),
FText::AsNumber(FMath::RoundToInt(StatsModel->Health)),
FText::AsNumber(FMath::RoundToInt(StatsModel->MaxHealth)));
}
}
void UCharacterStatsViewModel::UseHealthKit()
{
if (StatsModel)
{
// Apply healing logic
StatsModel->Health = FMath::Min(StatsModel->Health + 25.0f, StatsModel->MaxHealth);
UpdateFromModel();
}
}
3. Create the View
Now, create a UMG widget for your View. This is typically done in the UMG editor, but here’s how to set up the view bindings in C++:
// HealthBarWidget.h
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "MVVMViewBase.h"
#include "CharacterStatsViewModel.h"
#include "Components/ProgressBar.h"
#include "Components/TextBlock.h"
#include "Components/Button.h"
#include "HealthBarWidget.generated.h"
UCLASS()
class YOURGAME_API UHealthBarWidget : public UUserWidget, public IMVVMViewBase
{
GENERATED_BODY()
protected:
virtual void NativeConstruct() override;
virtual void BindViewModel(UMVVMViewModelBase* InViewModel) override;
UPROPERTY(meta = (BindWidget))
UProgressBar* HealthBar;
UPROPERTY(meta = (BindWidget))
UTextBlock* HealthText;
UPROPERTY(meta = (BindWidget))
UButton* HealthKitButton;
UPROPERTY()
UCharacterStatsViewModel* ViewModel;
UFUNCTION()
void OnHealthKitButtonClicked();
};
// HealthBarWidget.cpp
#include "HealthBarWidget.h"
void UHealthBarWidget::NativeConstruct()
{
Super::NativeConstruct();
if (HealthKitButton)
{
HealthKitButton->OnClicked.AddDynamic(this, &UHealthBarWidget::OnHealthKitButtonClicked);
}
}
void UHealthBarWidget::BindViewModel(UMVVMViewModelBase* InViewModel)
{
ViewModel = Cast<UCharacterStatsViewModel>(InViewModel);
if (ViewModel)
{
// Set up bindings
if (HealthBar)
{
UMVVMBindingHelper::BindProperty(this, ViewModel, "HealthPercentage", HealthBar, "Percent");
}
if (HealthText)
{
UMVVMBindingHelper::BindProperty(this, ViewModel, "HealthText", HealthText, "Text");
}
}
}
void UHealthBarWidget::OnHealthKitButtonClicked()
{
if (ViewModel)
{
ViewModel->UseHealthKit();
}
}
Connecting Everything in Your Game
Now let’s integrate our MVVM components into a game character:
// YourCharacter.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "CharacterStatsViewModel.h"
#include "HealthBarWidget.h"
#include "YourCharacter.generated.h"
UCLASS()
class YOURGAME_API AYourCharacter : public ACharacter
{
GENERATED_BODY()
public:
AYourCharacter();
virtual void BeginPlay() override;
// Called when character takes damage
UFUNCTION(BlueprintCallable)
void ApplyDamage(float DamageAmount);
protected:
// The ViewModel instance
UPROPERTY()
UCharacterStatsViewModel* StatsViewModel;
// Widget class to instantiate
UPROPERTY(EditDefaultsOnly, Category = "UI")
TSubclassOf<UHealthBarWidget> HealthWidgetClass;
// Widget instance
UPROPERTY()
UHealthBarWidget* HealthWidget;
};
// YourCharacter.cpp
#include "YourCharacter.h"
AYourCharacter::AYourCharacter()
{
// Create and setup the ViewModel
StatsViewModel = CreateDefaultSubobject<UCharacterStatsViewModel>(TEXT("StatsViewModel"));
}
void AYourCharacter::BeginPlay()
{
Super::BeginPlay();
// Create the widget and add it to viewport
if (HealthWidgetClass)
{
HealthWidget = CreateWidget<UHealthBarWidget>(GetWorld(), HealthWidgetClass);
if (HealthWidget)
{
HealthWidget->BindViewModel(StatsViewModel);
HealthWidget->AddToViewport();
}
}
}
void AYourCharacter::ApplyDamage(float DamageAmount)
{
if (StatsViewModel && StatsViewModel->StatsModel)
{
StatsViewModel->StatsModel->Health = FMath::Max(StatsViewModel->StatsModel->Health - DamageAmount, 0.0f);
StatsViewModel->UpdateFromModel();
}
}
Advanced MVVM Techniques
Once you’ve got the basics working, you can explore these more advanced MVVM features:
1. Data Transformation
ViewModels can transform data into more view-friendly formats:
FText UCharacterStatsViewModel::GetFormattedDamageBonus()
{
float bonus = StatsModel->DamageBonus;
return FText::Format(NSLOCTEXT("CharacterStats", "DamageBonus", "+{0}%"),
FText::AsNumber(FMath::RoundToInt(bonus * 100)));
}
2. Command Binding
For handling UI actions more elegantly:
// In ViewModel
UFUNCTION()
FMVVMCommand GetUseHealthKitCommand()
{
return FMVVMCommand::CreateUObject(this, &UCharacterStatsViewModel::UseHealthKit);
}
// In View
UMVVMBindingHelper::BindCommand(this, ViewModel, "GetUseHealthKitCommand", HealthKitButton, "OnClicked");
3. Collection Binding
For lists of items:
// In ViewModel
UPROPERTY(BlueprintReadOnly, FieldNotify)
TArray<UInventoryItemViewModel*> InventoryItems;
// In View (using ListView)
UMVVMBindingHelper::BindProperty(this, ViewModel, "InventoryItems", InventoryListView, "ItemsSource");
Common Challenges and Solutions
1. Performance Considerations
MVVM can introduce overhead, especially with large numbers of bindings. Consider these optimizations:
- Use sparse updates to your model when appropriate
- Batch view updates when multiple properties change simultaneously
- Consider lower update frequencies for non-critical UI elements
2. Blueprint Integration
While this tutorial focuses on C++, you may want to expose functionality to Blueprint:
UCLASS(BlueprintType, Blueprintable)
class YOURGAME_API UCharacterStatsViewModel : public UMVVMViewModelBase
{
// ...
UFUNCTION(BlueprintCallable)
void UpdateHealthValue(float NewValue);
}
3. Managing ViewModel Lifetime
Be careful about ViewModel destruction to avoid crashes:
void AYourCharacter::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
Super::EndPlay(EndPlayReason);
if (HealthWidget)
{
HealthWidget->RemoveFromParent();
}
// Ensure any references to the ViewModel are cleared
}
Conclusion
Implementing MVVM in Unreal Engine provides a powerful way to organize your UI code, making it more maintainable and testable. While there’s an initial learning curve and setup cost, the long-term benefits for complex UIs are substantial.
For our projects at AyaDog Games, we’ve found that MVVM particularly shines for:
- Inventory systems
- Character stat displays
- Complex menus with state-dependent options
- Data-driven UIs that need to reflect changing game state
The decoupling of UI presentation from game logic makes it much easier to iterate on both independently, and to adjust UI layouts without breaking functionality.
Remember that MVVM is a tool, not a requirement - use it where it makes sense for your project’s needs and complexity level. For simple UIs, a more direct approach might be more appropriate.