Skip to content

Commit 5707162

Browse files
committed
[MVVM] Implemented base logic to follow Model-View-ViewModel UI pattern
1 parent 1139da3 commit 5707162

File tree

13 files changed

+448
-1
lines changed

13 files changed

+448
-1
lines changed

Diff for: Bomber.uproject

+4
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@
5555
{
5656
"Name": "CineCameraRigs",
5757
"Enabled": true
58+
},
59+
{
60+
"Name": "ModelViewViewModel",
61+
"Enabled": true
5862
}
5963
]
6064
}

Diff for: Config/DefaultModelViewViewModel.ini

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[/Script/ModelViewViewModelBlueprint.MVVMDeveloperProjectSettings]
2+
; Selected 'Resolver' as default context creation type, UMVVMMyContextResolver should be always used to benefit from its features
3+
+AllowedContextCreationType=Resolver
4+
; Disabled Widget Binding, View Model window should be used instead
5+
bAllowBindingFromDetailView=False
6+
7+

Diff for: Content/Bomber/UI/InGame/WBP_InGame.uasset

-163 KB
Binary file not shown.

Diff for: Plugins/MyEditorUtils/Source/MyUtils/MyUtils.Build.cs

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ public MyUtils(ReadOnlyTargetRules Target) : base(Target)
1313
PublicDependencyModuleNames.AddRange(new[]
1414
{
1515
"Core"
16+
, "ModelViewViewModel" // Created MVVM base classes
1617
}
1718
);
1819

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// Copyright (c) Yevhenii Selivanov
2+
3+
#include "UI/ViewModel/MVVMMyContextResolver.h"
4+
//---
5+
#include "UI/ViewModel/MVVM_MyBaseViewModel.h"
6+
//---
7+
#include "Blueprint/UserWidget.h"
8+
#include "Engine/GameInstance.h"
9+
#include "MVVMGameSubsystem.h"
10+
//---
11+
#include UE_INLINE_GENERATED_CPP_BY_NAME(MVVMMyContextResolver)
12+
13+
// Is called to create a new instance of the ViewModel
14+
UObject* UMVVMMyContextResolver::CreateInstance(const UClass* ExpectedType, const UUserWidget* UserWidget, const UMVVMView* View) const
15+
{
16+
if (!ensureMsgf(ExpectedType, TEXT("ASSERT: [%i] %s:\n'ExpectedType' is nullptr"), __LINE__, *FString(__FUNCTION__))
17+
|| !ensureMsgf(ExpectedType->IsChildOf<UMVVM_MyBaseViewModel>(), TEXT("ASSERT: [%i] %s:\n'ExpectedType' is not a subclass of 'UMVVM_MyBaseViewModel'"), __LINE__, *FString(__FUNCTION__))
18+
|| !ensureMsgf(UserWidget, TEXT("ASSERT: [%i] %s:\n'UserWidget' is nullptr"), __LINE__, *FString(__FUNCTION__))
19+
|| !ensureMsgf(View, TEXT("ASSERT: [%i] %s:\n'View' is nullptr"), __LINE__, *FString(__FUNCTION__)))
20+
{
21+
return nullptr;
22+
}
23+
24+
if (UObject* SuperInstance = Super::CreateInstance(ExpectedType, UserWidget, View))
25+
{
26+
// Is created by Super
27+
return SuperInstance;
28+
}
29+
30+
const UGameInstance* GameInstance = UserWidget->GetGameInstance();
31+
if (!ensureMsgf(GameInstance, TEXT("ASSERT: [%i] %s:\n'GameInstance' is not valid!"), __LINE__, *FString(__FUNCTION__)))
32+
{
33+
return nullptr;
34+
}
35+
36+
const UMVVMGameSubsystem* MVVMSubsystem = GameInstance->GetSubsystem<UMVVMGameSubsystem>();
37+
checkf(MVVMSubsystem, TEXT("ERROR: [%i] %s:\n'MVVMSubsystem' is null!"), __LINE__, *FString(__FUNCTION__));
38+
39+
UMVVMViewModelCollectionObject* Collection = MVVMSubsystem->GetViewModelCollection();
40+
checkf(Collection, TEXT("ERROR: [%i] %s:\n'Collection' is null!"), __LINE__, *FString(__FUNCTION__));
41+
42+
// Construct a new View Model
43+
UMVVM_MyBaseViewModel* NewViewModel = NewObject<UMVVM_MyBaseViewModel>(UserWidget->GetOwningPlayer(), ExpectedType);
44+
NewViewModel->OnViewModelConstruct(UserWidget);
45+
46+
FMVVMViewModelContext Context;
47+
Context.ContextClass = NewViewModel->GetClass();
48+
Context.ContextName = NewViewModel->GetFName();
49+
50+
Collection->AddViewModelInstance(MoveTemp(Context), NewViewModel);
51+
52+
return NewViewModel;
53+
}
54+
55+
// Is called to destroy the instance of the ViewModel
56+
void UMVVMMyContextResolver::DestroyInstance(const UObject* ViewModel, const UMVVMView* View) const
57+
{
58+
UMVVM_MyBaseViewModel* MyBaseViewModel = const_cast<UMVVM_MyBaseViewModel*>(Cast<UMVVM_MyBaseViewModel>(ViewModel));
59+
if (!ensureMsgf(MyBaseViewModel, TEXT("ASSERT: [%i] %s:\n'MyBaseViewModel' is nullptr"), __LINE__, *FString(__FUNCTION__))
60+
|| !ensureMsgf(View, TEXT("ASSERT: [%i] %s:\n'View' is nullptr"), __LINE__, *FString(__FUNCTION__)))
61+
{
62+
return;
63+
}
64+
65+
Super::DestroyInstance(ViewModel, View);
66+
if (!IsValid(MyBaseViewModel))
67+
{
68+
// Is destroyed by Super
69+
return;
70+
}
71+
72+
// Get Game Instance from View
73+
const UGameInstance* GameInstance = MyBaseViewModel->GetWorld()->GetGameInstance();
74+
if (!ensureMsgf(GameInstance, TEXT("ASSERT: [%i] %s:\n'GameInstance' is not valid!"), __LINE__, *FString(__FUNCTION__)))
75+
{
76+
return;
77+
}
78+
79+
const UMVVMGameSubsystem* MVVMSubsystem = GameInstance->GetSubsystem<UMVVMGameSubsystem>();
80+
checkf(MVVMSubsystem, TEXT("ERROR: [%i] %s:\n'MVVMSubsystem' is null!"), __LINE__, *FString(__FUNCTION__));
81+
82+
UMVVMViewModelCollectionObject* Collection = MVVMSubsystem->GetViewModelCollection();
83+
checkf(Collection, TEXT("ERROR: [%i] %s:\n'Collection' is null!"), __LINE__, *FString(__FUNCTION__));
84+
Collection->RemoveAllViewModelInstance(MyBaseViewModel);
85+
86+
// Destroy the View Model
87+
MyBaseViewModel->OnViewModelDestruct();
88+
MyBaseViewModel->ConditionalBeginDestroy();
89+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// Copyright (c) Yevhenii Selivanov
2+
3+
#include "UI/ViewModel/MVVM_MyBaseViewModel.h"
4+
//---
5+
#include UE_INLINE_GENERATED_CPP_BY_NAME(MVVM_MyBaseViewModel)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Copyright (c) Yevhenii Selivanov
2+
3+
#pragma once
4+
5+
#include "View/MVVMViewModelContextResolver.h"
6+
//---
7+
#include "MVVMMyContextResolver.generated.h"
8+
9+
/**
10+
* This resolver automatically creates and destroys the View Models of given Views.
11+
* The main feature of this Resolver is calling extra events that original View Model does not have.
12+
* For instance, 'OnViewModelConstruct' event that allows a View to bind itself to different delegates in order to update own data.
13+
* To make it work:
14+
* 1. Widget has to have 'Creation Type' selected as 'Resolver' with this class.
15+
* 2. View Model has to be a subclass of 'UMVVM_MyBaseViewModel'.
16+
*/
17+
UCLASS(Blueprintable, BlueprintType, DisplayName = "My Context Resolver")
18+
class MYUTILS_API UMVVMMyContextResolver : public UMVVMViewModelContextResolver
19+
{
20+
GENERATED_BODY()
21+
22+
protected:
23+
/** Is called to create a new instance of the ViewModel.
24+
* To make it call, a Widget has to have 'Creation Type' selected as 'Resolver' with this class. */
25+
virtual UObject* CreateInstance(const UClass* ExpectedType, const UUserWidget* UserWidget, const UMVVMView* View) const override;
26+
27+
/** Is called to destroy the instance of the ViewModel. */
28+
virtual void DestroyInstance(const UObject* ViewModel, const UMVVMView* View) const override;
29+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright (c) Yevhenii Selivanov
2+
3+
#pragma once
4+
5+
#include "MVVMViewModelBase.h"
6+
//---
7+
#include "MVVM_MyBaseViewModel.generated.h"
8+
9+
/**
10+
* Base class for all View Model (MVVM) views.
11+
* Is connected to the widget to provide access UI data.
12+
* Widget has to have 'Creation Type' selected as 'Resolver' with 'MVVM_ByBaseContextResolved' class.
13+
*/
14+
UCLASS(Blueprintable, BlueprintType, DisplayName = "My Base View Model")
15+
class MYUTILS_API UMVVM_MyBaseViewModel : public UMVVMViewModelBase
16+
{
17+
GENERATED_BODY()
18+
19+
public:
20+
/** Is called when this View Model is constructed.
21+
* Is used for bindings to the changes in other systems in order to update own data. */
22+
UFUNCTION(BlueprintCallable, Category = "C++", BlueprintNativeEvent)
23+
void OnViewModelConstruct(const class UUserWidget* UserWidget);
24+
virtual void OnViewModelConstruct_Implementation(const class UUserWidget* UserWidget) {}
25+
26+
/** Is called when this View Model is destructed. */
27+
UFUNCTION(BlueprintCallable, Category = "C++", BlueprintNativeEvent)
28+
void OnViewModelDestruct();
29+
virtual void OnViewModelDestruct_Implementation() {}
30+
};

Diff for: Source/Bomber/Bomber.Build.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public Bomber(ReadOnlyTargetRules Target) : base(Target)
2121
, "FunctionPicker" // Created properties in UMyInputAction
2222
, "MetaCheatManager" // Created UMyCheatManager
2323
, "PoolManager" // Created property in FMapComponentSpec
24+
, "MyUtils" // Inherited from Base classes
2425
}
2526
);
2627

