Channel | UnityFx.Mvc |
---|---|
Github | |
Unity Asset Store | |
Npm |
UnityFx.Mvc is an MVC(S) framework for Unity.
Please see CHANGELOG for information on recent changes.
You may need the following software installed in order to build/use the library:
You can get the code by cloning the github repository using your preffered git client UI or you can do it from command line as follows:
git clone https://github.com/Arvtesh/UnityFx.Mvc.git
Npm package is available at npmjs.com. To use it, add the following line to dependencies section of your manifest.json
. Unity should download and link the package automatically:
{
"scopedRegistries": [
{
"name": "Arvtesh",
"url": "https://registry.npmjs.org/",
"scopes": [
"com.unityfx"
]
}
],
"dependencies": {
"com.unityfx.mvc": "0.3.1"
}
}
As outlined in ASP.NET Core documentation, the Model-View-Controller (MVC) architectural pattern separates an application into three main groups of components: Models, Views, and Controllers. This pattern helps to achieve separation of concerns. Using this pattern, user requests are routed to a Controller which is responsible for working with the Model to perform user actions and/or retrieve results of queries. The Controller chooses the View to display to the user, and provides it with any Model data it requires.
This delineation of responsibilities helps you scale the application in terms of complexity because it's easier to code, debug, and test something (model, view, or controller) that has a single job. It's more difficult to update, test, and debug code that has dependencies spread across two or more of these three areas. For example, user interface logic tends to change more frequently than business logic. If presentation code and business logic are combined in a single object, an object containing business logic must be modified every time the user interface is changed. This often introduces errors and requires the retesting of business logic after every minimal user interface change.
Both the view and the controller depend on the model. However, the model depends on neither the view nor the controller. This is one of the key benefits of the separation. This separation allows the model to be built and tested independent of the visual presentation.
In the same way that MVC takes the position that you should separate model logic from view and controller logic, MVCS takes this notion a step further by advocating for application logic to live in the services. This is the recommended way of sharing pieces of logic between controllers.
The Model in an MVC application represents the state of the application and any business logic or operations that should be performed by it. Business logic should be encapsulated in the model, along with any implementation logic for persisting the state of the application.
Views are responsible for presenting content through the user interface. There should be minimal logic within views, and any logic in them should relate to presenting content.
Controllers are the components that handle user interaction, work with the model. In an MVC application, the view only displays information; the controller handles and responds to user input and interaction. Generally controllers act like data bridges between services, model and the attached view.
Services usually contian very specialized logic, that is shared between several controllers. Services may on may not depend on Model, they should never depend on controllers and views. Examples of services: IFileSystem
, IAssetFactory
, IAudioService
.
Install the package and import the namespace:
using UnityFx.Mvc;
using UnityFx.Mvc.Extensions;
A presenter is an object capable of presenting view controllers. It should implement IPresenter
interface. There is a default presenter implementation, that is constructed via PresenterBuilder
class:
var presenter = new PresenterBuilder(myServiceProvider, gameObject)
.UseViewFactory(myViewFactory)
.Build();
The builder constructor takes 2 arguments:
- a service provider instance, that is used by both presenter and its builder to resolve dependencies; this is a required argument;
- a game object that acts as a presenter owner (it disposed the presenter when destroyed) and event source (forwards
Update
notifications to the presenter); this can be set tonull
, in this case presenter has no owner and requires an event source to be set explicitly.
Please note, there are many UseXXX
methods available in the builder for constructing a presenter instance. After everything is set, calling Build()
creates and returns the presenter itself.
Presenter uses IServiceProvider
instance to resolve controller dependencies. It also requires IViewFactory
to create views for the controllers presented.
A typical scenario of presenter usage is:
var presentResult = presenter.Present<SplashController>();
This code does the following:
- A present result proxy is created, that can be used to track the present operation state;
- A view for the
SplashController
is created and loaded (a view factory is used for that, it should be set viaPresenterBuilder
); - A
SplashController
instance is constructed, all dependencies required by its constructor is resolved via service provider passed to thePresenterBuilder
. Construction of the controller can be overriden withIViewControllerFactory
implementation.
The following code presents a message box and awaits its result:
var result = await presenter.PresentAsync<MyMessageBoxController>();
if (result == MessageBoxResult.Ok)
{
// Handle OK
}
else
{
// Handle CANCEL
}
Result of a present operation can also be used in a coroutine:
var presentResult = presenter.Present<MyMessageBoxController>();
yield return presentResult;
if (presentResult.Result == MessageBoxResult.Ok)
{
// Handle OK
}
else
{
// Handle CANCEL
}
There are a lot of overloads of the Present
method accepting additional arguments. In any case it needs a controller type to do the work.
Controller is any class that implements IViewController
interface. There are several default controller implementations, like ViewController
and ViewController<TView>
. In most cases users should inherit new controllers from one of these. A controller constructor usually accepts at least an argument of type IPresentContext
(or IPresentContext<TResult>
for controllers, that return a result value), which provides access to its context (including the view).
public class MinimalController : IViewController
{
private readonly IPresentContext _context;
public IView View => _context.View;
public MinimalController(IPresentContext context)
{
_context = context;
}
}
Main controller responsibility is managins its view. As noted above, a view is created before controller constructor is called. A controller can access its view at any time via View
property (for controllers inherited from ViewController
) or via IPresentContext.View
.
public class MyPrettyController : ViewController<MyPrettyView>
{
public MyPrettyController(IPresentContext context)
: base(context)
{
// View has type of `MyPrettyView`, so no cast is needed.
View.MyPrettyFunction(20);
}
// ...
}
ViewControllerAttribute
allows setting default controller attributes (tag, prefab path, present options etc.). The attribute is not required, but it is often a convenient way to set default present options.
[ViewController(PresentOptions = PresentOptions.Modal)]
public class MyModalController : ViewController
{
public MyModalController(IPresentContext context)
: base(context)
{
}
}
Linking a controler to its view is done via a string path. By default, for a controller named XxxController
presenter attempts to load view this path Xxx
. If that does not work for you, ViewControllerAttribute
can be used to set the path for controller:
// For MyController a view with name 'MySpecialViewPath' will be loaded.
// If PrefabPath is not set, a view with name 'My' will be loaded.
[ViewController(PrefabPath = "MySpecialViewPath")]
public class MyController : ViewController
{
public MyPrettyController(IPresentContext context)
: base(context)
{
}
}
UnityFx.Mvc controllers request dependencies explicitly via constructors. The framework has built-in support for dependency injection (DI). Services are added as a constructor parameters, and the runtime resolves specific service from the service container (via IServiceProvider
). Services are typically defined using interfaces.
public class MyPrettyController : ViewController
{
public MyPrettyController(IPresentContext context, MyDependency1 d1, MyDependency1 d2)
: base(context)
{
// MyDependency1 and MyDependency2 should be registered in IServiceProvider
// implementation, otherwise the present call will fail to resolve them.
}
// ...
}
A controller can implement IViewControllerEvents
interface to get lifetime notifications, implementing IUpdateTarget
allows getting frame updates. If you inherit ViewController
class, you get these notifications by overriding corresponding methods.
public class MyController : ViewController
{
public MyController(IPresentContext context)
: base(context)
{
}
protected override void OnActivate()
{
// Called when the controller becomes active.
}
protected override void OnDeactivate()
{
// Called when the controller becomes inactive.
}
protected override void OnPresent()
{
// Called after the controller has been initialized.
}
protected override void OnDismiss()
{
// Called when the controller is going to be dismissed.
}
protected override void OnUpdate(float frameTime)
{
// Called on each frame.
}
}
A controller is expected to receive input via implementing ICommandTarget
interface. ViewController
-based controllers just override OnCommand
method.
A generic command is any data, that is passed to a command target. There is CommandUtilities
statis class that contain command-related helpers.
public class MyController : ViewController
{
public enum Commands
{
MyCommand1,
MyCommand2
}
public MyController(IPresentContext context)
: base(context)
{
}
protected override bool OnCommand<TCommand>(TCommand command)
{
// A command can be anything. It is the controller responsibility
// to filter only commands it can process.
if (command != null && !IsDismissed)
{
// In this case we use enumeration as a list of possible commands.
if (CommandUtilities.TryUnpack(command, out Commands cmd))
{
if (cmd == Commands.MyCommand1)
{
Debug.Log("MyCommand1 received.");
}
else
{
Debug.Log("MyCommand2 received.");
}
// The command is recognized, return true to mark it as processed.
return true;
}
}
// The command is not processed. Leave it for someone else.
return false;
}
}
A controller can provide a result value. For instance, result value of a message box can be an identifier of the button pressed, a file dialog might return a path to the file selected by user. To mark a controller as having result value, it should inherit either ViewController<TView, TResult>
or IViewControllerRsult<TResult>
. After the controller has been dismissed, its result vaule can be retrieved via present result.
public class MyMessageBoxController : ViewController<MyMessageBoxView, int>
{
public MyMessageBoxController(IPresentContext<int> context)
: base(context)
{
}
private void OnOkPressed()
{
// Dismisses the controller with 1 result value.
Dismiss(1);
}
private void OnCancelPressed()
{
// Dismisses the controller with 0 result value.
Dismiss(0);
}
}
// ...
var result = await presenter.PresentAsync<MyMessageBoxController>();
View is a class that implements IView
interface. Views are created via IViewFactory
implementation that should be passed to PresentBuilder
before a presenter can be created. There is a UGUIViewFactoryBuilder
class for constructing UGUI-based view factories.
var viewFactory = new UGUIViewFactoryBuilder(gameObject)
.AddViewPrefab("view1", viewGo1)
.AddViewPrefab("view2", viewGo2)
.Build();
There is a default MonoBehaviour
-based view implementation (View
). It is recommended to inherit user views from this class. View is supposed to manage presentation-related logic and send user input to its controller. Please note, that there is no explicit reference to the controller in view. The preffered way of sending controller notifications is calling one of NotifyCommand
overloads (which in turn raises INotifyCommand.Command
event).
public class MinimalView : View
{
[SerializeField]
private Button _closeButton;
public void Configure(MinimalViewArgs args)
{
_closeButton.onClick.AddListener(OnCLose);
}
private OnClose()
{
NotifyCommand("close");
}
}
TODO
The project was initially created to help author with his Unity3d projects. Client .NET applications in general (and Unity applications specifically) do not have a standard structure or any kind of architecturing guidelines (like ASP.NET). This is an attempt to create a small yet effective and usable application framework suitable for Unity projects.
Please see the links below for extended information on the product:
- MVC in Wikipedia.
- MVC in ASP.NET Core.
- Dependency injection in ASP.NET Core.
- Routing in ASP.NET Core.
- Architectural pronciples in ASP.NET Core.
- Deeplinking in Wikipedia.
- Deeplinking basics.
Please see contributing guide for details.
The project uses SemVer versioning pattern. For the versions available, see tags in this repository.
Working on this project is a great experience. Please see below list of sources of my inspiration (in no particular order):
- ASP.NET. A great and well-designed framework.
- Everyone who ever commented or left any feedback on the project. It's always very helpful.