This article is intended for novice programmers who are looking at using Unity in a repeatable and less tightly coupled way than they currently are. I assume a basic understanding of inheritance and generics as well as Unity functions such as
The backbone of Unity games are GameObjects and MonoBehaviours. One of the complications with using them is that we cannot utilise constructors when creating an instance of these objects. So there is a fine balancing act instantiating and seeding MonoBehaviours when they are being added to a scene dynamically via code.
This article will explore different design patterns that attempt to provide constructor-like functionality to MonoBehaviours to make them a little more user-friendly for programmers used to the Object-Oriented programming paradigm.
For the purposes of this article we want to create a new Projectile which will be used by a Gun and when we create that projectile we want to give it an initial speed. Ideally, in an Object-Oriented paradigm we would pass this value in through the constructor like so.
However in Unity we are forbidden from using constructors on MonoBehaviours because they are utilised by the Editor before and after serialisation. Using them can lead to editor and engine breaking side effects when used improperly. In 2016 Unity introduced errors whenever trying to call Unity “main thread” API endpoints from constructors (and pretty much everything is a main thread API endpoint). If you’d like to read about it here is a short Official Unity Blog post.
The Unity way to create our projectile and set it’s speed would look something like the following.
Not only is there a lot more boilerplate code here but we have broken the encapsulation on the Projectile by making the speed float public. We also have no good way to make sure the projectile has a valid speed when it is created.
This is the crux of most of the Unity pattern. It is super flexible, values can be added in the Editor or via code, but because of that a lot code based implementations feel very tightly coupled. If we want to change the projectile to use a prefab instead of a new game object any class that makes a Projectile has to go and implement these changes. *EVERYWHERE.* The gun probably shouldn’t even know how a projectile goes together, it should just be able to make one. What are some ways we can get around this?
The Init Method is the most simple implementation which, likely because of its simplicity, is also the most useful in most situations. Simply add an Init method to any class that needs to be set up with values as soon as it is created.
- Easy to implement
- Unable to enforce use of the Init Method (we will discuss this later)
- Unable to use the Awake Method to set up anything else that relies on speed (this will also be a common theme moving forward)
Static Create Method
I like the Static Create Method approach because it is like the Init method above wrapped in some syntactic sugar. For basic instantiation we often want to repeat the same code over and over again. By leveraging a common base class we can do a lot of that repeatable code in one place. We can even generic out the base class, perhaps like below.
We could then add our own custom Create implementation in the child class, which we were doing with our Init function anyway…
Except now we don’t have to worry too much about actually creating and naming our GameObjects which is nice. We have also preserved encapsulation in our Projectile because the static method can work on the private members of that instance when it creates it. Then lastly our gun can create the projectile.
- Some nice syntactic sugar around Init
- Core implementation changes can happen in one place
- Obfuscates the need for Gun to know how to make a projectile
- Unable to enforce use of the Create Method
- Unable to use the Awake Method to set up anything else that relies on speed
Nested Private Class
This was about the weirdest and most restrictive use case I could come up with. It was derived from the idea that if we decide on a non-standard instantiation method for Monobehaviours it needs to be enforcable. My requirements for “enforcing” a code style (without git hooks or linting or anything crazy) was to make it so that you could not manually add the Component to a GameObject in the scene and also not randomly call AddComponent from anywhere in the game and improperly set up the MonoBehaviour.
Thus the Nested Private Class solution. How does this work, why does this work and should I use it?
How it works
This works by abusing encapsulation. Despite the speed float being public in the ProjectileBehaviour class, because that class itself is a private helper of the Projectile class, we can never get it’s values from outside of the parent.
Why it works
This boils down to how Unity handles MonoBehaviours. If a MonoBehaviour does not contain the same name as the file in which it resides (private or otherwise) then you cannot add it to a GameObject in the Editor.
Should I use it?
Probably not. One neat little trick though was to add some implicit operators so if I really wanted I could do something like this…
There are many cases where you might want to make a new Component but only care about it’s Transform, or some other general type. For example you may want to track it’s position, or see if it still exists in the scene. Although we created a new class of type Projectile we can implicitly operate on it as if it were a Transform.
- Enforces creation via class constructors
- Can’t get the actual MonoBehaviour type (in this case ProjectileBehaviour) back out due to encapsulation. You would need to wrap everything at the class level
- Not easily repeatable, at least, I’ve not found a good way to generic this out
- Implicit operators aren’t overly verbose and can make for hard to read code later
- Unable to use the Awake Method to set up anything else that relies on speed
I wouldn’t really recommend this pattern, but it does work!
Making Your Own MonoBehaviour
If I was willing to nest a private MonoBehaviour class inside a normal class, could I not just wrap the entire GameObject class? Of course I could! I shouldn’t, and for the sake of stopping this article getting too long I won’t show you the whole thing. But if you were nuts about attempting to enforce this pattern everywhere you could do something crazy like this.
I don’t suggest you try this at home, but for the curious…..
Making my Projectile:
- Able to enforce constructor based instantiation
- It feels “GameObjecty” when coding in the Projectile class because it utilises the Unity Lifecycle methods
- It is super over-engineered (and isn’t that what this is all about)
- It is super over-engineered
- You would need to expose and maintain all the functions that MonoBehaviours expose (which seems entirely pointless)
- All those “magic” functions that Unity looks for, such as Update, will not be optimised out of your build and each one of your ScottoBehaviours will be creating unnecessary calls to empty functions
- I didn’t performance check it but all those delegates would surely cause lots of issues down the track
- I couldn’t get the Finalizer to fire on my ScottoBehaviour either?
Prefabs & Factory Method
Prefabs are pre-made GameObjects with preset values that can be reused over and over again like a template in your game. You can make them in the editor and then save them into the project hierarchy for re-use within your project.
If you wanted to implement something that was the MOST Unity I would suggest creating Prefabs and using a class to return an instance of that Prefab at run-time. The specific implementation is up to you. A singleton instance would allow you to drag and drop the Prefabs into your game at run-time. A static class would allow you to load Prefabs from either an Asset Bundle or the Resources folder or you could come up with your own serialisation and deserialisation methods. Perhaps look at combining with a pool for even better performance! As an example I have written a simple static class that loads assets from the Resources folder based on a string value. Please don’t use string values in production code, use an enum or something else that can’t easily be typed incorrectly. The code below is merely supposed to demonstrate the concept.
Our Gun would then create a Projectile in the following fashion.
- Utilises all the Unity tools rather than trying to fight them
- Allows for tweaks to be done in editor rather than always in the code base
- No weird class wrapping
- Gun doesn’t need to concern itself with the speed of a bullet
- Can’t enforce creation through the factory method
- Can’t use the Awake function in projectile
Most game developers that I talk to that work on larger projects seem to spend most of their time fighting with Unity rather than embracing it’s paradigms. Sometimes there is good reason to do that but often I think it’s to try and do things a “better” way. Which, aside from being totally subjective, takes a lot of time away from making the actual product.
In almost all my test cases aside from going to everyone I work with and saying “This is the pattern we use for instantiation on this object now” there isn’t a great way to enforce a “newable” pattern on MonoBehaviours. All the solutions above would work in different circumstances but none of them work well in all situations. I tried very hard to do run-time checks to make sure an Init or Create function were called but they are simply too easily overridden and any other service on top just makes these approaches too heavy to manage. I’d like to explore linting rules later around this problem to see if that is an enforcement option.
Interestingly in all the situations, with exception of the entirely wrapped GameObject class, calling AddComponent on any MonoBehaviour class basically renders the Awake method moot if it needs to use any of the values you want to set via a script. Whether you do that the good old fashioned way, or over-engineer some nonsense like above, Awake is called immediately. You just have to deal with the fact that your MonoBehaviour won’t be set up in time properly to utilise that method so stick with Start if your set up requires these values.
Top comments (0)