Tutorial: Handling UI navigation with MVVM and Common Activatable Widgets

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