@@ -33,8 +34,8 @@ public Bomber(ReadOnlyTargetRules Target) : base(Target)
3334
, "Niagara" // VFX
3435
, "GameplayTags" // FGameplayTag
3536
, "GameFeatures", "ModularGameplay" // Modular Game Features
37+
, "ModelViewViewModel" // MVVM UI pattern
3638
//My modules
37-
, "MyUtils" // UUtilsLibrary
3839
, "SettingsWidgetConstructor" // Generates settings
3940
}
4041
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright (c) Yevhenii Selivanov
2+
3+
#include "UI/ViewModel/MVVM_MyGameViewModel.h"
4+
//---
5+
#include "Subsystems/GlobalEventsSubsystem.h"
6+
//---
7+
#include UE_INLINE_GENERATED_CPP_BY_NAME(MVVM_MyGameViewModel)
8+
9+
// Setter and Getter widgets about the current game state
10+
void UMVVM_MyGameViewModel::SetCurrentGameState(ECurrentGameState NewCurrentGameState)
11+
{
12+
UE_MVVM_SET_PROPERTY_VALUE(CurrentGameState, NewCurrentGameState);
13+
}
14+
15+
/*********************************************************************************************
16+
* Events
17+
********************************************************************************************* */
18+
19+
// Is called when the view is constructed
20+
void UMVVM_MyGameViewModel::OnViewModelConstruct_Implementation(const UUserWidget* UserWidget)
21+
{
22+
Super::OnViewModelConstruct_Implementation(UserWidget);
23+
24+
UGlobalEventsSubsystem::Get().OnGameStateChanged.AddUniqueDynamic(this, &ThisClass::SetCurrentGameState);
25+
}
26+
27+
// Is called when this View Model is destructed
28+
void UMVVM_MyGameViewModel::OnViewModelDestruct_Implementation()
29+
{
30+
Super::OnViewModelDestruct_Implementation();
31+
32+
if (UGlobalEventsSubsystem* GlobalEventsSubsystem = UGlobalEventsSubsystem::GetGlobalEventsSubsystem())
33+
{
34+
GlobalEventsSubsystem->OnGameStateChanged.RemoveAll(this);
35+
}
36+
}

