Model View Controller with Unity
Feature

Model View Controller with Unity

Source code, also known as spaghetti code, is almost impossible to decode and needs to be rewritten when in doubt. Nurdogan Erdem explains what kind of benefits an architectural pattern such as MVC has to offer.

Nurdogan Erdem

Senior Client Developer at Goodgame Studios

Nurdogan lives in Hamburg and is a Senior Client Developer at Goodgame Studios. Previously, he was a Senior Software Developer at InnoGames and a Tech Lead at Bigpoint. In his spare time he is busy working on his own retro-style RPG.

Making Games

@ Facebook

Making Games

@ Twitter
comment

Everybody has done it before and some are still doing it: spaghetti code. Source code that deserves this nickname is confusing, full of dependencies and hard to expand. In most cases it is impossible to decode unless you rewrite it from scratch. Once this decision has been made, one should consider using an architectural pattern such as MVC. MVC is short for Model View Controller and is basically aimed at reducing dependencies in the code. There are various benefits MVC provides. When used correctly, it may increase the reusability, expandability and testability of the code. StrangeIoC (Strange) provides an MVC framework which is available for Unity and C#. Strange is strongly inspired by Robotlegs, an MVC framework for ActionScript. Due to the code differences between C# and ActionScript it is no direct port though. However, developers with experience in Robotlegs should have no problem getting familiar with Strange.

Dependency Injection

One of Strange's main characteristics is Dependency Injection (DI). Basically DI is something we use on a daily basis. Every time we use objects as a parameter in the constructor of a class, we create a DI. In Strange, this only has to be done on a limited basis since the idea behind DI is for an external instance – the Injector – to take care of this task. This means every time a class wants something it's the Injector's task to give it to it. However, for the Injector to know what it should provide, what the class needs has to be clearly defined. This is done with the help of the »injectionBinders« and is therefore called binding. There are further binders which I'm going to point out later. What they all have in common is that they are usually defined within one class which derives from the »MVCSContext« class of the Strange framework. The dependency that we're binding should ideally implement an interface. This makes it easier later on to swap the dependency if needed.

injectionBinder.Bind<IMyClass >().To<MyClass>();

Now we can »inject« the dependency wherever we have access to the Injector or rather the Strange framework by using the [Inject] tag.

[Inject]
public IMyClass {get; set;}

If the interface is implemented by several classes, one may also use the ToName() method. This is called »named injection«.

injectionBinder.Bind<IEnemy>().To<Dwarf>().ToName(EnemyType.ADVANCED);
injectionBinder.Bind<IEnemy>().To<Rat>().ToName(EnemyType.BASIC);

The [Inject] tag is then extended by the respective name.

[Inject(EnemyType.BASIC)]
public IEnemy enemy {get;set;}

Polymorphic binding is possible, too, by connecting several interfaces to the concrete class.

injectionBinder.Bind<IHittable>().Bind<IUpdateable>().To<Dwarf>();

An illustration of the MVC component model consisting of model, view and controller.

 

Wherever either IHittable or IUpdateable is injected, you now get the instance of the Dwarf class and/or the corresponding interface. As we can see, the injectionBinder defines upfront what the Injector will deliver when asked for a certain class. The class that was injected in therefore doesn't know whether the dependency is a Singleton or a new instance. In case of this simple binding, there's always a new instance of MyClass that's injected. In reference to the Factory design pattern, this is called »Factory Mapping«. However, often there are central classes that only a single instance is needed of. What would be possible, for example, is an object that provides configuration data. The ToSingleton() method is suggested here. Many are probably familiar with the Singleton pattern as a design pattern. It injects an instance which only exists once. Again, we're using an interface.

injectionBinder.Bind<IConfiguration >().To<Configuration>().ToSingleton();

But what if we want to inject an object that we have received from an external instance? For example an object that provides configuration data? That's when »value mapping« comes in.

Configuration config = loadConfiguration();
injectionBinder.Bind<IConfig>().ToValue(config);

