In this post I will present my own implementation of a component-based architecture which closely mimics Unity’s implementation. It really isn’t as hard as one might think. Why would I ever do such a thing? I think I have plenty of good reasons, but I’ll let you decide whether or not its all worth it. As always, feel free to critique!
What is Component-Based Architecture
Put simply this is a system where a generic container holds the components which make it up. In Unity, the GameObject is the container which has the ability to add components – such as scripts inheriting from MonoBehaviour.
In my opinion, the use of this style of architecture helps you to favor object composition over class inheritance – a good thing. If I have lost you, then imagine the following scenario. We are creating an RPG and you are tasked with implementing the item and inventory system. Using inheritance you might implement this system as follows:
- Base Item Class (might hold information like a buy and sell price)
- Base Consumable Sub Class
- Health Potion
- Base Equippable Sub Class
- Base Armor Sub Class
- Light Armor
- Heavy Armor
This list shows how one might attempt to minimize code-duplication through a deep inheritance hierarchy. This list is a very brief example, and an abused inheritance chain could end up far deeper than this. The problems with inheritance appear when you start needing exceptions to the rule:
- What if we want items which you can equip or consume, but can’t sell, perhaps because they are key story items, or because they are cursed, etc?
- What if an item is both consumable and equippable? In some final fantasy games you can allow a character to use an un-equipped item to access its special features like casting a spell.
- How can you share special features between different inheritance branches?
It is possible to overcome these issues, but often, the approach taken is less than elegant and additional properties are added to the base classes which end up unused and wasted by every other class EXCEPT the exception case.
If you approached the same tasks via component based architecture, then the code would look very different. For starters, you don’t even need the base item class. The item itself would be a container class (like a GameObject) which is very reusable. To make the item sellable, you simply add the appropriate componet, say a Merchandise component. Any object which doesn’t have this component wont be added to a store menu and you can’t sell it – problem solved. To make an item equippable or consumable, again add the appropriate component. If you want it to be both equippable AND consumable, just add BOTH components. Again, problem solved. If you want, you can still use inheritance within this pattern, but the inheritance chain should be more shallow.
Why create my own instead of using Unity’s implementation?
So why would I do something like this? I’m glad you asked. Suppose you’re making an RPG (which I am trying to do). You will need to model (in the logical sense) your characters, items, equipment, etc. These items could appear visually in a variety of places such as the game’s over-world, a battle scene, a store, a menu, etc. This leads to the idea of separating your model from your view for reusability sake.
I would suggest reading about the Model-View-Controller architecture pattern if you are unclear so far.
I suppose it is only natural that l would implement the model portions of this code without inheriting from MonoBehaviour in order to maintain the greatest amount of flexibility and the least amount of baggage. That is how I started out. With something as complex as an RPG though, you need rules and exceptions, and even exceptions for your exceptions. You wont realize just how complex the system is until you try to write it yourself. I found that my models became smarter – they tried doing more things and even posted and responded to events. Not only did I struggle with tightly coupled classes, but I found it hard to come up with elegant and consistent ways of doing something as simple as knowing when to unregister from the events.
Why is that hard?
If you don’t know why good architecture for registering and unregistering for events can be hard, then consider the following example. Open a new Unity scene and attach a new script called Demo to the camera.
using UnityEngine; using System; using System.Collections; public class Demo : MonoBehaviour { public static event EventHandler apocolypseEvent; void Start () { CreateLocalScopedInstances(); GC.Collect(); if (apocolypseEvent != null) apocolypseEvent(this, EventArgs.Empty); } void CreateLocalScopedInstances () { new Mortal(); new Immortal(); } } public class Mortal { public Mortal () { Debug.Log("A mortal has been born"); } ~ Mortal () { Debug.Log("A mortal has perished"); } } public class Immortal { public Immortal () { Debug.Log("An immortal has been born"); Demo.apocolypseEvent += OnApocolypticEvent; } ~ Immortal () { // This won't ever be reached Debug.Log("An immortal has perished"); Demo.apocolypseEvent -= OnApocolypticEvent; } void OnApocolypticEvent (object sender, EventArgs e) { Debug.Log("I'm alive!!!"); } }
In this demonstration, I use a method to instantiate local object instances of a Mortal and an Immortal. Normally, when creating an object with local scope, you will expect the object to be able to go out of scope when the method ends. This is the case with the Mortal object which we can see perishes thanks to Garbage Collection.
Why does the Immortal object survive while the Mortal does not? No, it isn’t because of the name. In this example I showed a common mistake – I tried to use a Constructor as an opportunity to register for an event (like I might do with Awake, or OnEnable) and I also tried to use the Destructor as an opportunity to un-register from an event (like I might do with OnDisable or OnDestroy).
The problem is that the Immortal object survives because the static event maintains a strong pointer to it, and worse, the static event will NEVER be out of scope (well at least for as long as the app runs). I can’t use the Destructor as an oportunity to unregister because the object can’t be destroyed while a strong pointer keeps it alive.
To put things into perspective, Garbage Collection was added as a language feature because programmers frequently made mistakes with memory management. Managing the lifespan of an object can be very tricky and becomes trickier as the complexity of your system grows. Hopefully that helps illustrate why it isn’t always obvious to know something as seemingly simple as when you should add or remove event listeners.
Exit the Rabbit Trail
As I struggled with the complexities of my code, I began to sorely miss some of the wonderful features Unity so elegantly provided:
- I love the ability to model object’s in a hierarchy.
- I love the flexibility of a good Component based architecture which allows me to Get components from their shared container. Not to mention from a parent or child container within the same hierarchy.
- I love that all components can receive calls to life-cycle methods such as Awake, OnEnable, OnDisable, and OnDestroy.
All of these features make it easy to keep code decoupled, provide great opportunities for clean-up such as unregistering from events, and generally make a complex data model easier to understand. Still, this easy approach (as provided by Unity) comes with a lot of extra baggage as well as unfortunate constraints (like no background threads).
So I began to wonder… would it really be that hard to write my own component based system which has all the features I want from a MonoBehaviour?
On to the Code!
One of the hardest parts, sigh, was coming up with good names. I didn’t want to confuse people by re-using GameObject and Component in another namespace. I spent a lot of time looking at synonyms and finally settled on using Whole as my container and Part as my component. Furthermore, I decided to program to an interface just in case I might want to integrate the system with a Monobehaviour based Part etc, but because of that I needed to avoid duplicate property and method names as well.
Interfaces
First up is the Part (the component portion of the pattern). Aything implementing this interface must expose a reference to its container – the IWhole. I used two additional properties: allowed, and running which are very similar to activeSelf and activeInHierarchy from a GameObject. For example, a part can be allowed to run, but if the container is not allowed to run, then its parts are not running. Only if a part and its container and all parent containers are allowed, will the running flag be marked as true.
The Check method is used to tell a Part that it should check its state (whether it is running or not) – such as after modifying a parent hierarchy or toggling the allowed property of a component or container.
The Assemble method is basically the equivalent of Awake – it will only be triggered one time – the first time that the part is both allowed and running. The Disassemble method is its counterpart and would be similar to OnDestroy. It also only fires one time – before the part goes out of scope.
Resume and Suspend are similar to OnEnable and OnDisable – they could potentially fire multiple times. Anytime the part switches from not running to running, it is Resumed. Switching from running to not running triggers Suspend.
using UnityEngine; using System.Collections; public interface IPart { IWhole whole { get; set; } bool allowed { get; set; } bool running { get; } void Check (); void Assemble (); void Resume (); void Suspend (); void Disassemble (); }
Next up is the Whole (the container portion of the pattern). This interface has a few similarites to the Part such as the allowed, and running properties along with the companing Check method.
Otherwise, this interface looks a lot more like a mix between a GameObject and a Transform. It has the ability to Add and Remove Parts, Get Parts (from itself or the hierarchy) and manage its hierarchy (such as by adding a child or modifying its parent).
using UnityEngine; using System.Collections; using System.Collections.Generic; public interface IWhole { string name { get; set; } bool allowed { get; set; } bool running { get; } IWhole parent { get; set; } IList<IWhole> children { get; } IList<IPart> parts { get; } void AddChild (IWhole whole); void RemoveChild (IWhole whole); void RemoveChildren (); T AddPart<T> () where T : IPart, new(); void RemovePart (IPart p); T GetPart<T>() where T : class, IPart; T GetPartInChildren<T>() where T : class, IPart; T GetPartInParent<T>() where T : class, IPart; List<T> GetParts<T>() where T : class, IPart; List<T> GetPartsInChildren<T>() where T : class, IPart; List<T> GetPartsInParent<T>() where T : class, IPart; void Check (); void Destroy (); }
Implementation
Here is a class implementation for the IPart interface. Note that the component only has a weak reference to its container. This way, an object container can go out of scope even if there are strong references to its components. Originally I planed to assign this reference in a constructor, but I didn’t like any solutions I found which allowed me to create and add the parts using generics.
using UnityEngine; using System; using System.Collections; public class Part : IPart { #region Fields / Properties public IWhole whole { get { return owner != null ? owner.Target as IWhole : null; } set { owner = (value != null) ? new WeakReference(value) : null; Check(); } } WeakReference owner = null; public bool allowed { get { return _allowed; } set { if (_allowed == value) return; _allowed = value; Check(); } } bool _allowed = true; public bool running { get { return _running; } private set { if (_running == value) return; _running = value; if (_running) { if (!_didAssemble) { _didAssemble = true; Assemble(); } Resume(); } else { Suspend(); } } } bool _running = false; bool _didAssemble = false; #endregion #region Public public void Check () { running = ( allowed && whole != null && whole.running ); } public virtual void Assemble () { } public virtual void Resume () { } public virtual void Suspend () { } public virtual void Disassemble () { } #endregion }
Here is a class implementation for the IWhole interface:
using UnityEngine; using System.Collections; using System.Collections.Generic; public sealed class Whole : IWhole { #region Fields / Properties public string name { get; set; } public bool allowed { get { return _allowed; } set { if (_allowed == value) return; _allowed = value; Check(); } } bool _allowed = true; public bool running { get { return _running; } private set { if (_running == value) return; _running = value; for (int i = _parts.Count - 1; i >= 0; --i) _parts[i].Check(); } } bool _running = true; public IWhole parent { get { return _parent; } set { if (_parent == value) return; if (_parent != null) _parent.RemoveChild(this); _parent = value; if (_parent != null) _parent.AddChild(this); Check (); } } IWhole _parent = null; public IList<IWhole> children { get { return _children.AsReadOnly(); }} public IList<IPart> parts { get { return _parts.AsReadOnly(); }} List<IWhole> _children = new List<IWhole>(); List<IPart> _parts = new List<IPart>(); bool _didDestroy; #endregion #region Constructor & Destructor public Whole () { } public Whole (string name) : this () { this.name = name; } ~ Whole () { Destroy(); } #endregion #region Public public void Check () { CheckEnabledInParent(); CheckEnabledInChildren(); } public void AddChild (IWhole whole) { if (_children.Contains(whole)) return; _children.Add(whole); whole.parent = this; } public void RemoveChild (IWhole whole) { int index = _children.IndexOf(whole); if (index != -1) { _children.RemoveAt(index); whole.parent = null; } } public void RemoveChildren () { for (int i = _children.Count - 1; i >= 0; --i) _children[i].parent = null; } public T AddPart<T> () where T : IPart, new() { T t = new T(); t.whole = this; _parts.Add(t); return t; } public void RemovePart (IPart p) { int index = _parts.IndexOf(p); if (index != -1) { _parts.RemoveAt(index); p.whole = null; p.Disassemble(); } } public T GetPart<T>() where T : class, IPart { for (int i = 0; i < _parts.Count; ++i) if (_parts[i] is T) return _parts[i] as T; return null; } public T GetPartInChildren<T>() where T : class, IPart { T retValue = GetPart<T>(); if (retValue == null) { for (int i = 0; i < _children.Count; ++i) { retValue = _children[i].GetPartInChildren<T>(); if (retValue != null) break; } } return retValue; } public T GetPartInParent<T>() where T : class, IPart { T retValue = GetPart<T>(); if (retValue == null && parent != null) retValue = parent.GetPartInParent<T>(); return retValue; } public List<T> GetParts<T>() where T : class, IPart { List<T> list = new List<T>(); AppendParts<T>(this, list); return list; } public List<T> GetPartsInChildren<T>() where T : class, IPart { List<T> list = GetParts<T>(); AppendPartsInChildren<T>(this, list); return list; } public List<T> GetPartsInParent<T>() where T : class, IPart { List<T> list = new List<T>(); AppendPartsInParent<T>(this, list); return list; } public void Destroy () { if (_didDestroy) return; _didDestroy = true; allowed = false; parent = null; for (int i = _parts.Count - 1; i >= 0; --i) _parts[i].Disassemble(); for (int i = _children.Count - 1; i >= 0; --i) _children[i].Destroy(); } #endregion #region Private void CheckEnabledInParent () { bool shouldEnable = allowed; IWhole next = parent; while (shouldEnable && next != null) { shouldEnable = next.allowed; next = next.parent; } running = shouldEnable; } void CheckEnabledInChildren () { for (int i = _children.Count - 1; i >= 0; --i) _children[i].Check(); } void AppendParts<T> (IWhole target, List<T> list) where T : class, IPart { for (int i = 0; i < target.parts.Count; ++i) if (target.parts[i] is T) list.Add(target.parts[i] as T); } void AppendPartsInChildren<T>( IWhole target, List<T> list ) where T : class, IPart { AppendParts<T>(target, list); for (int i = 0; i < target.children.Count; ++i) AppendPartsInChildren<T>(target.children[i], list); } void AppendPartsInParent<T>( IWhole target, List<T> list ) where T : class, IPart { AppendParts<T>(target, list); if (target.parent != null) AppendPartsInParent<T>(target.parent, list); } #endregion }
Another Demo
Did you follow along with the first demo? The one where an Immortal object was not garbage collected because it had registered for a static event? Well, if so, then this next demo will look familiar. Thanks to my new architecture, this time I have an object which can safely register for the static event, but also unregister and be garbage collected.
using UnityEngine; using System; using System.Collections; public class Demo : MonoBehaviour { public static event EventHandler apocolypseEvent; void Start () { CreateLocalScopedInstances(); GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); if (apocolypseEvent != null) apocolypseEvent(this, EventArgs.Empty); } void CreateLocalScopedInstances () { IWhole whole = new Whole("Mortal"); whole.AddPart<MyPart>(); } } public class MyPart : Part { public override void Resume () { base.Resume (); Debug.Log("MyPart is now Enabled on " + whole.name); Demo.apocolypseEvent += OnApocolypticEvent; } public override void Suspend () { base.Suspend (); Debug.Log("MyPart is now Disabled"); Demo.apocolypseEvent -= OnApocolypticEvent; } ~ MyPart () { Debug.Log("MyPart has perished"); } void OnApocolypticEvent (object sender, EventArgs e) { // This won't actually be reached Debug.Log("I'm alive!!!"); } }
Summary
In this post, I mentioned some of the difficulties of writing an RPG architecture which doesn’t suck. Once I got passed that, I helped demonstrate what difficulties I had personally encountered to help explain why in the world I would ever want to write my own Component-Based Architecture along the lines of what Unity had already provided. I showed my implementation and a demo which overcame one of the challenges I brought up, and hopefully now you understand both the why and the how behind this post.
The code I provided here is very fresh (read – not battle tested) so it is very possible there is room for improvement. If you have anything good, bad, or indifferent to say feel free to comment below.
I’m guessing you’ve worked on it for a while now and I wanted to ask if the versatility compensates the loss of the visual editing (and debug ease)
I did work with this code for a little while before creating the post. I privately implemented some early portions of the Tactics RPG project with it. I enjoyed using it, but because of negative feedback from readers (on reddit comments etc) I decided not to use it in my projects, and therefore the code isn’t thoroughly battle-tested. I feel it has potential, but at the moment still lacks some conveniences a MonoBehaviour would provide for rapid prototyping.
Whether the versatility compensates for the loss of those features or not is really going to end up an opinion for you to decide. I have stuck with MonoBehaviour on the Tactics RPG project and am enjoying a fair amount of versatility and visual editing and debug features, so I don’t really feel that I am missing that much.
I think this system would be something I would definitely consider if I wanted Unity style architecture outside of Unity, or if you had particular needs that the Unity architecture was failing to provide (like threading etc) or if you found working with GameObjects too expensive (because you ended up making a ton of them).
Hope that helps 🙂
Thanks for the answer, it definitely helps :p I’ll be sticking with Monobehaviours for now then (the visual debugging is just too good :/)
Keep the tutorials coming! They’re applicable on a lot more genres than rpg’s (Doing a roguelike atm, which shares a lot more with rpg’s than most devs are willing to admit :))
Sounds good, and I’d love to see your project when you are ready to share!
I really liked the explanation and examples of custom components. I had worked on a project that used a similar solution to make custom updates that were handling heavy calculations of artificial intelligence.
I will try to apply the knowledge acquired in this post in my current projects.
its sad all negative feedback but unity has custom editors and well there is the possibility
to debug all this, there is the possibility to add a window and a static instance to read all, also just adding a monobehaviour(property drawers) can work. monoGame has a nice setup to do what you need making something like that can make unity a lot better.
I appreciate any feedback, good or bad. However, I would like to challenge it because custom editors, debugging, and property drawers all have nothing whatsoever to do with the architecture I set up here. They can all work together.
I can still add custom editors and property drawers to my custom non-Unity based objects, and you can debug a game created with this style. Also, “static” can be a useful tool, but relying on it too much may be an indication that your architecture was poorly designed.
[Edited because I misread which post you were responding to]
Hey, I’ve stumbled upon this pretty much for the same reasons as you wrote the code. I’m starting a project that requires composition, and determining interactions between objects from many parts and will require efficient coding.
I’m switching back and forth between just using unity’s native system and simply going with the implementation I’ve designed, trying to weigh pros and cons of each approach.
Could you elaborate on what you believe to be the strengths of each approach, and what exactly steered you from utilizing Unity’s native features?
The pros of the custom system are that you have no architectural restrictions on you that are imposed by Unity, such as whether or not you can use initializers (open the door to all kinds of useful patterns) or background threads (maybe not as valuable), and that you avoid the overhead of Unity’s objects which may contain functionality you wont be utilizing. In addition, the more of your code you “own”, the easier you could port to a different engine (not that you necessarily ever would).
The cons of a custom system are that it isn’t as tightly integrated with Unity, and if you ever work with anyone else, you will probably continually have to say “why” you re-built the wheel. They will also need to learn your new patterns etc. Finally, there is always the possibility that you will introduce bugs that don’t exist with Unity.
With all that said, I personally liked this system, but didn’t end up using it due to negative feedback I got from readers. My projects ended up going with variations of ECS presented by Adam Martin. I think the power of such a system has been demonstrated by the way Unity is now making DOTS. I think what I have here is significantly easier to use, but not as scalable or performant. So a lot of the decisions could come down to what kind of game / performance you require. The kinds of RPG’s I wanted to make (single player) in many cases wont face the same kinds of problems as other games like MMORPG’s or RTS etc.
Thanks for taking the time to answer! I see it’s as I expected. I’m trying to make a somewhat systemic game. Nothing new, just an old genre with some more exploration modelled via composition, and emphasis on emergent narrative through component behaviors.
I’m gonna need a LOT of components, and I’d like for them to have as little overhead as possible. Using as many as I’ll need will probably saturate Unity with multiple calls to its magic methods and ingrained Monobehaviour functionality.
I’m gonna try to model my own ECS-ish thing for this project, as Unity’s implementation is still in early stages and it puts a lot of strain on the editor itself. Unity’s ECS is also much more geared towards performance at the cost of code accesibility, and I’m just not THAT smart yet.
Thanks again for giving me a hand.