Diff for: Source/Bomber/Private/UI/ViewModel/MVVM_MyHUD.cpp

+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Copyright (c) Yevhenii Selivanov
2+
3+
#include "UI/ViewModel/MVVM_MyHUD.h"
4+
//---
5+
#include "GameFramework/MyGameStateBase.h"
6+
#include "LevelActors/PlayerCharacter.h"
7+
#include "Subsystems/GlobalEventsSubsystem.h"
8+
#include "UtilityLibraries/MyBlueprintFunctionLibrary.h"
9+
//---
10+
#include UE_INLINE_GENERATED_CPP_BY_NAME(MVVM_MyHUD)
11+
12+
/*********************************************************************************************
13+
* Countdown timers
14+
********************************************************************************************* */
15+
16+
// Setter about the summary seconds of launching 'Three-two-one-GO' timer that is used on game starting
17+
void UMVVM_MyHUD::SetStartingTimerSecRemain(const FText& NewStartingTimerSecRemain)
18+
{
19+
UE_MVVM_SET_PROPERTY_VALUE(StartingTimerSecRemain, NewStartingTimerSecRemain);
20+
}
21+
22+
// Setter about the seconds to the end of the round
23+
void UMVVM_MyHUD::SetInGameTimerSecRemain(const FText& NewInGameTimerSecRemain)
24+
{
25+
UE_MVVM_SET_PROPERTY_VALUE(InGameTimerSecRemain, NewInGameTimerSecRemain);
26+
}
27+
28+
// Called when the 'Three-two-one-GO' timer was updated
29+
void UMVVM_MyHUD::OnStartingTimerSecRemainChanged_Implementation(float NewStartingTimerSecRemain)
30+
{
31+
const int32 Value = FMath::CeilToInt(NewStartingTimerSecRemain);
32+
SetStartingTimerSecRemain(FText::AsNumber(Value));
33+
}
34+
35+
// Called when remain seconds to the end of the match timer was updated
36+
void UMVVM_MyHUD::OnInGameTimerSecRemainChanged_Implementation(float NewInGameTimerSecRemain)
37+
{
38+
const int32 Value = FMath::CeilToInt(NewInGameTimerSecRemain);
39+
SetInGameTimerSecRemain(FText::AsNumber(Value));
40+
}
41+
42+
/*********************************************************************************************
43+
* PowerUps
44+
********************************************************************************************* */
45+
46+
// Setter about current amount of speed power-ups
47+
void UMVVM_MyHUD::SetPowerUpSkate(const FText& NewPowerUpSkate)
48+
{
49+
UE_MVVM_SET_PROPERTY_VALUE(PowerUpSkate, NewPowerUpSkate);
50+
}
51+
52+
// Setter about current amount of max bomb power-ups
53+
void UMVVM_MyHUD::SetPowerUpBomb(const FText& NewPowerUpBomb)
54+
{
55+
UE_MVVM_SET_PROPERTY_VALUE(PowerUpBomb, NewPowerUpBomb);
56+
}
57+
58+
// Setter about current amount of blast radius power-ups
59+
void UMVVM_MyHUD::SetPowerUpFire(const FText& NewPowerUpFire)
60+
{
61+
UE_MVVM_SET_PROPERTY_VALUE(PowerUpFire, NewPowerUpFire);
62+
}
63+
64+
// Called when power-ups were updated
65+
void UMVVM_MyHUD::OnPowerUpsChanged_Implementation(const FPowerUp& NewPowerUps)
66+
{
67+
SetPowerUpSkate(FText::AsNumber(NewPowerUps.SkateN));
68+
SetPowerUpBomb(FText::AsNumber(NewPowerUps.BombN));
69+
SetPowerUpFire(FText::AsNumber(NewPowerUps.FireN));
70+
}
71+
72+
/*********************************************************************************************
73+
* Events
74+
********************************************************************************************* */
75+
76+
// Is called when the view is constructed
77+
void UMVVM_MyHUD::OnViewModelConstruct_Implementation(const UUserWidget* UserWidget)
78+
{
79+
Super::OnViewModelConstruct_Implementation(UserWidget);
80+
81+
BIND_AND_CALL_ON_LOCAL_PLAYER_READY(this, ThisClass::OnLocalPlayerReady);
82+
}
83+
84+
// Is called when this View Model is destructed
85+
void UMVVM_MyHUD::OnViewModelDestruct_Implementation()
86+
{
87+
Super::OnViewModelDestruct_Implementation();
88+
89+
if (AMyGameStateBase* MyGameState = UMyBlueprintFunctionLibrary::GetMyGameState())
90+
{
91+
MyGameState->OnStartingTimerSecRemainChanged.RemoveAll(this);
92+
MyGameState->OnInGameTimerSecRemainChanged.RemoveAll(this);
93+
}
94+
95+
if (APlayerCharacter* Player = UMyBlueprintFunctionLibrary::GetLocalPlayerCharacter())
96+
{
97+
Player->OnPowerUpsChanged.RemoveAll(this);
98+
}
99+
}
100+
101+
// Called when local player character was spawned and possessed, so we can bind to data
102+
void UMVVM_MyHUD::OnLocalPlayerReady(APlayerCharacter* PlayerCharacter)
103+
{
104+
checkf(PlayerCharacter, TEXT("ERROR: [%i] %s:\n'PlayerCharacter' is null!"), __LINE__, *FString(__FUNCTION__));
105+
PlayerCharacter->OnPowerUpsChanged.AddUniqueDynamic(this, &ThisClass::OnPowerUpsChanged);
106+
107+
AMyGameStateBase& MyGameState = AMyGameStateBase::Get();
108+
MyGameState.OnStartingTimerSecRemainChanged.AddUniqueDynamic(this, &ThisClass::OnStartingTimerSecRemainChanged);
109+
MyGameState.OnInGameTimerSecRemainChanged.AddUniqueDynamic(this, &ThisClass::OnInGameTimerSecRemainChanged);
110+
}
+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright (c) Yevhenii Selivanov
2+
3+
#pragma once
4+
5+
#include "Bomber.h"
6+
#include "UI/ViewModel/MVVM_MyBaseViewModel.h"
7+
//---
8+
#include "MVVM_MyGameViewModel.generated.h"
9+
10+
enum class ECurrentGameState : uint8;
11+
12+
/**
13+
* Contains general data to be used only by widgets.
14+
*/
15+
UCLASS(DisplayName = "My Game View Model")
16+
class BOMBER_API UMVVM_MyGameViewModel : public UMVVM_MyBaseViewModel
17+
{
18+
GENERATED_BODY()
19+
20+
public:
21+
/** Setter and Getter widgets about the current game state. */
22+
UFUNCTION()
23+
void SetCurrentGameState(ECurrentGameState NewCurrentGameState);
24+
ECurrentGameState GetCurrentGameState() const { return CurrentGameState; }
25+
26+
protected:
27+
/** Represents the current game state.
28+
* Is commonly used by 'UMyBlueprintFunctionLibrary::GetVisibilityByGameState' to show or hide own widget. */
29+
UPROPERTY(BlueprintReadWrite, Transient, FieldNotify, Setter, Getter, Category = "C++")
30+
ECurrentGameState CurrentGameState = ECurrentGameState::None;
31+
32+
/*********************************************************************************************
33+
* Events
34+
********************************************************************************************* */
35+
protected:
36+
/** Is called when this View Model is constructed.
37+
* Is used for bindings to the changes in other systems in order to update own data. */
38+
virtual void OnViewModelConstruct_Implementation(const UUserWidget* UserWidget) override;
39+
40+
/** Is called when this View Model is destructed. */
41+
virtual void OnViewModelDestruct_Implementation() override;
42+
};

0 commit comments

Comments
 (0)