Usages Of Nested MonoBehaviour classes in Unity

Posted by : on

Category : Unity

Introduction

In Unity we can add components with the AddComponent command. If we have made our own MonoBehaviour scripts, we can use the AddComponent to add it to a game object, instead of dragging it in the inspector or by pressing the Add Component button.

The name of our MonoBehaviour class has to be the same as the file name. This requirement though, is only for the serialization that happens while we are using the Unity Editor.

In fact Unity doesn’t care, if a MonoBehaviour class is the same name as the file. It is still going to work normally, we will only loose the Editor functionality of adding it to our game objects from the Editor.

This can be used to our advantage, if we don’t want our game designers or whoever is using the Unity Editor to be able to add scripts to our game objects manually, because we want certain scripts to be only added during run time or only by a tool of our own making.

For example, during runtime we may want to have certain MonoBehaviours that are being used as tags for collision, like OnFire, Acidic etc. Although these classes can have normal MonoBehaviour functionality, they are only added to our game objects under certain conditions.

Another use case, is that we may want to have MonoBehaviours that can be added to a gameObject in the editor only with a tool of our own making, that provides some restrictions. For example, we may have Enemy types that only one of those can be added to a game Object.

Finally we may want to encapsulate these classes, so no one else can use them directly, even from code. If someone wants to use these classes will have to do it, by using an API that we have provided. Let’s see how we can do that and some examples for usage in runtime and for editor tools.

Nested MonoBehaviour classes

By creating a class, that has nested private MonoBehaviour classes, we can encapsulate the addition of those classes to the game objects. For example the following is completely valid:

public static class Tags 
{
    public static void AddRotation(GameObject gameObject)
    {
        if(gameObject != null && !gameObject.TryGetComponent(out NestedRotation _))
            gameObject.AddComponent<NestedRotation>();
    }

    public static void RemoveRotation(GameObject gameObject)
    {
        if(gameObject != null && gameObject.TryGetComponent(out NestedRotation rotation))
            Object.Destroy(rotation);
    }
    
    private class NestedRotation : MonoBehaviour
    {
        private void Update()
        {
            gameObject.transform.Rotate(Vector3.forward, 45f * Time.deltaTime);
        }
    }
}

Here the NestedRotation class can only be added via the AddRotation method. This can be done, during runtime or from an Editor tool.

Usage in runtime

For runtime usage, this can be beneficial, if we want to have our objects decorated with certain tags. Although Unity has a tag functionality, in reality it is more like a label. We can have only one tag, for each game object.

If we find ourselves in need for checking certain conditions during collisions, for example a game object can have an Aggressive tag, an OnFire tag and a Flying tag with each one providing its own behaviours, then being able to add these components during runtime and using the OnCollisionEnter to check for those components can simplify our code.

Adding those scripts beforehand and disabling/enabling them whenever is appropriate wouldn’t work, as the collision checks in Unity work for disabled scripts too.

Obviously the AddComponent command is relatively slow, so this wouldn’t work well for performance critical scenarios, but for certain situations, being able to encapsulate our MonoBehaviour components and add them at runtime through code whenever needed can simplify our code architecture.

Usage for Editor tools, an example with Scriptable Wizard

A more common scenario, is using nested MonoBehaviours in our editor tools. If we want to create some tools, that perform certain operations whenever a MonoBehaviour component is added to a game object while denying the ability for someone to add it manually, then encapsulating our MonoBehaviour classes can be really useful.

For example, let’s suppose that we have many enemy types, each one is a MonoBehaviour class, but a game object can have only one of those types. We can create a tool, that adds a type and that tool is the only way to add or change the type. There are no scripts that can be dragged and dropped in the Unity Editor and there is no appropriate field if someone tries to add them by pressing the Add Component button in the game object inspector.

Here is a simple static class, that provides that functionality:

public interface IEnemyType
{
#if UNITY_EDITOR
    public void RemoveComponent();
#endif
}

public static class EnemyTypes
{
#if UNITY_EDITOR
    public static void AddGoblin(GameObject gameObject) => gameObject.AddComponent<Goblin>();
    public static void AddSkeleton(GameObject gameObject) => gameObject.AddComponent<Skeleton>();
#endif
    
    private class Goblin : MonoBehaviour, IEnemyType
    {
        
        // Goblin Code Here Start, Update etc...

#if UNITY_EDITOR
        public void RemoveComponent() => DestroyImmediate(GetComponent<Goblin>());
#endif
    }

    private class Skeleton : MonoBehaviour, IEnemyType
    {
        
        // Skeleton Code Here Start, Update etc...

#if UNITY_EDITOR
        public void RemoveComponent() => DestroyImmediate(GetComponent<Skeleton>());
#endif
    }
}

Here we have two different Enemy types, that are also MonoBehaviours, a Goblin and a Skeleton class. The only way for these classes to be added to a game object, is by calling the AddGoblin and AddSkeleton methods, as both classes are private and nested in the EnemyTypes class.

Now we can make an editor tool that uses these methods. This tool will be the only way to add and change enemy types components to our game objects. For this example, I will use a ScriptableWizard as it is really fast to make one, but obviously any kind of editor tool will do. If you want to learn more about the ScriptableWizard in Unity, you can check my ScriptableWizard post.

public class EnemySelector : UnityEditor.ScriptableWizard
{
    enum EnemyType
    {
        Goblin,
        Skeleton
    }

    [SerializeField] private EnemyType selectedEnemyType;
    private IEnemyType _enemyType;

    [MenuItem("Tools/Enemy Type")]
    private static void CreateWizard() => 
        DisplayWizard<EnemySelector>("Change EnemyType", "Apply Type and Close", "Apply Type");

    private void Awake() => GetEnemyComponent();

    private void OnSelectionChange() => GetEnemyComponent();

    private void OnWizardUpdate() => GetEnemyComponent();
    
    private void OnWizardCreate() => ChangeEnemyType();

    private void OnWizardOtherButton()
    {
        ChangeEnemyType();
        GetEnemyComponent();
    }

    private void GetEnemyComponent()
    {
        if(Selection.activeTransform)
            _enemyType = Selection.activeTransform.GetComponent<IEnemyType>();
    }

    private void ChangeEnemyType()
    {
        _enemyType?.RemoveComponent();

        switch (selectedEnemyType)
        {
            case EnemyType.Goblin:
                EnemyTypes.AddGoblin(Selection.activeGameObject);
                break;
            case EnemyType.Skeleton:
                EnemyTypes.AddSkeleton(Selection.activeGameObject);
                break;
            default:
                throw new ArgumentOutOfRangeException();
        }
    }
}

Really simple and like that, we have restricted the additions of enemy types to game objects only through our tool.

Conclusion

Encapsulation of functionality is an important part of OOP. By creating nested private Monobehaviours, we can encapsulate the addition of components in game objects, for anyone that is using the Unity editor, so that we can control what happens whenever we want certain actions to occur before or after one of our scripts is added to a gameobject in the inspector.

As a runtime usage, we can have certain Monobehaviours being used as tags, when disabling the script component won’t be enough.

Thank you for reading and as always, if you have any questions or comments you can use the comments section or contact me directly via the contact form or by email. Also if you don’t want to miss any of the new blog posts, you can always subscribe to my newsletter or the RSS feed.


About Giannis Akritidis

Hi, I am Giannis Akritidis. Programmer and Unity developer.

Follow @meredoth
Follow me: