Search Unity

  1. Unity 6 Preview is now available. To find out what's new, have a look at our Unity 6 Preview blog post.
    Dismiss Notice
  2. Unity is excited to announce that we will be collaborating with TheXPlace for a summer game jam from June 13 - June 19. Learn more.
    Dismiss Notice
  3. Dismiss Notice

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

Discussion in 'Scripting' started by darthdeus, May 10, 2024.

  1. darthdeus

    darthdeus

    Joined:
    Oct 13, 2013
    Posts:
    94
    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.
     
  2. PraetorBlue

    PraetorBlue

    Joined:
    Dec 13, 2012
    Posts:
    7,944
    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.
     
    orionsyndrome likes this.
  3. darthdeus

    darthdeus

    Joined:
    Oct 13, 2013
    Posts:
    94
    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.
     
  4. PraetorBlue

    PraetorBlue

    Joined:
    Dec 13, 2012
    Posts:
    7,944
    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 https://docs.unity3d.com/ScriptReference/MonoScript.html representation which would also break. These are not insurmountable problems but you can kind of see the logic in it.
     
    Owen-Reynolds likes this.
  5. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    4,179
    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.
     
    orionsyndrome and Ryiah like this.
  6. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    4,179
    Just as an example. When you have a script like this:

    Code (CSharp):
    1. // Holder.cs
    2. using UnityEngine;
    3.  
    4. public class Holder : MonoBehaviour
    5. {
    6.     void Start()
    7.     {
    8.         var c = gameObject.AddComponent<MyHiddenComp>();
    9.         c.text = "apples";
    10.         c.count = 42;
    11.     }
    12. }
    13.  
    14. public class MyHiddenComp : MonoBehaviour
    15. {
    16.     public int count;
    17.     public string text;
    18.     void Start()
    19.     {
    20.         Debug.Log($"I'm here and I'm fine and I have {count} {text}");
    21.     }
    22. }
    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.
     
    orionsyndrome and Ryiah like this.
  7. Ryiah

    Ryiah

    Joined:
    Oct 11, 2012
    Posts:
    21,664
    Technically it can be added while in edit mode. It just won't stay once the editor performs a scene reload.

    Code (csharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using UnityEditor;
    5.  
    6. public class MonoBehaviourA : MonoBehaviour
    7. {
    8.     [MenuItem("Test/Add")]
    9.     private static void AddMe()
    10.     {
    11.         ObjectFactory.AddComponent<MonoBehaviourB>(Selection.activeGameObject);
    12.     }
    13. }
    14.  
    15. public class MonoBehaviourB : MonoBehaviour
    16. {
    17.     public int x;
    18. }

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

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

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

    Code (csharp):
    1. --- !u!114 &147030781
    2. MonoBehaviour:
    3.   m_ObjectHideFlags: 0
    4.   m_CorrespondingSourceObject: {fileID: 0}
    5.   m_PrefabInstance: {fileID: 0}
    6.   m_PrefabAsset: {fileID: 0}
    7.   m_GameObject: {fileID: 147030778}
    8.   m_Enabled: 1
    9.   m_EditorHideFlags: 0
    10.   m_Script: {fileID: 0}
    11.   m_Name:
    12.   m_EditorClassIdentifier:
    13.   x: 123
     
    Last edited: May 10, 2024
    darthdeus likes this.
  8. darthdeus

    darthdeus

    Joined:
    Oct 13, 2013
    Posts:
    94
    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
    Code (CSharp):
    1. // not inheriting from anything
    2. class CreateNewZombieAnimation {
    3.   // ... odin stuff
    4. }
    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?
     
  9. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    4,179
    Right, the problem is this:
    Code (CSharp):
    1. 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.

    Code (CSharp):
    1. public class SomeScriptableObject : ScriptableObject
    2. {
    3.     public SomeData data;
    4.  
    5.     [System.Serializable]
    6.     public class SomeData
    7.     {
    8.         public string blubb;
    9.     }
    10. }
    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

    Code (CSharp):
    1. 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.
     
    orionsyndrome, darthdeus and Ryiah like this.
  10. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    39,311
    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://forum.unity.com/threads/partial-class-and-unity.1114747/#post-7170964
     
    darthdeus and Ryiah like this.
  11. Ryiah

    Ryiah

    Joined:
    Oct 11, 2012
    Posts:
    21,664
    A custom script template would make this even better.
    Code (csharp):
    1. #ROOTNAMESPACEBEGIN#
    2. public partial class #SCRIPTNAME# : UnityEngine.MonoBehaviour {}
    3. #ROOTNAMESPACEEND#
    https://support.unity.com/hc/en-us/articles/210223733-How-to-customize-Unity-script-templates
     
    darthdeus and Kurt-Dekker like this.
  12. darthdeus

    darthdeus

    Joined:
    Oct 13, 2013
    Posts:
    94
    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 :)

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