Rules for what classes need to be defined in their own file?

This is a bit of a broad question, but I feel like I’ve definitely run into issues where a class (MonoBehaviour) was defined in a file that doesn’t match the filename, or when multiple ones were defined in a same file.

However, I feel like I’ve been following this as a kind of blind rule, and while googling I can’t find any definitive and explicit reference for how exactly Unity works with respect to class/file naming.

Some specific questions:

  • Is this only for Mono Behaviors? Can it be worked around?
  • Are Scriptable Objects also affected?
  • Are Serializable classes/structs that aren’t MonoBehaviour or ScriptableObject affected?
  • Any other types where this matters?

I want to clarify, I’m not generally asking about code style. I understand different people have different preferences and reasons for organizing things a certain way, but that’s a completely independent issue. Same goes for “code quality” and “maintainability”. Those are all important, but again subjective.

This question is aimed purely at “does it work” and “under what conditions” and “why?”.

I also wonder if there’s any official documentation that explains this in more detail, as often it feels like this information is gained by just playing around in Unity, trying to reproduce things and see what happens, and figure out the behavior from that, rather than just reading a comprehensive document.

The only actual rule here is that MonoBehaviour and ScriptableObject derived classes must live in a file whose name matches the class name. Beyond that, it’s the wild west. There are no other rules.

So the answers to your questions are:

  • No (also includes ScriptableObject) and sure I guess it can be worked around if you just never use the inspector (Use AddComponent only)
  • Yes
  • No
  • No

Personally, unless it’s a special case, I put every type inside its own file. This seems simple, maintainable, and obvious to my brain. It’s also the best way to avoid merge conflicts when collaborating with other developers.

1 Like

Thanks for replying, a bit unfortunate that ScriptableObjects are also affected. Personally I really like big files, coming from Rust and C and other languages I find it so much easier to just read a file with a bunch of small things rather than jump between 5 different files each 10 lines of class definition.

Well frankly this is a Unity restriction and not a C# restriction. When you think about it, it makes sense because you can do things like drag the script from the project window onto a GameObject and it will create an instance of the script on that object. This functionality would break if you could put multiple MonoBehaviours in a file. There’s also the Unity - Scripting API: MonoScript representation which would also break. These are not insurmountable problems but you can kind of see the logic in it.

1 Like

While it’s somewhat true that MonoBehaviours and ScriptableObject classes need to be in their own file with a matching file name, though this is only true when you actually need instances of that class to be serialized in the editor. This is only possible when the class has a direct reference to an asset. Each script file represents a TextAsset and is represented in the editor as a “MonoScript”. The asset ID (GUID) that this script file gets will be directly linked to the class with the matching name. However you could have as many MonoBehaviour components as you like inside a single script file. However you can not drag them onto a gameobject in the editor. You could only create them through AddComponent at runtime.

Many editor classes like EditorWindows or Editors are actually ScriptableObjects as well. Though they don’t get serialized in the scene or a prefab and therefore do not need an asset ID.

Note that externally compiled managed DLLs are somewhat an exception to the rule. When you import a pre-compiled DLL into your project, Unity will actually create MonoScript instances for each MonoBehaviour in that DLL as sub assets so they can actually be used in the editor. Though that means you have to compile that DLL externally and have to import / overwrite it every time you change something in a script in that DLL. So it’s not really more convenient ^^.

Also having separate files makes it easier to navigate and find certain classes when you’re not in an IDE or when you extensivly use interfaces or baseclasses. Because in that case you can not look up the runtime type as that may not be known during compile time. This makes debugging a lot harder as even when you know the actual type name, you would need to search for it in your project.

2 Likes

Just as an example. When you have a script like this:

// Holder.cs
using UnityEngine;

public class Holder : MonoBehaviour
{
    void Start()
    {
        var c = gameObject.AddComponent<MyHiddenComp>();
        c.text = "apples";
        c.count = 42;
    }
}

public class MyHiddenComp : MonoBehaviour
{
    public int count;
    public string text;
    void Start()
    {
        Debug.Log($"I'm here and I'm fine and I have {count} {text}");
    }
}

This actually works. You can attach the Holder script to a gameobject.

Though as I said, that MyHiddenComp can’t be added to a gameobject during edit time.

Since ScriptableObjects are essentially the same thing as a MonoBehaviour, just without a gameobject (at least from the native engine’s C++ core), a similar thing applies to them. So you can not store instances as assets, but you can create them at runtime.