When I inject a class, the Injector makes sure that all injections within that class are available. However, when I instantiate an object with »new MyClass()«, no dependencies will be injected since the Strange framework obviously takes no notice of the instantiation with the new operator. So if you want to inject a class manually and make sure its dependencies will be injected as well, you have to use the GetInstance<Classname>() method.

IMyClass myInstance = injectionBinder.GetInstance<IMyClass>() as IMyClass;

If you then want to access injections directly in the constructor of MyClass, be prepared for a surprise. The injections aren't available at all simply because they haven't been injected yet at that point. In this case you may use the [PostConstruct] tag. Each method this tag is applied to will be executed immediately as soon as all injections are available. So the alleged problem with the constructor is actually no problem at all. Since all dependencies are provided by the Injector, there's generally no need for a parameterized constructor. So some rethinking must be done in terms of that.

The MVC component model

The three different components of MVC are the model, the view and the controller. The model stores the data, the view takes care of displaying this data while the controller is all about operational logic.

The model

Imagine, for example, an RPG like »Diablo«: There is an abundance of items whose data needs to be stored somewhere. Any such item can have various attributes like, for example, the damage it inflicts. Apart from graphic data which belongs in the view, an item object therefore stores anything that's somewhat relevant for an item.

An RPG like »Diablo 3« has an abundance of items whose data needs to be stored somewhere.

 

In order to accommodate the item objects, we generate, for example, the class ItemModel and store it in a dictionary. I recommend to only grant class ItemModel direct access to the dictionary. A client wanting to store or delete item objects in ItemModel, should therefore make do with public methods such as AddItem() or DeleteItem(). If a client still needs the complete list of item objects, ItemModel can convert the dictionary to a list object and provide a getter. In brief: The management of the dictionary should solely be left to the ItemModel. In order to avoid generating dependencies towards the client, the model class should, of course, implement an interface again.

The reason this model is interesting for the other components is that preferably all of them want to access it. That's why I would like to give some advice on how to avoid implementing the operational logic in the wrong places.

  • Write access to the model should only be performed by the controller and/or a command. Read access is allowed, too, of course.
  • A mediator can inject the model to access it. However, access should be limited to reading only. This creates a dependency between mediator and model, but since we work with interfaces, it's nothing to worry about and can therefore save quite a bit of writing work.
  • A model should not send any messages to the Strange framework. A command should do this task instead after changing data in the model.
  • A model itself should not take notice of any messages.

The controller

The controller takes care of the operational logic which is basically built on the command pattern. You essentially use a separate class here which derives from the framework's command class. This class overwrites the »Execute()« method of its super class which is executed during the instantiation process. This method will then include the actual operational logic. A command should have complete access to the model, i.e. read and write access. The instantiation of the command can be performed manually but also indirectly by an external instance. In latter case, the messaging system in Strange will be used. A message's sender can be of various origins. It can be a user interaction or a service that has finished loading certain data. It also happens sometimes though that the controller wants to tell the view something, for example when data in the model has changed and the view needs to be updated. In this case, the command sends a message to the framework which a mediator can react to. Commands are generally short-dated objects. If a command has been instantiated and its execute method carried out, the command object will be destroyed. However, you can use the asynchronous methods Retain() and Release() if you want to control it manually. This may be helpful in case the command is waiting for an asynchronous reply from a service.

The view

The view is in charge of the graphic presentation of our game and usually consists of a view component which derives from MonoBehavior. If it is a complex view like, for example, a graphic interface, a mediator is added to the view component. Why a mediator? A view component should not directly communicate with the Strange framework. This task is carried out by the mediator which serves as an interface for the view component. Therefore one of the mediator's tasks is to receive and process messages from the framework and then adjust its view component accordingly. If, on the other hand, the view wants to inform the mediator about something, e.g. a user action, this is done again via messaging. The view sends a message which is received by its mediator which then decides what to do with it. In general, the mediator now generates another message which is linked to a command. Each view component has its own mediator. A mediator is only ever generated when its view is instantiated. Therefore the binding of the mediator to its view component needs to be carried out in the MVCSContext as soon as possible.

mediationBinder.Bind<DashboardView>().To<DashboardMediator>();

