Tutorial: Handling UI navigation with MVVM and Common Activatable Widgets

Just randomly stumbled upon this thread.

I don’t really remember the details of the bluperint-based resolver nightmare I cooked during the lab at Orlando anymore but I’d agree that the fact one can create global resolver in BP shouldn’t mean it should be done in BP unless the project is blueprint-only project.

Now, for those that are interested in really barebones C++ implementation of the global resolver here it is (there are huge limitations, described below):

// YourGameNameGlobalVMResolver.h
// Your Copyright Notice

#pragma once

#include "CoreMinimal.h"
#include "View/MVVMViewModelContextResolver.h"
#include "UObject/ObjectKey.h"

#include "YourGameNameGlobalVMResolver.generated.h"

class UMVVMViewModelBase;

/**
 * Bare minimum implementation of a global view model resolver.
 */
UCLASS(MinimalAPI)
class UYourGameNameGlobalVMResolver : public UMVVMViewModelContextResolver
{
	GENERATED_BODY()
	
public:
	virtual UObject* CreateInstance(const UClass* ExpectedType, const UUserWidget* UserWidget, const UMVVMView* View) const override;
	virtual void DestroyInstance(const UObject* ViewModel, const UMVVMView* View) const override;
	
private:
	mutable TMap<FObjectKey, TWeakObjectPtr<UMVVMViewModelBase>> ViewModelCache;
};

// YourGameNameGlobalVMResolver.cpp
// Your Copyright Notice

#include "ViewModels/YourGameNameGlobalVMResolver.h"
#include "MVVMViewModelBase.h"

#include UE_INLINE_GENERATED_CPP_BY_NAMEYourGameNameGlobalVMResolver)

UObject* UYourGameNameGlobalVMResolver::CreateInstance(const UClass* ExpectedType, const UUserWidget* UserWidget, const UMVVMView* View) const
{
	// Check if the ViewModel is already cached
	if (const TWeakObjectPtr<UMVVMViewModelBase>* FoundPtr = ViewModelCache.Find(ExpectedType))
	{
		if (UMVVMViewModelBase* FoundViewModel = FoundPtr->Get())
		{
			return FoundViewModel;
		}
		else
		{
			ViewModelCache.Remove(ExpectedType);
		}
	}

	// Create a new instance since none was found or the cached one was invalid
	UMVVMViewModelBase* NewViewModel = NewObject<UMVVMViewModelBase>(GetTransientPackage(), ExpectedType);
	if (NewViewModel)
	{
		ViewModelCache.Add(ExpectedType, NewViewModel);
	}
	return NewViewModel;
}

void UYourGameNameGlobalVMResolver::DestroyInstance(const UObject* ViewModel, const UMVVMView* View) const
{
	// Do nothing for now
}

So, now to the limitations… this is pretty much the most basic resolver you can write and is actually something that’s not intended to work like this (this class is intended to work as a accessor for other subsystem etc.)… This implementation does not distinguish between different players, nor does it ever destroy the VMs themself (unless they expire because ref-counts), it just purely handles the case where you want to dynamically share one instance between multiple widgets. Due to the fact that the resolver is source of the objects themself it’s impossible to access them from C++ which pretty much beats the main reason you’d want to deal with C++ in the first place.

Proper implementation should for example call into a GameInstance subsystem which would handle caching per local player etc. and in C++ you’d access the contexts through said subsystem.

When I’m less busy later today I’ll try to factor out the implementation we’ve been using and provide somewhat real-life example here.

2 Likes

Ok, it’s taken me a bit more time than I wanted to…

but here’s the more advanced implementation of the VM resolver, I’ve made it into a plugin, though as it’s pulled pretty much straight out of our game all the classes still reference “MATCHO”… the plugin ideally isn’t made to be directly used by anoter project (although it still can) but rather it’s intended to be copied and used directly in your game code/as a reference.

In our case the code lies in “MatchoUI” module which part of our main game project

Here’s the code : GitHub - klukule/GlobalVMResolver: Example implementation of global view-model resolver for global and player-scoped viewmodels. Using Unreal's MVVM plugin

Some parts of the code are bit messy as they’re pretty much unused/old code (for example view models with manual lifetime, nowadays we exclusively use viewmodels that are created by the widgets and are destroyed once last reference is released of relying on manual creation and destruction, but remnants of the code are still there)

Here’s example usecase from our game:

UDialogSystemSubsystem* DialogSystem = ...;
APlayerController* PC = ...;

if (SelectionEntries.Num() > 0)
{
	TArray<TObjectPtr<UDialogChoiceViewModel>> Choices;
	TObjectPtr<UDialogChoiceViewModel> FocusedChoice = nullptr;
	for (const auto& SelectionEntry : SelectionEntries)
	{
		const FFlowNode_DialogSelectionEntry Entry = SelectionEntry.Value;

		if (!Entry.EvaluateVisibility(InWorld))
		{
			continue;
		}

		// Build the choice
		UDialogChoiceViewModel* Choice = NewObject<UDialogChoiceViewModel>();
		Choice->SetChoiceId(SelectionEntry.Key);
		Choice->SetIconStyle(Entry.IconStyle);
		Choice->SetChoiceText(Entry.ChoiceText);
		Choice->SetIsVisited(Entry.EvaluateVisited(InWorld));
		// Choice->SetIsVisible(Entry.EvaluateVisibility(InWorld));
		Choice->SetIsEnabled(Entry.EvaluateEnabled(InWorld));
		Choices.Add(Choice);
		
		if (!FocusedChoice && Choice->IsEnabled())
		{
			FocusedChoice = Choice;
		}
	}

	// Show choices if at least one passed visibility filtering
	if (Choices.Num() > 0)
	{
		// Push choice widget for this player
		ChoiceWidgetInstance = DialogSystem->PushChoiceWidget(PC);

		// Fetch view models
		UDialogChoiceListViewModel* ListVM = UMatchoViewModelsManager::FindViewModel<UDialogChoiceListViewModel>(PC);
		UDialogChoiceSelectionViewModel* SelectionVM = UMatchoViewModelsManager::FindViewModel<UDialogChoiceSelectionViewModel>(PC);

		// Register to selection change
		SelectionVM->AddFieldValueChangedDelegate(UDialogChoiceSelectionViewModel::FFieldNotificationClassDescriptor::SelectedChoice, UDialogChoiceSelectionViewModel::FFieldValueChangedDelegate::CreateUObject(this, &ThisClass::OnChoiceSelected));

		// Fill the list and set the first choice as focused
		ListVM->SetChoices(Choices);
		SelectionVM->SetFocusedChoice(FocusedChoice);
	}
}

Another example would be in case you have a Widget defined in C++ and want to use the manager then:

void UMyAwesomeWidget::NativeOnInitialized()
{
	Super::NativeOnInitialized();
	if (!IsDesignTime())
	{
		if(UGameInstance* GameInstance = GetGameInstance())
		{
			if(UMatchoViewModelsManager* VMManager = GameInstance->GetSubsystem<UMatchoViewModelsManager>())
			{
				MyAwesomeVM = VMManager->GetOrCreateViewModel(UMyAwesomeViewModel::StaticClass(), this);
			}
		}
	}
}

void UMyAwesomeWidget::NativeDestruct()
{
	if(UGameInstance* GameInstance = GetGameInstance())
	{
		if(UMatchoViewModelsManager* VMManager = GameInstance->GetSubsystem<UMatchoViewModelsManager>())
		{
			VMManager->RemoveWiewModel(MyAwesomeVM);
		}
	}
}

(this method is used exactly once in our code, otherwise we have everything in BP and just fetch stuff)

These are pretty much all our use-cases in our game, either widget creates the VM in C++ or in BP… and external code just uses the FindViewModel function

2 Likes

Hi @klukule thankyou so much for sharing.

As with manual creation destruction, you you “remove viewmodel every time a widget is closed or deactivated? There isnt much documentation what is needed to destroy the viewmodel after each use. I know Im creating a new viewmodel every time I open my widget(s), this is a problem Im sure, without the knowledge to properly destroy them.

Thankyou in advance!

Simon.

I’m not sure I understand your question, so if I’m not answering it properly etc. let me know… but here we go

we pretty much stopped using manually created/destroyed view models through resolver… since resolvers’s job is to provide shared instance that multiple widgets can bind to (in case of one of the examples I provided it would be the DialogChoiceSelectionVM and DialogChoiceListVM) meanwhile manually created view models are created and shared separately (for example the DialogChoiceVM)

In terms of destroying view models… we let unreal’s garbage collector take care of things, the moment its not used unreal will throw it away and free up the memory

As for when/how often you create specific view models it depends…. View model sits between the view (widget) and model (your code/logic) and usually represents some kind of data from the model layer … so you create your view when you need to put some data into it… there are certain view that live for a long time… for example “PlayerHealthVM” or “InventoryVM” … these exists as long as player exists but then there are some thst are more transient, for example “MapVM” which would get created and populated with POIs the moment you’d open your ingame map… and would get destroyed (left behind for unreal to deal with) the moment you’d close the window again, next time you’d open the map again, new one would be created and populated (in this case you’d use the example shown in “UMyAwesomeWidget”)

1 Like

Yes i believe you understood my question, thankyou.

I will give an example of the way I am implementing viewmodels so I can understand your knowledge:

I have a player inventory screen. Whenever that player inventory screen is triggered to open (activatable widget pushed to a stack) I create a viewmodel of each item and set their values. This all works fine.

I think you answered my question when the player CLOSES this screen but i just want to make sure im getting this right:

From what I understand from your answer Unreal will deal with those viewmodels with its garbage collection so i dont need to cache them and destory them manually. So it is safe/best practice to construct those viewmodels EVERY time the player inventory screen is opened and then let Unreal handle it once it is closed?

I just got concerned that if the inventory screen is opened 1000 times during gameplay then there will be a ridiculous amount of constructed Viewmodels in the background just hanging out, so to speak.

Thanks again for answering my questions it is helping a heck of alot.

This is fantastic, thanks Lukas! Coming in clutch once again :smiley:

1 Like

Hey Vitor!

I will make a tutorial about generic MVVM soon.

Just want to specify a few things:

  • conversion functions don’t need input and output values of the same tpye. The requirements for conversion functions are:
    • needs to be PURE and CONST
    • needs to have a return value
    • can have any number of input parameters, only one output
  • then, in order to use the conversion function in a view binding, you will want the output type of the conversion function to be the same type as the other side of the view binding.
    • for example, you want to use a bunch of VM properties to determine the visibility of a certain element (say that you have a panel that you want to collapse under certain conditions). You would create a conversion function that takes in the parameters you need, and outputs the visibility as a E_SlateVisibility enum. Then you can bind that directly to the visibility property of the panel you want to show/collapse
    • another example, you have an array of objects that you want to filter based on some other VM condition. So in this case you have 2 input parameters in your conversion function: the array and the condition (a bool for example). Your conversion function does the calculations to filter or not the array, and outputs an array of the same type as the input one, just filtered. Then you might bind this to another function that takes the array as input parameter and, for example, instantiates a widget for each item in the array.
    • in any case, whenever you need to use more than one input parameter in a view binding, you need to use a conversion function.

Regarding your other question:
”can I use the viewmodel to store and calculate gameplay status like health and mana in a PlayerController or it is something that should be strict to widgets?”

You can definitely use a view model to store information like health and mana. But the MVVM plugin is only for UMG, so what ideally you would do is:

  • your view model class has a reference of the PlayerController (or other systems you need) and updates its properties when gameplay stuff happens (health changes, healing and so on)
  • your UI is using the view model to display information accordingly, implementing a view for that view model

The last distinction: MVVM is a plugin for UMG, but it’s also a programming pattern. So you could technically implement the same pattern in your code without using the plugin in UMG. I would need to check if you can use the MVVM macros and functions in code for other objects that aren’t widgets, but I think so.

I hope this helps!

The suggested “sequel tutorial”, using MVVM to populate the widgets, would be really helpfull, it would also help to understand this tutorial even better.

1 Like

It is super helpful, thank you Irene! My brain keeps forgeting about conversion functions everytime.

About the stats, I ended up storing the health in the characters classes and then in their blueprints there is a OnHealthChange thats sends the values to the PlayerState or the AIController in case of a NPC, the ActionRPG example was really helpfull. Characters are now all connected to the UI seamlessly. :slightly_smiling_face: :sign_of_the_horns:

Fantastic tutorial! Really helped me understand the concept and how to use it.
I also found it very useful to be able to download the sample project and pick it apart.

I do have one follow-up question: What’s the intended way to feed data (or events) back from the UI to my model (e.g. the player character)? Is this supposed to go through the ViewModel as well?

One example for this would be your inventory: As soon as I equip one item, how would I - in theory - need to call a function like AMyPlayerChar::EquipItem(ItemBaseClass* Item)?

Not looking for specific implementation here, but a high level overview of how this should or could be handled.

ViewModels are bi-directional (or technically speaking directionless as they just sit between two sides… the logic and the ui and share data using event-driven aproach)

so in C++ or BP you simply add new entry to the array and fire the notifies if not done automatically (working with arrays is kinda funky if I remember correctly)

and then your BP/C++ code can register necessary notifies to receive notification about that

Practical example of this would be the concept of Selection View Model where UI “events” (selected item change etc.) are fed into the view-model and another piece of code elsewhere reacts on them

It’s worth nothing that view-model itself operates on data, so you have to change some data to trigger the events, there is no concept of events/delegates on their own.

Also some of my code here Tutorial: Handling UI navigation with MVVM and Common Activatable Widgets - #23 by klukule might be useful reference (not strictly the global VM itself, but the accompanying example code)