2 Likes

Technically it can be added while in edit mode. It just won’t stay once the editor performs a scene reload.

Editor Code

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

public class MonoBehaviourA : MonoBehaviour
{
    [MenuItem("Test/Add")]
    private static void AddMe()
    {
        ObjectFactory.AddComponent<MonoBehaviourB>(Selection.activeGameObject);
    }
}

public class MonoBehaviourB : MonoBehaviour
{
    public int x;
}

Attached

9827346--1412946--upload_2024-5-10_15-30-51.png

Scene reload

9827346--1412949--upload_2024-5-10_15-32-53.png

It’ll serialize the data too if you save it before the scene reload.

Serialized Data

--- !u!114 &147030781
MonoBehaviour:
  m_ObjectHideFlags: 0
  m_CorrespondingSourceObject: {fileID: 0}
  m_PrefabInstance: {fileID: 0}
  m_PrefabAsset: {fileID: 0}
  m_GameObject: {fileID: 147030778}
  m_Enabled: 1
  m_EditorHideFlags: 0
  m_Script: {fileID: 0}
  m_Name:
  m_EditorClassIdentifier:
  x: 123
1 Like

I’m very glad you mention this because I kind of forgot to ask now realizing it should be a followup question.

  • If Editors/EditorWindows are ScriptableObjects, I assume this mainly means they get serialized through a Unity recompile cycle (allowing the instance to keep its field values), but since it doesn’t get an asset Id they can be defined wherever without any downside?
  • Is there ever any problem with adding more things into a file that has a MonoBehaviour/ScriptableObject? For example I’m using Odin to create some resource management windows, and I have a lot of relatively small ScriptableObjects that also need additional classes defined.

Specifically, say that I have ZombieAnimation : ScriptableObject, but I also want to create this from an OdinMenuTree, which is easy if I create something like

// not inheriting from anything
class CreateNewZombieAnimation {
  // ... odin stuff
}

These are often small and very tightly coupled to the ScriptableObject, so I assume it should be totally safe to define it in the same file, correct?

Right, the problem is this:

m_Script: {fileID: 0}

So it serializes the data but it can not recreate the instance from the serialized data since it’s missing the asset reference. A similar thing is true for generic MonoBehaviour classes. You can actually attach them through AddComponent without having a concrete derived class now. However it can not be serialized since there’s not asset it can reference. Maybe they will add the actual classname to the serialized data in the future. That would help to solve most issues. Though you would still need some kind of custom editor UI to handle that (or Unity will upgrade their own when we get there).

Well, yes. Normal serializable classes which may just be nested objects in a MonoBehaviour or a ScriptableObject can just be defined in the same script file without any issues. This is quite common. You could even make them nested classes when you think they are “really” that tightly coupled.

public class SomeScriptableObject : ScriptableObject
{
    public SomeData data;
  
    [System.Serializable]
    public class SomeData
    {
        public string blubb;
    }
}

So that SomeData class would be a nested class and when you want to use it outside that scriptable object, you would need to address it as

var someNewData = new SomeScriptableObject.SomeData();

So the outer class acts like a namespace. Though don’t do this if that nested class may actually be of use for some other class. This is usually only done when you group certain variables into separate classes that are only used inside that outer class. In normal OOP we would make those classes private, though this doesn’t play well with the serialization system in most cases.

3 Likes

I feel your pain, as a C / C++ oldskooler.

This next point doesn’t address your issue directly, but lies close by the problem space and may help you construct a more-readable project for yourself: look into partial classes.

It’s a horrible name, it should be instead “single classes defined in multiple different files.”

I love the partial keyword for organizing bigger classes, even though many object oriented purists disagree.

Here’s more of my mad scribblings about partial classes in Unity3D:

https://discussions.unity.com/t/842074/2

2 Likes

A custom script template would make this even better.

#ROOTNAMESPACEBEGIN#
public partial class #SCRIPTNAME# : UnityEngine.MonoBehaviour {}
#ROOTNAMESPACEEND#
2 Likes

Oh that’s amazing, thank you so much! I’ve only used partial classes in the context of WPF, so it hasn’t really occurred to me that this was something that would work, but I love that it does, and really appreciate your writeup in the other thread :slight_smile:

While it may not be a perfect solution, this is exactly the kind of flexibility I wish languages had more of.

1 Like