1
\$\begingroup\$

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:

Eternal Quest - Connecting Data Layer and Display Layer (Through ModelMonoBehaviourWrapper)

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:

Eternal Quest - Connecting Data Layer and Display Layer (The static SceneManagerController)

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:

Eternal Quest - Connecting Data Layer and Display Layer (SceneManagerController as an Orchestrator)

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:

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

\$\endgroup\$
2
  • \$\begingroup\$ Are you using or aware of RX? Is there specific reason for data layer being owned by a game object, as opposed to a program itself? Is DI/IoC used for data layer? What sort of component is responsible for creating data-layer-game-object? Is there a controller on a data layer to communicate with a scene/engine? \$\endgroup\$ Mar 30, 2021 at 0:14
  • \$\begingroup\$ @ShadowsInRain thanks for the questions, I answered them in the Update 1 of the question \$\endgroup\$
    – fguillen
    Mar 30, 2021 at 9:11

1 Answer 1

1
\$\begingroup\$

The idiomatic way of using Unity is that your should not separate your game architecture into models, views and controllers. A gameObject should contain its own data, logic and presentation, distributed among multiple MonoBehaviours, where each behaviour represents a single functionality of that gameObject. And when you have data which is shared between multiple gameObjects, that data should be in form of assets implemented as ScriptableObject's.

Nevertheless, creating an MVC architecture on top of Unity can in some cases have its advantages. Those often outweigh the drawbacks for games with mechanics which are mostly abstract and not driven by the Unity physics engine. The greatest advantage in my opinion is that having the whole state of the game in a well-encapsulated data model makes it a lot easier to implement savegames, which otherwise can get pretty ugly in Unity.

However, your particular implementation appears quite over-engineered to me. My usual approach to this pattern (when I choose to implement it) is a far simpler one:

  1. Create a plain old C# class representing your whole model. I usually call this class GameState.
  2. Give that class a public static GameState current property, which retrieves the current game state.The initialization problem can be solved by making this a property which initializes a default GameState when accessed for the first time ( if (current == null) current = new GameState(); return current;). You should also have a static method Load(string filename) to replace current with a GameState deserialized from a savegame file.
  3. Give each instance of each class in your model an ID.
  4. Give the GameState class public accessor methods to retrieve model entities by ID. Like GetCharacter(int id), GetCity(int id) or GetFaction(int id). Those methods can return the actual entity, or just as well return an interface or facade instead through which the current state of that entity can be accessed.
  5. When a MonoBehavior needs to reference some class from the model, then implement it like that:

class CharacterView : MonoBehaviour {
       public int characterId = 0;
       private model.Character data;

       void Start() {
           data = GameState.current.GetCharacter(characterId);
       }

       void FixedUpdate() {   
            transform.SetPositionAndRotation(             
                data.currentLocation,
                Quaternion.Euler(0f, data.direction, 0f)
            );
       }
}

When creating an instance of this MonoBehaviour (in the editor or via the Instantiate method from a script), you then have to assign the characterId. This example script does not support changing the characterId after the object was created. Usually I don't see a need for that, because there are few situations where I would ever want to reuse one of these gameObjects for a different entity instead of just creating a new one. But if you need that (when you want to do object pooling, for example), then hide characterId behind a property which again calls data = GameState.current.GetCharacter(characterId); in its setter.


Which gets us to the next problem: Instantiating and destroying a gameObject whenever an entity is created or removed in the model.

When a controller decides to add a new entity to the model, then that usually means that you need to Instantiate a new gameObject and set the corresponding ID on each of its behaviours. But you likely don't want any using UnityEngine in your model or your controllers. You certainly want them to be as independent from the engine as possible. The best solution in my opinion is using C# events for that.

Give the GameState a bunch of public events like onCreateCharacter, onRemoveCharacter etc.. which it invokes whenever a controller does one of these things. Then have single MonoBehaviours in your scene like this one, which instantiates objects for all entities already in the model and then subscribes to those events to instantiate more or remove those it spawned before:

public CharacterSpawner : MonoBehaviour {

     public GameObject characterPrefab;   

     private Dictionary<int, GameObject> managedCharacters = new Dictionary<int, GameObject>();      

     void Start() {
          for(int id in GameState.current.allCharacterIds) {
               CreateCharacter(id);
          }
          GameState.current.onCreateCharacter += CreateCharacter;
          GameState.current.onRemoveCharacter += (id) => {
                Destroy(managedCharacters[id]);
                managedCharacters.Remove(id);
          };

     }

     private void CreateCharacter(int id) {              
          GameObject newCharacter = Instantiate(characterPrefab);
          newCharacter.GetComponent<CharacterView>().characterId = id;
          managedCharacters[id] = newCharacter;
     }

}
\$\endgroup\$
2
  • \$\begingroup\$ Yes, this makes sense. If I understood correctly, It is essentially what I called Using a public static pointer to SceneManagerController in my Question, but a bit more specific. \$\endgroup\$
    – fguillen
    Mar 30, 2021 at 10:55
  • 1
    \$\begingroup\$ @fguillen I expanded the answer to explain how creating and destroying entities can be handled in this architecture. \$\endgroup\$
    – Philipp
    Mar 30, 2021 at 11:08

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .