Separation of concerns with Unity UI

I have worked with various interpretations of MVC-like patterns over the years and have experimented a lot with applying them to UIs in Unity. The patterns that I found to work best with nGUI and uGUI are MVP (Model-View-Presenter) (passive view) and MVVM (Model-View-ViewModel).


Contents

Some notes

There are some pretty significant tradeoffs between the MVP and MVVM patterns:

  • MVP - Can be implemented without using a special framework; but you will likely end up having to implement many more interfaces.

  • MVP - Values can be passed around efficiently using properties.

  • MVP - Properties can be renamed easily using refactoring tools.

  • MVVM - Exceptionally nice to work with once you have a good framework in place

  • MVVM - Data binding tends to be implemented using reflection, property wrappers and expression trees. Reflection isn’t ideal for real-time since it can lead to GC spikes. Normally expression trees could be used to optimize this except many of Unity’s target platforms do not allow JIT. This leaves property wrappers but these are far from ideal since suddenly view models are nolonger POCOs.

  • MVVM - Works very well with the INotifyPropertyChanged interface. Modern C# language features can automatically capture property names making them refactor friendly; for instance with the nameof syntax… unfortunately at the moment Unity has no support for this syntax so refactoring is a bit trickier.

Wiring up the view using the inspector

In both cases the view component is added to the root-most game object of the view. Controls are wired up tot he view component using the inspector:

Screenshot of view panel in editor

MVP (Passive View)

MVP overview diagram

I find that it helps to have a general interface to activate and deactivate presenters so that interfaces can be entirely disabled whilst they are not the center of attention. In Unity it is a common workflow for smaller games to have a single “MenuSystem” prefab that holds all of the game’s UI panels so that they don’t have to be continuously instantiated and destroyed.

public interface IPresenter
{
    void Activate();
    void Deactivate();
}

The view is injected into the presenter upon construction and in this situation I am using an event aggregator to communicate navigation events to the navigation controller.

Upon being activated the associated view is shown using the view-specific Show method which could potentially accept additional arguments. The view’s properties are also initialized from the applications current state.

The presenter updates the enabled state of the “Next” button whenever the view reports changes to the “Nickname” input. For this example I chose to simply expose a dedicated event although another option might be to have the view implement INotifyPropertyChanged which would allow the presenter to update the enabled state of the “Next” button based upon any input being changed.

public sealed class InputPlayerDetailsPresenter : IPresenter
{
    private readonly IInputPlayerDetailsView view;
    private readonly IEventAggregator events;


    public InputPlayerDetailsPresenter(
        IInputPlayerDetailsView view,
        IEventAggregator events
    ) {
        this.view = view;
        this.events = events;
    }


    private bool CanProceed {
        get { return !string.IsNullOrEmpty(this.view.Nickname); }
    }


    public void Activate()
    {
        this.view.Show();
        this.view.PlayerNumber = 1;
        this.view.NicknameChanged += this.View_NicknameChanged;
        this.view.BackAction += this.View_BackAction;
        this.view.NextAction += this.View_NextAction;
    }

    public void Deactivate()
    {
        this.view.Hide();
        this.view.NicknameChanged -= this.View_NicknameChanged;
        this.view.BackAction -= this.View_BackAction;
        this.view.NextAction -= this.View_NextAction;
    }


    private void View_NicknameChanged()
    {
        this.view.EnableNextAction = this.CanProceed;
    }

    private void View_BackAction()
    {
        this.events.Publish(new ReturnToPreviousStageMessage());
    }

    private void View_NextAction()
    {
        if (!this.view.EnableNextAction) {
            return;
        }
        this.events.Publish(new AdvanceToNextStageMessage(this.view.Nickname));
    }
}

Each view has an interface and a component that implements that interface. The presenter can only communicate with the view component via that interface.

The “Back” and “Next” interactions were given the “Action” suffix rather than “Clicked” since these could theoretically be actuated by some other input such as tapping the escape key on the keyboard.

Non-editable properties only need to have a setter since they will only ever be set by the presenter; the presenter has no interest in reading any modifications that the view may have made.

Editable properties require a setter and a getter since the presenter will want to capture user inputs from the view. A setter is of course not necessary if the presenter should never initialize the value of that input.

public interface IInputPlayerDetailsView
{
    event Action NicknameChanged;
    event Action BackAction;
    event Action NextAction;


    // A non-editable properties.
    int PlayerNumber { set; }
    bool EnableNextAction { set; }

    // An editable property.
    string Nickname { get; set; }


    void Show();
    void Hide();
}

The view component should be attached to the root-most game object that represents the entire “Input Player Details” panel. Each control of interest can be associated with the view component using the inspector.

In this case the view has a hard-coded string to format the player number label; this can easily be localized by injecting a localization service into the view.

The view subscribes to various Unity UI control events and relays them to the presenter via the events that were declared in the IInputPlayerDetailsView interface. The presenter can then decide how to handle the user interaction. For instance, the presenter can update the enabled state of the “Next” button each time changes are made to the nickname input.

public sealed class InputPlayerDetailsView : MonoBehaviour, IInputPlayerDetailsView
{
    [SerializeField]
    private Text playerNumberText = null;
    [SerializeField]
    private InputField nicknameInputField = null;
    [SerializeField]
    private Button backButton = null;
    [SerializeField]
    private Button nextButton = null;