Here's a little hint: One should try to keep the mediator as clean as possible, i.e. implement as little logic as possible in the mediator – which is usually always the case when changing something in the view. For this purpose, we can use commands again. But this time, we don't generate a binding between a command and a message. Instead we inject the command with injector.GetInstance<Commandname>() while referencing the view component. We then perform the execute method in command. All changes necessary in the view component will now be carried out in the command's execute method. To get a better overview, one can also label commands which reference view components as behavior.
So these are the three components: model, view and controller. Obviously, none of them have any effect on their own. So how do these modules communicate with each other?

The indie game »Ironshade« uses StrangeIoC/MVC for a more flexible code architecture.

 

The messaging system

In order to exchange messages between the individual components in the Strange framework, either events or signals can be used. Both variants offer the option to transport objects to the receiver, although developers of the framework recommend the use of signals rather than events, simply because events are a lot more inflexible because compared to signals they can only transport a single data object and aren't type-safe. Additionally, events are constantly re-instantiated and therefore strain the garbage collector. A combination of events and signals is not intended, by the way. One needs to define exactly in the application's MVCSContext which variant is going to be used. A simple signal with two parameters looks like this.

public class PositionUpdatedSignal : Signal<string> { }

The signal needs to be mapped in MVCSContext before it can be injected.

injectionBinder.Bind<PositionUpdatedSignal>().ToSingleton();

We can then inject and dispatch the signal.

[Inject]
public PositionUpdatedSignal signal {get; set;}
signal.Dispatch(value);

This is a simple signal that can be generated by the controller or a view component, for example. The instance which wants to receive the signal (e.g. a mediator) needs to inject the signal as well and reference a method.

PositionUpdatedSignal.AddListener(OnPositionUpdated);

A signal can also be linked to a command by using »commandBinder«.

commandBinder.Bind<KeyPressedSignal>().To<HandleKeyboardCommand>();

The generator of this signal would be, for example, a mediator which wants to activate the controller. I'm going to show the corresponding command in the next picture. Please note that parameters (such as keyCode) need to be transferred with signal.Dispatch(keyCode) and injected in command.

public class HandleKeyboardCommand: Command{
   [Inject]
   public KeyCode keyCode{get; set;}
   public override void Execute (){
     //Programmlogik
   }
}

Let's take a more detailed example case for using signals. Let's assume the user clicks on an icon on screen which is supposed to throw an arrow onto the opponent in view. In code, this looks like follows:

  • The view reacts to a user interaction and sends a signal which the mediator receives.
  • The mediator receives the view's signal and generates another signal linked to a command.
  • The command checks in the model if the requirements for generating an arrow have been met. If so, the command reduces the number of arrows in the model.
  • The command then generates a signal with the target coordinates as the transport object.
  • The mediator receives the signal and passes the coordinates on to the view.
  • The view draws an arrow onto the screen, moving it to the target coordinates.

In this case, the mediator communicated with the controller and the view. Another scenario would be, for example, moving an object which is already on screen.

  • The Command checks in the model if the requirements for moving the object have been met. If so, the command changes the coordinates of the object in the model. They don't necessarily have to be real 3D coordinates. It can also be coordinates in a grid, which the view then converts to real 3D coordinates.
  • The command now sends a signal to the mediator with the object ID as parameter.
  • The mediator injecting the model receives the signal and accesses the data object in the model, based on the ID. The determined coordinates are converted to real 3D coordinates and then forwarded to the view component.

Performance

Frameworks using the dependency injection are usually said to be slow. This is due to reflection – in this case study, reflection in C#. Performance tests supporting this thesis can be found online on relevant websites. The problem here is that a lot of those tests are lab tests which are never applied in practice. Obviously it's not exactly performant to inject 10,000 dependencies in a loop. Therefore, the problem is usually implementation. StrangeIoC also offers some mechanisms in order to accelerate reflection, for example by writing each reflection to a cache. One should therefore consider putting up with a marginal speed loss for the sake of a clean, expandable architecture.

Where can I get it?

StrangeIoC is free of charge and can be downloaded from the Unity Asset Store. The package contains various sample projects, including one that shows the use of signals.

 

Nurdogan Erdem

 

Further reading on MakingGames.biz