-
Notifications
You must be signed in to change notification settings - Fork 14
MVC
We use a pretty traditional approach for handling views:
All Controllers should inherit ControllerBase<TView, TInputData> (if they accept input parameters) or ControllerBase<TView> (if they don't require them).
Controller is bound to the view instance: it's not shared between multiple views.
public delegate TView ViewFactoryMethod();
protected ControllerBase(ViewFactoryMethod viewFactory)
{
this.viewFactory = viewFactory;
State = ControllerState.ViewHidden;
}
By default, there are two shortcuts to produce TView from the prefab:
- Lazily
public static ViewFactoryMethod CreateLazily<TViewMono>(TViewMono prefab, Transform root) where TViewMono: MonoBehaviour, TView =>
() => Object.Instantiate(prefab, Vector3.zero, Quaternion.identity, root);
- Pre-warmed
public static ViewFactoryMethod Preallocate<TViewMono>(TViewMono prefab, Transform root, out TViewMono instance) where TViewMono: MonoBehaviour, TView
{
TViewMono instance2 = Object.Instantiate(prefab, Vector3.zero, Quaternion.identity, root);
instance = instance2;
instance2.HideAsync(CancellationToken.None, true).Forget();
return () => instance2;
}
Views should implement ViewBase and IView. Views should not contain any logic, views should hold only reference to the various UI elements in a form of serialized properties like:
[field: SerializeField]
internal Button exampleButton { get; private set; }
Views can override the ViewBase ShowAsync and HideAsync methods if there is the need of custom show/hide animations.
Controllers should hold all the logic related to their own view and create any underlying/nested controllers (more in following chapters).
If a controller needs to access properties of its view it should be done with an override of OnViewInstantiated, this ensures that the view is correctly instantiated to avoid null refs.
The main controllers should be created by Plugins in order to ensure a correct flow of data and accessibility to functionalities.
Generally speaking the UI architecture is agnostic from the ECS world, but there exists some scenarios in which we need to access ECS data or even write something in the ECS world.
Accessing ECS data in Main Controllers:
Plugins that create main controllers can also inject Systems to the ECS World, we'll now see a practical example.
One example of ECS data need is the minimap, there we need to move the camera based on the player position in the world.
In order to do this we will need to create a system in the MinimapController, add it to the SystemBinding structure and create the needed query.
System definition:
[UpdateInGroup(typeof(PresentationSystemGroup))]
public partial class TrackPlayerPositionSystem : ControllerECSBridgeSystem
{
internal TrackPlayerPositionSystem(World world) : base(world) { }
}
In the constructor of the controller:
SystemBinding = AddModule(new BridgeSystemBinding<TrackPlayerPositionSystem>(this, QueryPlayerPositionQuery));
And the query:
MVCManager is a storage of controllers and an entry for showing a particular view.
WindowStackManager provides capabilities for maintaining stacks for different view types. It's sub-ordinate to MVCManager and should not be used outside of it.
The sorting layer defines how the corresponding view behaves in the Windows Stack. For each type, there is a different unique logic under the hood:
-
Persistentviews are always rendered behind everything else and once shown can't be hidden. They are rendered in the same order and should not overlap.-
Minimapis a good example of aPersistentView.
-
-
Fullscreenviews are rendered in front of thePersistentlayer.- When a
Fullscreenview is pushed into the stack allPopupsget hidden and allPersistentviews getBlurred. - When a
Fullscreenview closes allPersistentviews receiveFocus
- When a
-
Popup- There could be several
Popupsin the stack. The next popup pushed is drawn above the previous one -
Popupreceives aBlursignal when it gets obscured by a newPopup; andFocuswhen it becomes the highest in the hierarchy - There is a special
IPopupCloserViewthat exists in a single copy and provides the capability of closing the top-most pop-up by clicking on the background
- There could be several
-
Overlayviews are always drawn above every other view.- When the
Overlayis pushed it hidesFullscreenandPopupviews - When the
Overlayis popped no automatic recovery for hidden views is performed
- When the
To show a view a respective ShowCommand<TView, TInputData> should be passed into the UniTask ShowAsync<TView, TInputData>(ShowCommand<TView, TInputData> command) method.
Controller is bound with a view type and input arguments' type one to one: thus, to prevent argument mismatch IssueCommand(TInputData inputData) from the typed Controller should be used, e.g.:
mvcManager.ShowAsync(ExplorePanelController.IssueCommand(new ExplorePanelParameter(ExploreSections.Navmap))).Forget()
The structure previously explains handles the generic UIs, like the minimap, the explore panel, any popup, ... In situations like nested controllers there isn't a strict architecture to follow, but it is recommended to follow some guide lines.
- Views in nested objects of the main view hierarchy should be referenced by the main view
- Controllers related to those sub views should be created by the main controllers
- Views should be logic-less