L’utilisation de la conception MVC dans l’outil Unity3D n’est pas simple. Je ne suis pas sur que cela même pertinent. Mais je pense que cette conception permet d’approcher Unity3D différemment que ne le fait les tutoriels classiques.
Je me suis basé sur principalement 3 sources pour concevoir mon MVC:
– https://www.toptal.com/unity-unity3d/unity-with-mvc-how-to-level-up-your-game-development
– https://www.patrykgalach.com/2019/04/29/simple-mvc-for-unity/
– https://www.gamedeveloper.com/programming/mvc-in-unity/
La conception en sous contrôleur a été retenu dans cet exemple. Ce qu’il faut voir c’est que chaque GameObject de la hiérarchie est associé à un script écris en C#.
La hiérarchie définie avec les GameObject est la suivante :
RootController est associé au script RootController dans le dossier Controllers
LocalController est associé au script LocalController dans le dossier Controllers
UILocalRoot est associé au script UILocalRoot dans le dossier Views
UILocalViewCube est associé au script UILocalViewCube dans le dossier Views
Le seul GameObject réel est un gameobject cube appelé UILocalViewCube . Le UILocalRoot est un groupement de GameObject car à terme il n’y aura pas qu’un seul cube.
On a bien visible la séparation entre la partie Contrôleur et la partie Vue du MVC. La partie Modèle serait lié au niveau du RootControlleur au besoin.
Une instance de LocalController est incluse dans RootController et si d’autres sous-contrôleur était nécessaire, ils seraient aussi ajoutés sous RootController:
// Dans la classe RootController l'inclusion du controlleur local
[Header("Controllers")]
[SerializeField]
private LocalController localController;
Le but du RootControlleur est d’engager ou de désengager des sous-controlleur selon le besoin. Les sous-contrôleurs peuvent se passer « la main » mais un seul n’est actif à un moment donné.
Tous les sous-contrôleurs dérivent d’une classe virtuelle un peu technique appelée SubController. Cette classe virtuelle contient une référence au RootControlleur pour assurer le passage de « la main » au besoin.
public abstract class SubController : MonoBehaviour
{
[HideInInspector]
public static RootController root;
public static void CreateRootControllerReference(RootController rootController)
{
Debug.Log("CreateRootControllerReference");
SubController.root = rootController;
}
La méthode CreateRootControllerReference aurait dû être un constructeur de SubController mais Unity3D ne peut pas avoir une référence sur un RootControlleur tant qu’il n’est pas créé.
Donc dans RootController sur l’évènement Start on a l’appel à cette méthode statique:
private void Start()
{
SubController.CreateRootControllerReference(this);
ChangeController(ControllerTypeEnum.Local);
}
On voit aussi qu’il a un appel à la méthode ChangeConbtroller pour donner la main au premier contrôleur (et unique dans notre exemple Local et qui ne contient qu’un cube)
LocalController recevra donc les évènements parce qu’il s’abonnera aux évènement de sa hierachie de classe:
public class LocalController : SubController<UILocalRoot>
Dans la notation ci-dessus un peu complexe, le LocalController est de type SubController mais de template <UILocalRoot> qui fait la liaison avec le UILocalViewCube
public class UILocalRoot : UIRoot
{
[SerializeField]
private UILocalViewCube uiLocalViewCube;
Donc notre LocalController a la main sur les évènements d’une instance de uiLocalViewCube via cette opération abonnement sur action :
public override void EngageController()
{
ui.UILocalViewCube.actionClick += doActionClick;
}
private void doActionClick()
{
}
Le actionClick de type UnityAction dans la classe UILocalViewCube déclenchera donc l’appel dans le LocalController de la méthode doActionClick()
La liaison est donc réalisée d’un évènement dans la classe UILocalViewCube associé à notre GameObject UILocalViewCube:
public UnityAction actionClick;
public void OnMouseDown()
{
actionClick?.Invoke();
}
La remontée du message depuis sur le clic de la souris sur OnMouseDown vers le LocalController est donc un abonnement sur l’UnityAction actionClick vers une méthode doActionClick dans le LocalController
UILocalViewCube.OnMouseDown -> actionClick -> doActionClick
Maintenant supposons que cette doActrionClick doit permettre de changer de contrôleur (sinon cela resterait des méthodes dans le LocalController) :
private void doActionClick(){
root.ChangeController(RootController.ControllerTypeEnum.GameOver);
}
L’appel ci-dessus demande au RootController de faire un changement de contrôleur.
Il faut ajouter sur la partie vue que tous les différents UILocalRoot qui gèrent l’ensemble des GameObject de cette vue spécifique (appelé Local, mais on aurait put l’appelé Welcome pour l’écran de démarrage) doivent hériter de la classe UIRoot qui contient une méthode pour cacher ou d’écacher leur GameObject associé :
public class UIRoot : MonoBehaviour
{
/// <summary>
/// Method used to show UI.
/// </summary>
public virtual void ShowRoot()
{
gameObject.SetActive(true);
}
/// <summary>
/// Method used to hide UI.
/// </summary>
public virtual void HideRoot()
{
gameObject.SetActive(false);
}
}
Et voici donc notre UILocalRoot qui gère l’ensemble des éléments graphiques:
public class UILocalRoot : UIRoot
{
[SerializeField]
private UILocalViewCube uiLocalViewCube;
public UILocalViewCube UILocalViewCube => uiLocalViewCube;
public override void ShowRoot()
{
base.ShowRoot();
uiLocalViewCube.ShowView();
}
public override void HideRoot()
{
uiLocalViewCube.HideView();
base.HideRoot();
}
}
En remontant voici notre contrôleur local LocalController point central des évènements :
public class LocalController : SubController<UILocalRoot>
{
public override void EngageController()
{
ui.UILocalViewCube.actionClick += doActionClick;
base.EngageController();
}
public override void DisengageController()
{
base.DisengageController();
ui.UILocalViewCube.actionClick -= doActionClick;
}
private void doActionClick()
{
//TODO: code pour la gestion de l'évènement onClick sur le UILocalViewCube
}
}
Le plus haut niveau avec un RootController qui ne contient donc deux sous-contrôleurs LocalControler et GameOverController qui doit être fabriqué par la suite :
public class RootController : MonoBehaviour
{
public enum ControllerTypeEnum
{
Local,
GameOver
}
[Header("Controllers")]
[SerializeField]
private LocalController localController;
[SerializeField]
private GameOverController gameOverController;
private void Start()
{
SubController.CreateRootControllerReference(this);
ChangeController(ControllerTypeEnum.Local);
}
public void ChangeController(ControllerTypeEnum controller)
{
DisengageControllers();
switch (controller)
{
case ControllerTypeEnum.Local:
localController.EngageController();
break;
case ControllerTypeEnum.GameOver:
gameOverController.EngageController();
break;
default:
break;
}
}
public void DisengageControllers()
{
localController.DisengageController();
gameOverController.DisengageController();
}
}
À tester sur cette architecture très complexe si on ne sépare pas trop les éléments, mais j’aime bien avoir des sous-contrôleurs en charge chacun de groupe d’objets graphiques qui sont regroupé dans une classe dérivée de UIRoot (dans notre cas UILocalRoot).
Au final, la liste des scripts et leur utilisation, avec les controlleurs en premier dans le dossier Assets/Scripts/Controllers/:
- RootController.cs -> Contrôleur principal lié au LocalController
- LocalController.cs -> Contrôler spécialisé hérite de SubController une référence vers le RootController
- SubController.cs -> Classe virtuelle dont dépend tous les sous-contrôleurs
Les vues ensuite dans le dossier Assets/Scripts/Views:
- UIView.cs -> classe virtuelle dont tous les GameObject visuel doivent dériver pour avoir les méthodes de base ShowView et HideView.
- UIRoot.cs -> classe virtuelle dont tous les GameObjects gérant les groupes de GameObjects doivent dériver pour avoir les méthodes de base ShowRoot et HideRoot. Ces méthodes appelerons sur chaque GameObject la demande de cacher ou de se rendre visible avec leur ShowView et HideView
- UILocalRoot.cs -> assure la gestion des GameObjects lié à un LocalControler. Sa classe est dérivée de UIRoot
- UILocalViewCube.cs -> Classe liée au GameObject Cube affiché à l’écran avec l’évènement OnMouseDown.
On peut voir UIView et UIRoot comme des interfaces pour que les descendants de ces méthodes (respectivement UILocalViewCube et UILocalRoot) aient les méthodes nécessaires en cas d’engagement ou de désengagement de leur fameux controleur LocalControler .
Il ne manque plus que d’ajouter les codes complets des fichiers manquants
UIView.cs :
using UnityEngine;
/// <summary>
/// Base class for every UI element and view.
/// </summary>
public class UIView : MonoBehaviour
{
/// <summary>
/// Method used to show view or element.
/// </summary>
public virtual void ShowView()
{
gameObject.SetActive(true);
}
/// <summary>
/// Method used to hide view or element.
/// </summary>
public virtual void HideView()
{
gameObject.SetActive(false);
}
}
UIRoot.cs
using UnityEngine;
/// <summary>
/// Base class for UI roots for different controllers.
/// </summary>
public class UIRoot : MonoBehaviour
{
/// <summary>
/// Method used to show UI.
/// </summary>
public virtual void ShowRoot()
{
gameObject.SetActive(true);
}
/// <summary>
/// Method used to hide UI.
/// </summary>
public virtual void HideRoot()
{
gameObject.SetActive(false);
}
}
UILocalViewCube.cs
using UnityEngine;
using UnityEngine.Events;
/// <summary>
/// UI Cube with event OnMouseDown
/// </summary>
public class UILocalViewCube : UIView
{
public UnityAction actionClick;
public UnityAction actionOver;
public void OnMouseDown()
{
actionClick?.Invoke();
}
public void OnMouseOver()
{
actionOver?.Invoke();
}
}
UILocalRoot.cs
using UnityEngine;
/// <summary>
/// UI root class for Menu controller.
/// </summary>
public class UILocalRoot : UIRoot
{
[SerializeField]
private UILocalViewCube uiLocalViewCube;
public UILocalViewCube UILocalViewCube => uiLocalViewCube;
public override void ShowRoot()
{
base.ShowRoot();
uiLocalViewCube.ShowView();
}
public override void HideRoot()
{
uiLocalViewCube.HideView();
base.HideRoot();
}
}
LocalController.cs
using UnityEngine;using System.Text;using System.Net.Sockets;using System.Threading;using System;
/// <summary>
/// Controller responsible for menu phase.
/// </summary>
public class LocalController : SubController<UILocalRoot>
{
public override void EngageController()
{
ui.UILocalViewCube.actionClick += doActionClick;
ui.UILocalViewCube.actionOver += doActionOver;
base.EngageController();
}
public override void DisengageController()
{
base.DisengageController();
ui.UILocalViewCube.actionClick -= doActionClick;
ui.UILocalViewCube.actionOver -= doActionOver;
}
private void doActionClick()
{
Debug.Log("doActionClick");
}
private void doActionOver()
{
Debug.Log("doActionOver");
root.ChangeController(RootController.ControllerTypeEnum.GameOver);
}
}
RootController.cs
using UnityEngine;
/// <summary>
/// Root controller responsible for changing game phases with SubControllers.
/// </summary>
public class RootController : MonoBehaviour
{
// SubControllers types.
public enum ControllerTypeEnum
{
Local,
GameOver
}
// References to the subcontrollers.
[Header("Controllers")]
[SerializeField]
private LocalController localController;
/// <summary>
/// Unity method called on first frame to act as initialisation
/// </summary>
private void Start()
{
SubController.CreateRootControllerReference(this);
ChangeController(ControllerTypeEnum.Local);
}
/// <summary>
/// Method used by subcontrollers to change game phase.
/// </summary>
/// <param name="controller">Controller type.</param>
public void ChangeController(ControllerTypeEnum controller)
{
// Reseting subcontrollers.
DisengageControllers();
// Enabling subcontroller based on type.
switch (controller)
{
case ControllerTypeEnum.Local:
Debug.Log("localController.EngageController()");
localController.EngageController();
break;
case ControllerTypeEnum.GameOver:
// gameOverController.EngageController();
break;
default:
break;
}
}
/// <summary>
/// Method used to disable all attached subcontrollers.
/// </summary>
public void DisengageControllers()
{
localController.DisengageController();
gameOverController.DisengageController();
}
}
SubController.cs
using UnityEngine;
/// <summary>
/// Base class for SubControllers with reference to Root Controller.
/// </summary>
public abstract class SubController : MonoBehaviour
{
[HideInInspector]
public static RootController root;
public static void CreateRootControllerReference(RootController rootController)
{
SubController.root = rootController;
}
public virtual void EngageController()
{
gameObject.SetActive(true);
}
public virtual void DisengageController()
{
gameObject.SetActive(false);
}
}
/// <summary>
/// Extending SubController class with generic reference UI Root.
/// </summary>
public abstract class SubController<T> : SubController where T : UIRoot
{
[SerializeField]
protected T ui;
public T UI => ui;
public override void EngageController()
{
base.EngageController();
ui.ShowRoot();
}
public override void DisengageController()
{
base.DisengageController();
ui.HideRoot();
}
}