Can a MonoBehaviour be a private inner class?

I want to use a private inner class MonoBehaviour as part of the internal implementation of a class (also a MonoBehaviour). Reduced to the essentials:

public class OuterClass : MonoBehaviour {
  public void SomeMethod(GameObject go) {
    go.AddComponent<InnerClass>();
  }

  private class InnerClass : MonoBehaviour {
  }
}

This appears to be all working absolutely fine, with no errors or warnings. But it also appears to violate the statement in the Unity documentation) that:

Is this a problematic violation of that rule? If so, what are the problems? I am aware that it means the script is not visible in the Editor, but that’s exactly what I want: it should never be added by anything other than its enclosing class.

Does the inner class need to derive from MB for some reason?
I don’t know the workings/rules for putting an MB within another one, that’s interesting…

You could empirically test for the problems but remember that you have become a test pilot. This means that since the API docs don’t say what might happen, the behavior might change in the future without notice.

For instance, let’s say you test it and determine that Awake(), Start() and Update() all get called normally today.

There’s no guarantee API-wise it would continue to work under all future versions.

If you don’t NEED Monobehavior functions such as Update(), don’t base off MB.

1 Like

I want the inner class to receive OnEnable and OnDisable callbacks when its host GameObject is activated/deactivated, so I think it does need to be a MonoBehaviour.

The comment in the documentation here:

Means that you can’t add it via the inspector. Unity when serializing your scenes/prefabs lookup your scripts via a guid, that guid is stored inside a meta file along with the script file.

For example. I’ve create a scene with 2 GameObjects in it:
6975206--822587--upload_2021-3-25_18-22-40.png

The MyTestGameObject has a single script on it called zTest03:
6975206--822593--upload_2021-3-25_18-23-49.png

Here you can see that script has a meta file:
6975206--822596--upload_2021-3-25_18-24-36.png
The contents of the meta file is this:

fileFormatVersion: 2
guid: 7efa508250b1b1148b7fd26533435563
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:

Note that guid. That’s the unique identifier for this file.

Now when we go and look at the *.unity file for this scene we’ll find the GameObject in question:

%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!29 &1
####
.... SNIPPED A MAJOR PORTION OF YAML UNRELATED ...
####
GameObject:
  m_ObjectHideFlags: 0
  m_CorrespondingSourceObject: {fileID: 0}
  m_PrefabInstance: {fileID: 0}
  m_PrefabAsset: {fileID: 0}
  serializedVersion: 6
  m_Component:
  - component: {fileID: 2096099674}
  - component: {fileID: 2096099673}
  m_Layer: 0
  m_Name: MyTestGameObject
  m_TagString: Untagged
  m_Icon: {fileID: 0}
  m_NavMeshLayer: 0
  m_StaticEditorFlags: 0
  m_IsActive: 1
--- !u!114 &2096099673
MonoBehaviour:
  m_ObjectHideFlags: 0
  m_CorrespondingSourceObject: {fileID: 0}
  m_PrefabInstance: {fileID: 0}
  m_PrefabAsset: {fileID: 0}
  m_GameObject: {fileID: 2096099672}
  m_Enabled: 1
  m_EditorHideFlags: 0
  m_Script: {fileID: 11500000, guid: 7efa508250b1b1148b7fd26533435563, type: 3}
  m_Name:
  m_EditorClassIdentifier:
--- !u!4 &2096099674
Transform:
  m_ObjectHideFlags: 0
  m_CorrespondingSourceObject: {fileID: 0}
  m_PrefabInstance: {fileID: 0}
  m_PrefabAsset: {fileID: 0}
  m_GameObject: {fileID: 2096099672}
  m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
  m_LocalPosition: {x: -5.313465, y: 3.7368402, z: -2.9684873}
  m_LocalScale: {x: 1, y: 1, z: 1}
  m_Children: []
  m_Father: {fileID: 0}
  m_RootOrder: 1
  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}

In it you can see 3 things… The GameObject, a MonoBehaviour, and a Transform.

The MonoBehaviour is the serialized representation of our script.

And at this line:

m_Script: {fileID: 11500000, guid: 7efa508250b1b1148b7fd26533435563, type: 3}

You can see our previous guid:
7efa508250b1b1148b7fd26533435563

This is super helpful for many reaons:

  1. you can easily rename files since the guid doesn’t change. The rename just also renames the meta file.
  2. you can have 2 scripts with the same name (just in different namespaces so C# doesn’t freak out, and different folders so your FileSystem doesn’t freak out). This is helpful for if you import 3rd party libraries say from the asset store
  3. it just makes for easy/fast lookup by the editor/serialization engine
  4. more reasons I’m likely unaware of on the internal side of things

There is an exception to this rule though… and that is scripts found within dll’s. And this is actually where the ā€˜fileID’ gets used. If your script is inside a dll, the guid will point to the meta file associated with the dll. And the fileID will be a hash associated with the script located inside that dll. Unity has created code to deal with this specific edge case, and it’s fairly trivial since they can rely on the various .net/mono tools for managing dll’s to extract that info.

Where as doing so for a random *.cs file that happens to have 2 scripts written in it… not as simple to do. Doable… in theory sure. But why support it other than to allow people to create more difficult to manage code?

Note even the dll thing is annoying because if you happen to delete that dll (say to import a new version of it), and that meta file gets deleted. Well… you just lost reference to ALL the scripts in that dll and the editor won’t know how to find them. This even goes for if you rename the class itself within the dll… makes developing a dll super annoying. It’s why I no longer use them for my libs.

…

Mind you this doesn’t stop you from adding said nested MonoBehaviour at runtime via ā€œAddComponentā€.

For example:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class zTest03 : MonoBehaviour
{

    private void Update()
    {
        if(Input.GetKeyDown(KeyCode.Space))
        {
            this.gameObject.AddComponent<Blargh>();
        }
    }

    private class Blargh : MonoBehaviour
    {
        private void Start()
        {
            Debug.Log("Blargh: " + this.gameObject.GetComponents<MonoBehaviour>().Length);
        }
    }

}

This will work fine because the runtime doesn’t care. I’ve actually done this in games I’ve released as ways to attach little tags to gameobjects for various things.

The whole asset guid thing is a editor/inspector/serializer dealio. You can’t attach the script via the editor and have the build pipeline recognize it.

3 Likes

While technically your code will work.

Why not just define OnEnable/OnDisable (or whatever you’d like to call them) on your private nested class and then from OuterClass call those respective methods from its OnEnable/OnDisable methods?

1 Like

I’d choose this. If the outer class truly is ā€œthe bossā€ of the inner class helpers and they all live in the same adorable little cottage, er GameObject, why not have the boss be in charge of the little helpers. Lets you explicitly control order of operation too.

2 Likes

Ah - the InnerClass is not attached to the same GameObject as OuterClass: OuterClass attaches InnerClass to other GameObjects in order to receive a callback from InnerClass when those GameObjects are activated/deactivated.

Thanks for all the responses, really helpful. I’ve woken up this morning realising that it would be perfectly fine to make InnerClass a more general, standalone ā€˜callback on enable/disable’ component, that could be useful in all sorts of other contexts - so I’ll move away from the inner class design, and save needing to worry whether it’ll bite me in the future!

We do this all the time. It should be fine.

Usually we do it so the nested class can access private fields of the container class.

2 Likes

As well as create collapsible sections in the Inspector!