Home > Blockchain >  How to apply an attribute to all classes who inherit monobehaviour in a project/ an assembly?
How to apply an attribute to all classes who inherit monobehaviour in a project/ an assembly?

Time:02-11

I'm asking this because I want to eliminate the "Failed to insert item" warnings in a Unity project, which is caused by amount of scripts exceeding a certain value. Here is a solution provided by unity, which suggests applying [AddComponentMenu("")]attribute to all monobehaviours accordingly.

Since I'm working on a project with lots of scripts, it seems very difficult to apply this attribute manually. I need to:

  • Finding all classes under \Assets\Scripts (or in Assembly-CSharp) who inherit monobehaviour or any of its child class
  • Applying [AddComponentMenu("PATH")] attibute to the classes found above and put the realtive path of the script in the "PATH"

I don't know how to implement these and have been failing to find a solution by myself.

Some additional features would be wonderful (not necessary):

  • If there is existing [AddComponentMenu("")] in the script, do not override the original one.
  • Automatically applying [AddComponentMenu("")] to the scripts to be created.

Looking forward to all kinds of help. Thanks a lot!

CodePudding user response:

There are probably many ways to do this outside of Unity. However, I will assume you want to do it all in Unity itself just for the simpleness of things.

Remark: There are for sure edge-cases left but I hope this is a good starting point


Finding all classes under \Assets\Scripts (or in Assembly-CSharp) who inherit monobehaviour or any of its child class

This first point would be quite simple - kind of. The Assets don't allow to find script types themselves but you can get all script assets by searching for t:MonoScript in the search bar in the Assets view.

The same can be done also via script using AssetDatabase.FindAssets like e.g.

var scriptGUIDs = AssetDataBase.FindAssets($"t:{nameof(MonoScript)}",  new[] {"Assets/Scripts"});

From there you will need to load the script asset and check its type using e.g.

foreach (var scriptGUID  in scriptGUIDs)
{
    // e.g. Assets/Scripts/MyComponent.cs
    var scriptAssetPath = AssetDatabase.GUIDToAssetPath(scriptGUID);
        
    // first get the actual asset
    var scriptAsset = AssetDatabase.LoadAssetAtPath<MonoScript>(scriptAssetPath);
    // get the system type
    var scriptType = scriptAsset.GetClass();
    // Now first of all check if this is actually something derived from MonoBehaviour -> if not skip 
    if(!scriptType.IsSubclassOf(typeof(MonoBehaviour))) continue;


    .....
}

Applying [AddComponentMenu("PATH")] attribute to the classes found above and put the realtive path of the script in the "PATH"

This is of course way more tricky since it requires to find the according code line (right above the class implementation) and add text into the file.

So this requires multiple steps

  • Check if this type already has the attribute -> skip, which also solves

    If there is existing [AddComponentMenu("")] in the script, do not override the original one.

  • Load the raw text content
  • find the line of code which contains public class TYPENAME :
  • Insert your attribute line before that line
  • Write all lines back to the file
  • After all done refresh the data base to cause a reload/recompilation

This could look somewhat like e.g.

// Find all assets of type "MonoScript" in the folder Assets/Scripts
// or remove /Scripts if you really want to go for all in the Assets
var scriptGUIDs = AssetDatabase.FindAssets($"t:{nameof(MonoScript)}",  new[] {"Assets/Scripts"});

foreach (var scriptGUID  in scriptGUIDs)
{
    // e.g. Assets/Scripts/MyComponent.cs
    var scriptAssetPath = AssetDatabase.GUIDToAssetPath(scriptGUID);
        
    // first get the actual asset
    var scriptAsset = AssetDatabase.LoadAssetAtPath<MonoScript>(scriptAssetPath);
    // get the system type
    var scriptType = scriptAsset.GetClass();
    // Now first of all check if this is actually something derived from MonoBehaviour -> if not skip 
    if(!scriptType.IsSubclassOf(typeof(MonoBehaviour))) continue;
    // check if this class already has the attribute -> if so skip
    if(scriptType.IsDefined(typeof(AddComponentMenu), true)) continue;

    // otherwise load in the lines as a list so we can insert our new line
    var lines = File.ReadAllLines(scriptAssetPath).ToList();
    // find the one defining the class
    var indexOfClassLine = lines.FindIndex(l => l.Contains($" class {scriptType.Name} : "));

    // e.g. Assets/Scripts/MyComponent
    // => you might want to e.g. also remove the "Assets" part ;)
    var relativePathWithoutExtension = scriptAssetPath.Substring(0, scriptAssetPath.Length - 3);
        
    // insert the new attribute one line right above that line
    lines.Insert(indexOfClassLine, $"[AddComponentMenu(\"{relativePathWithoutExtension}\")]");
        
    File.WriteAllLines(scriptAssetPath, lines);
}
    
AssetDatabase.Refresh();

And finally

Automatically applying [AddComponentMenu("")] to the scripts to be created.

Somewhere in you project you need an implementation of AssetModificationProcessor which will basically do the same thing as before, just this time it is slightly easier since we don't need to check the type since all c# scrips created via the menu are MonoBehaviour by default

public class ScriptKeywordProcessor : UnityEditor.AssetModificationProcessor
{
    public static void OnWillCreateAsset(string path)
    {
        // ignore meta files
        var tempPath = path.Replace(".meta", "");
        var index = tempPath.LastIndexOf(".");
        if (index < 0) return;

        // ignore if not a .cs script
        var file = tempPath.Substring(index);
        if (file != ".cs") return;
        
        // otherwise load in the lines as a list so we can insert our new line
        var lines = File.ReadAllLines(path).ToList();
        // find the one defining the class
        var indexOfClassLine = lines.FindIndex(l => l.Contains($" class {Path.GetFileNameWithoutExtension(path)} : "));

        // e.g. Assets/Scripts/MyComponent
        // => you might want to e.g. also remove the "Assets" part ;)
        var relativePathWithoutExtension = path.Substring(0, path.Length - 3);
    
        // insert the new attribute one line right above that line
        lines.Insert(indexOfClassLine, $"[AddComponentMenu(\"{relativePathWithoutExtension}\")]");
    
        File.WriteAllLines(path, lines);

        AssetDatabase.Refresh();
    }
}
  • Related