I have been struggling with this dilemma for quite a while already.
Our game is complex enough that we have tons of logic in normal C# scripts that function very independently of any Unity specific component.
This logic is eventually modifying the Data Layer which is basically a batch of simple Classes with state that we call Models.
Everything works fine until we have to connect this Layer with the visualization layer (aka Unity MonoBehaviours)
These are the approaches I have tried (they all work but I am not totally satisfied with any)
Using a MonoBehaviourWrapper
I have a MonoBehaviour class with only one property:
[SerializeField] DataModel _dataModel;
I call it ModelMonoBehaviourWrapper
I have a GameObject in the Scene to visualize the state of the Model I call it ModelView
I have a script that listens to changes in the Model and modifies the ModelView
. I call it ModelViewController
. This requires access to the Model to listen to changes so it has this property:
[SerializeField] ModelMonoBehaviourWrapper _modelMonoBehaviourWrapper;
I attach ModelViewController
to the ModelView
GameObject as component (The property _modelMonoBehaviourWrapper remains unlinked at this moment)
I have a GameObject in the Scene as a holder of all the internal logic. I call it SceneManager
. This GameObject has an script attached called SceneManagerController
who is in charge of running the Game. This script has, therefore, access to the Data Models. This script has also this property:
[SerializeField] ModelMonoBehaviourWrapper _modelMonoBehaviourWrapper;
I attach a ModelMonoBehaviourWrapper
Script as a component to SceneManager
, then I link this Component to the property _modelMonoBehaviourWrapper
in both: ModelViewController
on ModelView
and SceneManagerController
in SceneManager
This way I manage to have the same Model instance connected in ModelViewController
and SceneManagerController
.
I initialize ModelMonoBehaviourWrapper._dataModel through SceneManagerController
and I can read it from ModelViewController
.
If you think this is over-messy I also think the same. There are also some initialization race condition problems, for example if you are trying to access to the Model (through ModelMonoBehaviourWrapper
) in ModelViewController
before SceneManagerController
has initialized it. If you are careful it works though.
Here you have a diagram trying to represent this structure:
Using a public static pointer to SceneManagerController
In a similar structure as above but instead of using the ModelMonoBehaviourWrapper
to make the Model accessible in both Controllers I make the SceneManagerController
public and static like this:
public static SceneManagerController Instance;
This way I can access to the Model
from the ModelViewController
like this:
SceneManagerController.Instance.GetModel();
This works, but I am concerned about the strong counter-intuitive dependency generated here. I would like ModelViewController
to be depended only in the Model
but now it is depending on this intermediate class SceneManagerController
just to have access to the model
Diagram:
Using the SceneManagerController as a Initializer Orchestrator
The SceneManagerController
can contain references to all elements, not only the Data Models (that it already has), but also to all Visual GameObjects like:
[SerializeField] GameObject ModelView;
This way SceneManagerController can instantiate the ModelViewController
and give it access to the Model and to the View element.
This way I keep ModelViewController
only dependent to the Model. I am adding all references to the SceneManagerController what is even helpful because this way I have to only wire Component Properties in one GameObject.
I am playing with this structure right now. The problem I see is that now the SceneManagerController
becomes too much dependent in the set up of the Scene and I can not Add/Remove Scene elements easily because they are not self-managed as it is the case in the previous above architectures.
Diagram:
I would like to know, from people with experience in similar situations, how you have solved it and what solutions you have found more solid and maintainable.
Update 1
Answering to some questions in the comments:
Are you using or aware of RX?
No, I am reading now:
- http://introtorx.com/Content/v1.0.10621.0/00_Foreword.html
- https://github.com/neuecc/UniRx
- https://assetstore.unity.com/packages/tools/integration/unirx-reactive-extensions-for-unity-17276
It looks like nice solutions to have code reacting to changes in a Model. We are solving this problem with basic EventHandlers..
But even if I integrate RX, if I understood it properly, I will have the same problem: how can I share a Model instance between my DataLayer/GameLogicLayer (where the instance has been created) and the VisualLayer (MonoBehviour scripts in the Scene GameObjects, where the the scripts are listening to changes in the instance?
Is there specific reason for data layer being owned by a game object, as opposed to a program itself?
Not necessarily, a part of having things separate. I am ok of having a DataManager storing all the Models.
Is DI/IoC used for data layer?
Yes we have different level of DI/IoC implementations
What sort of component is responsible for creating data-layer-game-object?
The Models instances are created in the DataManager.cs which acts as a factory. But they are owned by the GameScripts that needed to create them.
Is there a controller on a data layer to communicate with a scene/engine?
DataManager.cs has static public methods to instantiate Models. These methods can be accessed by the Scene/Engine scripts