Search Unity

  1. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

How to use MVVM in Unity

Discussion in 'Assets and Asset Store' started by LoxodonStudio, Jan 9, 2023.

  1. LoxodonStudio

    LoxodonStudio

    Joined:
    Nov 1, 2016
    Posts:
    40
    Unity-MVVM

    In Unity3D game development, the MVVM framework can be used very well. The following is a description of how the Unity project is layered.

    View

    The view layer generally includes windows, view scripts, UI controls, animation scripts, view resources, and other view layer auxiliary tools, such as view locators. Specifically, you can abstract and plan according to your own project situation.

    Window/UIView

    Window and view scripts control the life cycle of all views, such as the creation and destruction of subviews and subwindows should be written in this layer of code. If the logic of opening and closing the interface is triggered by functions in the ViewModel layer, then use IDialogService or exchange requests to send events to the view script for execution.

    UI controls (UGUI controls or custom controls)

    UI control layer, custom UI controls should be written in this layer, and it is strongly recommended that UI functions be controlled, such as lists, dialog boxes, progress bars, Grid, Menu, etc. should be written as universal UI controls.

    Animation

    UI animation layer, for example, you can use DoTween to write various window animations or interface animations, and directly hang them on the UI GameObject. You can refer to my example to write. If it is a window animation, please inherit my UIAnimation or use GenericUIAnimation to implement.

    View locator(IUIViewLocator)

    View locator, which uses the view locator to load view templates from Resources or from AssetBundle. Of course, you can refer to my UI view locator to write your own 3D view locator.

    Interation Action

    Interaction behavior. This is an abstraction for window and view creation code reuse. It encapsulates some frequently used interface creation code as interaction behavior.

    ViewModel

    The view model layer contains all the view models and subview models. Generally, the view models of Window and View are paired one by one. A window must have a view model, and the subviews under a window should generally have corresponding subviews. model. However, pure view object-encapsulated subview models, such as UserInfoVM, can be shared by multiple views, and when the UserInfoVM property changes, multiple interfaces bound to it will change at the same time.

    View model locator (IViewModelLocator)

    View model locator, which is used to manage the shared sub-view model, or to save the window view model (such as the window is closed but the view model is not destroyed, the next time you open the window, you can use it to restore the window state). This layer is not necessary, it can be omitted or replaced with another solution.

    Application layer (Service)

    The application layer is mainly used to express user use cases and coordinate the behavior between objects in different domains. If the design concept of the DDD congestion model is adopted, it is only a very thin layer. It exists as a bridge between the presentation layer and the domain layer, and provides services for the presentation layer through application services. If the design idea of the traditional anemia model is adopted, it should include all the business logic processing. I don't know much about DDD programming among the students who use the framework, so here I recommend the design idea of the traditional anemia model to develop the game. In my project example, it corresponds to the Services layer.

    For example, a game project may include character services, backpack services, equipment services, skill services, chat services, item services, and so on. These services are used to manage character information, items in the backpack, user equipment, user learned skills, chat information , Chat room information, and more. The service caches this information and ensures that they are synchronized with the server through Load or server push. When there is a message update, an event is triggered to notify the view model layer to update. For example, various red dots on the main interface (the state that prompts a new message) can be designed through the events of each service and the red dot status on the view model.

    Domain Model

    The domain layer is responsible for the expression and processing of business logic and is the core of the entire business. If you program according to DDD, the domain layer generally includes concepts such as entities, value objects, domain services, aggregation, aggregation roots, storage, and factories, because the concepts involved are numerous and persistence needs to be compatible with the CQRS + ES model, and there are considerable thresholds to master. So if you are not very familiar with DDD, I don't recommend designing your code completely according to the DDD programming idea, but adopt the idea of the anemia model. Below, I will only make a simple idea of some concepts to be used in the anemia model. Introduction.

    Entity

    Entities must have unique identifiers, such as objects such as account numbers, characters, skills, equipment, and props in the game, which are all entity objects.

    Value Object

    The value object is used to describe an object in a certain aspect of the domain that does not have a conceptual identifier. The value object is different from the entity. It has no unique identifier and its attributes are immutable, such as some game table information.

    Repository

    The warehousing layer is responsible for functions such as adding, deleting, modifying, and checking the entity objects. Through the warehousing layer, you can read data or persist data. The data can be saved in local Json, xml, SQLite, or on the server through the network.

    Infrastructure

    The base layer contains the framework, database access components, network components, Log components, Protobuf components, public helper classes and methods.

    I'm the author of LoxodonFramework, here are some examples to show the relationship between View and ViewModel.

    View:

    Code (CSharp):
    1. using Loxodon.Framework.Binding;
    2. using Loxodon.Framework.Binding.Builder;
    3. using Loxodon.Framework.Interactivity;
    4. using Loxodon.Framework.Views;
    5. using Loxodon.Log;
    6. using UnityEngine.UI;
    7.  
    8. namespace Loxodon.Framework.Examples
    9. {
    10.    public class LoginWindow : Window
    11.    {
    12.    //private static readonly ILog log = LogManager.GetLogger(typeof(LoginWindow));
    13.  
    14.    public InputField username;
    15.    public InputField password;
    16.    public Text usernameErrorPrompt;
    17.    public Text passwordErrorPrompt;
    18.    public Button confirmButton;
    19.    public Button cancelButton;
    20.  
    21.  
    22.    protected override void OnCreate(IBundle bundle)
    23.    {
    24.        BindingSet<LoginWindow, LoginViewModel> bindingSet = this.CreateBindingSet<LoginWindow, LoginViewModel>();
    25.        bindingSet.Bind().For(v => v.OnInteractionFinished).To(vm => vm.InteractionFinished);
    26.        bindingSet.Bind().For(v => v.OnToastShow).To(vm => vm.ToastRequest);
    27.  
    28.        bindingSet.Bind(this.username).For(v => v.text, v => v.onEndEdit).To(vm => vm.Username).TwoWay();
    29.        bindingSet.Bind(this.usernameErrorPrompt).For(v => v.text).To(vm => vm.Errors["username"]).OneWay();
    30.        bindingSet.Bind(this.password).For(v => v.text, v => v.onEndEdit).To(vm => vm.Password).TwoWay();
    31.        bindingSet.Bind(this.passwordErrorPrompt).For(v => v.text).To(vm => vm.Errors["password"]).OneWay();
    32.        bindingSet.Bind(this.confirmButton).For(v => v.onClick).To(vm => vm.LoginCommand);
    33.        bindingSet.Bind(this.cancelButton).For(v => v.onClick).To(vm => vm.CancelCommand);
    34.        bindingSet.Build();
    35.    }
    36.  
    37.    public virtual void OnInteractionFinished(object sender, InteractionEventArgs args)
    38.    {
    39.        this.Dismiss();
    40.    }
    41.  
    42.    public virtual void OnToastShow(object sender, InteractionEventArgs args)
    43.    {
    44.        Notification notification = args.Context as Notification;
    45.        if (notification == null)
    46.        return;
    47.  
    48.        Toast.Show(this, notification.Message, 2f);
    49.    }
    50.    }
    51. }
    ViewModel:

    Code (CSharp):
    1. using System;
    2. using System.Text.RegularExpressions;
    3.  
    4. using Loxodon.Log;
    5. using Loxodon.Framework.Contexts;
    6. using Loxodon.Framework.Prefs;
    7. using Loxodon.Framework.Asynchronous;
    8. using Loxodon.Framework.Commands;
    9. using Loxodon.Framework.ViewModels;
    10. using Loxodon.Framework.Localizations;
    11. using Loxodon.Framework.Observables;
    12. using Loxodon.Framework.Interactivity;
    13.  
    14. namespace Loxodon.Framework.Examples
    15. {
    16.     public class LoginViewModel : ViewModelBase
    17.     {
    18.     private static readonly ILog log = LogManager.GetLogger(typeof(ViewModelBase));
    19.  
    20.     private const string LAST_USERNAME_KEY = "LAST_USERNAME";
    21.  
    22.     private ObservableDictionary<string, string> errors = new ObservableDictionary<string, string>();
    23.     private string username;
    24.     private string password;
    25.     private SimpleCommand loginCommand;
    26.     private SimpleCommand cancelCommand;
    27.  
    28.     private Account account;
    29.  
    30.     private Preferences globalPreferences;
    31.     private IAccountService accountService;
    32.     private Localization localization;
    33.  
    34.     private InteractionRequest interactionFinished;
    35.     private InteractionRequest<Notification> toastRequest;
    36.  
    37.     public LoginViewModel(IAccountService accountService, Localization localization, Preferences globalPreferences)
    38.     {
    39.         this.localization = localization;
    40.         this.accountService = accountService;
    41.         this.globalPreferences = globalPreferences;
    42.  
    43.         this.interactionFinished = new InteractionRequest(this);
    44.         this.toastRequest = new InteractionRequest<Notification>(this);
    45.  
    46.         if (this.username == null)
    47.         {
    48.         this.username = globalPreferences.GetString(LAST_USERNAME_KEY, "");
    49.         }
    50.  
    51.         this.loginCommand = new SimpleCommand(this.Login);
    52.         this.cancelCommand = new SimpleCommand(() =>
    53.         {
    54.         this.interactionFinished.Raise();/* Request to close the login window */
    55.         });
    56.     }
    57.  
    58.     public IInteractionRequest InteractionFinished
    59.     {
    60.         get { return this.interactionFinished; }
    61.     }
    62.  
    63.     public IInteractionRequest ToastRequest
    64.     {
    65.         get { return this.toastRequest; }
    66.     }
    67.  
    68.     public ObservableDictionary<string, string> Errors { get { return this.errors; } }
    69.  
    70.     public string Username
    71.     {
    72.         get { return this.username; }
    73.         set
    74.         {
    75.         if (this.Set<string>(ref this.username, value))
    76.         {
    77.             this.ValidateUsername();
    78.         }
    79.         }
    80.     }
    81.  
    82.     public string Password
    83.     {
    84.         get { return this.password; }
    85.         set
    86.         {
    87.         if (this.Set<string>(ref this.password, value))
    88.         {
    89.             this.ValidatePassword();
    90.         }
    91.         }
    92.     }
    93.  
    94.     private bool ValidateUsername()
    95.     {
    96.         if (string.IsNullOrEmpty(this.username) || !Regex.IsMatch(this.username, "^[a-zA-Z0-9_-]{4,12}$"))
    97.         {
    98.         this.errors["username"] = localization.GetText("login.validation.username.error", "Please enter a valid username.");
    99.         return false;
    100.         }
    101.         else
    102.         {
    103.         this.errors.Remove("username");
    104.         return true;
    105.         }
    106.     }
    107.  
    108.     private bool ValidatePassword()
    109.     {
    110.         if (string.IsNullOrEmpty(this.password) || !Regex.IsMatch(this.password, "^[a-zA-Z0-9_-]{4,12}$"))
    111.         {
    112.         this.errors["password"] = localization.GetText("login.validation.password.error", "Please enter a valid password.");
    113.         return false;
    114.         }
    115.         else
    116.         {
    117.         this.errors.Remove("password");
    118.         return true;
    119.         }
    120.     }
    121.  
    122.     public ICommand LoginCommand
    123.     {
    124.         get { return this.loginCommand; }
    125.     }
    126.  
    127.     public ICommand CancelCommand
    128.     {
    129.         get { return this.cancelCommand; }
    130.     }
    131.  
    132.     public Account Account
    133.     {
    134.         get { return this.account; }
    135.     }
    136.  
    137.     public async void Login()
    138.     {
    139.         try
    140.         {
    141.         if (log.IsDebugEnabled)
    142.             log.DebugFormat("login start. username:{0} password:{1}", this.username, this.password);
    143.  
    144.         this.account = null;
    145.         this.loginCommand.Enabled = false;/*by databinding, auto set button.interactable = false. */
    146.         if (!(this.ValidateUsername() && this.ValidatePassword()))
    147.             return;
    148.  
    149.         IAsyncResult<Account> result = this.accountService.Login(this.username, this.password);
    150.         Account account = await result;
    151.         if (result.Exception != null)
    152.         {
    153.             if (log.IsErrorEnabled)
    154.             log.ErrorFormat("Exception:{0}", result.Exception);
    155.  
    156.             var tipContent = this.localization.GetText("login.exception.tip", "Login exception.");
    157.             this.toastRequest.Raise(new Notification(tipContent));/* show toast */
    158.             return;
    159.         }
    160.  
    161.         if (account != null)
    162.         {
    163.             /* login success */
    164.             globalPreferences.SetString(LAST_USERNAME_KEY, this.username);
    165.             globalPreferences.Save();
    166.             this.account = account;
    167.             this.interactionFinished.Raise();/* Interaction completed, request to close the login window */
    168.         }
    169.         else
    170.         {
    171.             /* Login failure */
    172.             var tipContent = this.localization.GetText("login.failure.tip", "Login failure.");
    173.             this.toastRequest.Raise(new Notification(tipContent));/* show toast */
    174.         }
    175.         }
    176.         finally
    177.         {
    178.         this.loginCommand.Enabled = true;/*by databinding, auto set button.interactable = true. */
    179.         }
    180.     }
    181.  
    182.     public IAsyncResult<Account> GetAccount()
    183.     {
    184.         return this.accountService.GetAccount(this.Username);
    185.     }
    186.     }
    187. }
    Domains:

    Code (CSharp):
    1. using System;
    2.  
    3. using Loxodon.Framework.Observables;
    4.  
    5. namespace Loxodon.Framework.Examples
    6. {
    7.     public class Account : ObservableObject
    8.     {
    9.         private string username;
    10.         private string password;
    11.  
    12.         private DateTime created;
    13.  
    14.         public string Username {
    15.             get{ return this.username; }
    16.             set{ this.Set<string> (ref this.username, value); }
    17.         }
    18.  
    19.         public string Password {
    20.             get{ return this.password; }
    21.             set{ this.Set<string> (ref this.password, value); }
    22.         }
    23.  
    24.         public DateTime Created {
    25.             get{ return this.created; }
    26.             set{ this.Set<DateTime> (ref this.created, value); }
    27.         }
    28.     }
    29. }

     
    TomavX likes this.
  2. TomavX

    TomavX

    Joined:
    Oct 23, 2016
    Posts:
    4
    I'm currently looking for how to solve the UI. I will test your solution, maybe this is it. After initial inspection, it looks appealing. Thanks for sharing this solution. Regards.
     
  3. LoxodonStudio

    LoxodonStudio

    Joined:
    Nov 1, 2016
    Posts:
    40
    I have also provided numerous plugins in my framework, all optimized for performance. Since the English documentation is not very comprehensive, you can take a look at the article below; it might be helpful for you.

    https://www.reddit.com/r/Unity3D/co.../?utm_source=share&utm_medium=web2x&context=3

    https://www.reddit.com/user/clark_y.../?utm_source=share&utm_medium=web2x&context=3
     
  4. TomavX

    TomavX

    Joined:
    Oct 23, 2016
    Posts:
    4
    Thank you. I will take a look at those.