    public event Action NicknameChanged;
    public event Action BackAction;
    public event Action NextAction;


    public int PlayerNumber {
        set {
            this.playerNumberText.text = string.Format("Player {0}", value);
        }
    }

    public string Nickname {
        get { return this.nicknameInputField.text; }
        set { this.nicknameInputField.text = value; }
    }

    public bool EnableNextAction {
        set { this.nextButton.IsInteractable = value; }
    }


    private void OnEnable()
    {
        this.nicknameInputField.onValueChange.AddListener(this.OnNicknameChanged);
        this.backButton.onClick.AddListener(this.OnBackAction);
        this.nextButton.onClick.AddListener(this.OnNextAction);
    }

    private void OnDisable()
    {
        this.nicknameInputField.onValueChange.RemoveListener(this.OnNicknameChanged);
        this.backButton.onClick.RemoveListener(this.OnBackAction);
        this.nextButton.onClick.RemoveListener(this.OnNextAction);
    }


    private void OnNicknameChanged()
    {
        var handler = this.NicknameChanged;
        if (handler != null) {
            handler.Invoke();
        }
    }

    private void OnBackAction()
    {
        var handler = this.BackAction;
        if (handler != null) {
            handler.Invoke();
        }
    }

    private void OnNextAction()
    {
        var handler = this.NextAction;
        if (handler != null) {
            handler.Invoke();
        }
    }


    public void Show()
    {
        this.gameObject.SetActive(true);
    }

    public void Hide()
    {
        this.gameObject.SetActive(false);
    }
}

MVVM

MVVM overview diagram

I created a proof-of-concept implementation of a general purpose data binding solution since .NET / Mono does not include a general purpose solution. My implementation supports the standard binding modes (UpdateOnce, OneWay and TwoWay).

I also opened a feature request on the .NET Core project’s git repository since I believe that this would be a fantastic addition to the framework.

Using my proof-of-concept framework a view model can be implemented like follows:

public class InputPlayerDetailsViewModel : ViewModelBase
{
    private readonly IEventAggregator events;

    private string playerNumber = "Player 1";
    private string nickname = "";

    private readonly DelegateCommand backCommand;
    private readonly DelegateCommand nextCommand;


    public GameRulesScreenViewModel(IEventAggregator events)
    {
        this.events = events;

        this.backCommand = new DelegateCommand(this.OnBackCommand);
        this.nextCommand = new DelegateCommand(this.OnNextCommand, () => this.CanProceed);
    }


    // Input fields can be bound to this property.
    public string PlayerNumber {
        get { return this.playerNumber; }
        set { this.SetPropertyField(ref this.playerNumber, value, "PlayerNumber"); }
    }

    public string Nickname {
        get { return this.nickname; }
        set { this.SetPropertyField(ref this.nickname, value, "Nickname"); }
    }


    // Button controls can be bound to these properties.
    public ICommand BackCommand {
      get { return this.backCommand; }
    }

    public ICommand NextCommand {
      get { return this.nextCommand; }
    }


    private bool CanProceed {
        get { return !string.IsNullOrEmpty(this.Nickname); }
    }


    private void OnBackCommand()
    {
        this.events.Publish(new ReturnToPreviousStageMessage());
    }

    private void OnNextCommand()
    {
        this.events.Publish(new AdvanceToNextStageMessage(this.Nickname));
    }
}

Each view is represented by a view component on the root game object of a uGUI or nGUI panel. All bindable controls are associated with the view component using the inspector window. As you can see in the following example, I created adapters for some of Unity’s UI controls to make some of their important properties bindable by implementing INotifyPropertyChanged.

public class InputPlayerDetailsView : MonoView<InputPlayerDetailsViewModel>
{
    [SerializeField]
    private BindableText playerNumberText = null;
    [SerializeField]
    private BindableInputField nicknameInputField = null;

    [SerializeField]
    private BindableButton backButton = null;
    [SerializeField]
    private BindableButton nextButton = null;


    protected override void OnBind()
    {
        this.Bindings.AddProperty(this.ViewModel, "PlayerNumber", this.playerNumberText, "Text");
        this.Bindings.AddProperty(this.ViewModel, "Nickname", this.nicknameInputField, "Text");

        this.Bindings.AddCommand(this.backButton, this.ViewModel.BackCommand);
        this.Bindings.AddCommand(this.nextButton, this.ViewModel.NextCommand);
    }
}

Note - The InputPlayerDetailsViewModel instance is injected into the above view. In this particular experiment my navigation controller has ownership of the view model.

My proof-of-concept binding mechanism doesn’t provide a way to format or convert the values of bound properties; hence why the player number is already encoded as a string. This is a feature that could be added if I decide to use the MVVM design pattern in a project.

Conclusion

Having spent a couple of days on each of these approaches using uGUI and nGUI I found that the MVP pattern provided the least amount of friction. This was primarily due to the fact that MVP is barebones in that it doesn’t require a special framework.

With MVP (passive view) the binding logic is implemented manually which means that it is fairly straightforward to support the binding of all sorts of exotic properties with UI controls. Also there aren’t too many moving parts; it is quite easy to follow the flow of execution throughout the binding layer.

With MVVM I found myself having to write new adapters and utilities frequently to assist with data binding and I realize that this would likely be an ongoing process since projects are always going to require specialized binding logic. The framework would grow and become more capable as more projects are developed.