There are many ways to design your Unity app from a coding perspective. Today’s article talks about just one path you might take. Like any path, it’ll have its own advantages and disadvantages. The choice of app code design will make a huge impact, so it’s good to think about it before you start down any path. Perhaps this is the right one for you…

The “pure code” path— as mentioned briefly last week—minimizes the MonoBehaviour class as much as possible. This has the effect of minimizing the Unity GUI editor too. Minimizing the editor maximizes the code, hence the name.

The MonoBehaviour class is minimized by creating as few derivative classes of them as possible. Usually this entails just one or two in the whole app. The first runs almost the entire app’s logic and is attached to a single empty GameObject in a single Unity scene. The Start or Awake function of this MonoBehaviour is treated like the constructor of a Flash app’s “document class” or “root Sprite” or a command line app’s main or Main function. It’s just like the application’s entry point. Here’s how one might look:

using UnityEngine;
 
public class MyApp : MonoBehaviour
{
	public void Start()
	{
		// {all app startup code}
	}
}

Likewise, the Update, FixedUpdate, LateUpdate, OnApplicationQuit, and various other functions are treated like app-wide events rather than events regarding a particular actor in the scene. The MonoBehaviour now looks like this:

using UnityEngine;
 
public class MyApp : MonoBehaviour
{
	public void Start()
	{
		// {all app startup code}
	}
 
	public void Update()
	{
		// {all app logic}
	}
 
	public void OnApplicationQuit()
	{
		// {all app shutdown code}
	}
}

Other game objects may be added to the scene, but they never have a MonoBehaviour added to them for the purposes of controlling that GameObject. Instead, an auxiliary MonoBehaviour is used simply for for the purposes of forwarding events related to the GameObject. Without this MonoBehaviour, it’d be quite difficult in this scheme to use the GameObject in many ways. Here’s how that event-forwarding class might look with just a couple of events:

using UnityEngine;
 
public class EventForwarder : MonoBehaviour
{
	public delegate void EventHandler<T>(T e);
 
	public event EventHandler<Collision> CollisionEnterEvent = c => {};
	public event EventHandler<float> JointBreakEvent = f => {};
 
	public void OnCollisionEnter(Collision collisionInfo)
	{
		CollisionEnterEvent(collisionInfo);
	}
 
	public void OnJointBreak(float force)
	{
		JointBreakEvent(force);
	}
}

The EventForwarder class performs no logic of its own. Instead, it receives events using Unity’s “magic event function” (for lack of a better term) system and forwards them along via the standard C# event system using the delegate and event keywords.

With this event-forwarding MonoBehaviour in place, every GameObject except the “main” one is treated simply as input and output, not as an independent object that handles its own logic. All other GameObject instances are really just tools of the “main” GameObject.

At this point it may sound like the MonoBehaviour on the “main” GameObject will become absolutely huge and unmaintainable. This is certainly not the case though any more than it is the case for any given command-line application like a compiler. The key is to minimize the size and complexity of the “main” MonoBehaviour by simply forwarding along the Start, Update, and other events to other classes. These classes are not derived from MonoBehaviour and they therefore are not attached to any GameObject.

Of course there are tons of different ways to structure your non-MonoBehaviour classes. One common approach is to use some form of a finite-state machine to represent the app’s major states like UI screens or gameplay modes. In turn, these state classes delegate much of their logic to the components of those states like UI widgets/components or game elements.

If the above all sounds very straightforward, that’s because it is. The preceding paragraph simply describes a form of object-oriented programming (OOP), which is ubiquitous these days. Everything else in this article only exists to shoehorn “standard” programming like OOP into Unity. That’s not to say that OOP is required. You could certainly use imperative or functional programming (e.g. with F#) if you’d rather.

The “pure code” path is great for programmers coming from other environments—including Flash—because it ignores a lot of Unity-specific quirks and details. This is also its main drawback as some of those quirks and details are quite powerful and ignoring them negates their advantages. For example, this approach doesn’t use the Unity editor’s ability to show public variables in the Inspector panel. The “normal” approach to Unity programming might have a public int Health field on an Enemy MonoBehaviour attached to the GameObject with that enemy’s graphical representation. Selecting the enemy GameObject in the Hierarchy panel of the Unity editor would show the Enemy component including a “Health” field with the current health. That might be interesting to see or change for debugging purposes.

Overall, losing this capability is mainly a drawback. However, it can also be an advantage when it comes to the design of many classes. For access protection, you probably don’t want to make every field of every class public. However, in the above example it was necessary to make the Health field a public variable in order for it to show up in the Inspector panel of the Unity editor. A private variable or a public property won’t work unless the SerializeField workaround property is used.

Another advantage is that typical MonoBehaviour derivatives don’t really get to use constructors:

Note to experienced programmers: you may be surprised that initialization of an object is not done using a constructor function. This is because the construction of objects is handled by the editor and does not take place at the start of gameplay as you might expect. If you attempt to define a constructor for a script component, it will interfere with the normal operation of Unity and can cause major problems with the project.

But constructors are essential to core OOP techniques like RAII. Losing them does serious damage to code maintainability, quality, and ease of use. Workarounds like creating an Init function and remembering to call it every time provide only weak workarounds.

Again though, this is just another example of the “pure code” approach’s philosophy of code purity over all else. It’d rather give up some Unity editor usefulness to get access to techniques that many would argue make code “cleaner”. In the end, as always, the decision will be up to the individual development team and must be made with project-specific factors—skill sets, timelines, etc.— in mind. This is definitely not the “normal” approach to programming Unity projects, but it’s probably worth considering when you plan out a new project.

What do you think of the “pure code” approach? Post a comment and let me know!