Go to Top

Some problems require thinking outside the box and pulling together a host of different techniques. In this case study we look at fixing the perennial problem of Missing MonoBehaviours.

Motivation

You should read this tutorial if you want to:

  • Understand how to fix "missing scripts" in Unity
  • See how debugging can diagnose a solution to a complex problem
  • Understand more about SerializedObjects and SerializedProperties
  • Learn more about the fine art of bodging

Introduction

You don't have to programming Unity for long before you encounter the "Missing MonoBehaviour" errors in the console and the sudden realisation you've lost the connection to some script because of an import or a project move - or just plain accident!  Even if you restore a copy of your script, the connection is not re-established.

When it's one or two object you can easily just reassign them, but what if it's a script that was on hundreds of items?  Complete nightmare.

I had this problem, and managed to find a solution.  I thought that the way it was found might prove interesting to others facing difficult challenges when it feels like the whole system is against you.

Solving The Problem

To solve this problem there are four steps:

  • Identify game objects and prefabs that have missing scripts
  • Find all of the currently available scripts
  • Match the data (the fields that still appear in the Inspector) with the fields of the available scripts
  • If there is a match, update the existing broken object to point to the new script

Identify GameObjects and prefabs that have missing scripts

Ok, so the first step isn't so hard - it's pretty easy to ask Unity to give you every game object whether it's in the current scene or in the project view.  I don't like the way that the traditional FindMissingScripts script means you have to select all of the objects, so we're going a different route.

To find all the objects we just need to use Resources.FindAllObjectsOfType - that does more than just find resources, it finds everything - including a bunch of Unity stuff.  We need to find all of the GameObjects and then find all of their components and see if any are null.

With the magic of Linq we can do that in a single line of code - but I've broken it into several to make it clearer:

brokenList =
	//Find all of the game objects
	Resources.FindObjectsOfTypeAll(typeof(GameObject))
		//FindObjectsOfTypeAll returns an Object[] 
		//so make it a GameObject enumeration
		.Cast<GameObject>()
		//Check each object
		.Where(
			//Get all of the components
			c=>c.GetComponents<Component>()
				//Check if any return null
				.Any(o=>o==null)
		)
		//Turn it into a list
		.ToList();

So after that we have a list of GameObjects that contain broken scripts, no matter if they are in the project or the scene.

Find all of the available scripts

Well this is easy too, using a very similar approach.  We just need to find all of the objects that are of the type MonoScript - we have one more requirement though here.  In the next step we are going to match up the missing scripts based on the properties that they are showing in the inspector.  That means we need to know the properties of all of the scripts we are scanning - so time for a little reflection, getting the public and private fields that have SerializeField set and storing them in a Dictionary for fast lookup.

	//All of the candidate scripts
	static List<ScannedScript> scripts;
	//Whether we have scanned for scripts
	static bool _initialized = false;

	//Holds a scanned script
	class ScannedScript
	{
		//All of the serialized properties
		public Dictionary<string, FieldInfo> properties;
		//The instance id
		public int id;
		//The script itself
		public MonoScript script;
	}

	//Initialise the scanned scripts
	void Initialize()
	{
		if(_initialized)
			return;
		_initialized = true;

		ScanAll();

	}

	void ScanAll()
	{
		//Get all of the scripts
		scripts = Resources.FindObjectsOfTypeAll(typeof(MonoScript))
			//Make this a collection of MonoScripts
			.Cast<MonoScript>()
			//Make sure that they aren't system scripts
			.Where(c=>c.hideFlags == 0)
			//Make sure that they are compiled and
			//can retrieve a class
			.Where(c=>c.GetClass() != null)
			//Create a scanned script for each one
			.Select(c=>new ScannedScript { id = c.GetInstanceID(), 
				script = c,
				//The properties need to be all public
				//and all private with [SerializeField] set
				properties = c.GetClass().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
					.Where(p=>p.IsPublic || (!p.IsPublic && p.IsDefined(typeof(SerializeField), false)))
					.ToDictionary(p=>p.Name)
			})
			.ToList();
	}

At the end of that we have a list of all the candidate scripts and each script has a dictionary of its serialized properties.  Techy, a bit advanced but still straightforward.

Match the data with the fields of the available scripts

And this is where the trouble starts.  If you recall - if we GetComponent on the GameObject with the missing script it returns null - so how on Earth do we actually get access to that list of properties when everything we call returns null?

The problem is further compounded by the fact that Unity is doing all of the script getting and editor allocation in the C++ back end - nowhere near our Editor classes and we have no access.

This had me stumped for a while (in truth this is where I started because I knew it would be difficult, but the story makes more sense in this order!)

Examine the facts:

  • GetComponent on a missing script returns null
  • There is no way to access the serialized data for an object without a reference to it
  • The Inspector shows a list of the properties even though it doesn't know what the script is

Clearly the clue must be in that last statement.  Something that is available to C# must have access to that data stream.  Further, that thing must be an Editor so that it is displayed in the inspector.

So what we need to do is inject ourselves into that part of the system, to become a thing that can inspect this missing object in the editor.  All we need to do is write a custom editor for MonoBehaviour itself!

When we do that, sure enough, we get the data stream passed to us in the inspector's serializedObject field.  Hooray!  All we need to do now is work out whether this is a MonoBehaviour without a script - we do that by inspecting the SerializedProperties of the object we got.

Next up we need to do some debugging - the question is "What's in that data stream?"

You work through the values in a SerializedObject by calling GetIterator() and the SerializedProperty that returns to step through them using either Next or NextVisisble.  These two calls both take a boolean parameter that says whether the data should be drilled into - on the first call it must be true for our purposes on subsequent calls it needs to be false.

var property = serializedObject.GetIterator():
var first = true;
while(property.NextVisible(first))
{
     first = false;
     Debug.Log(property.name);
}

By using Debug.Log on each member of the serialized data (that is visible) we learn that the script, currently containing null is in the m_Script variable, and lo and behold, the rest of the stream contains those public properties that the default inspector is drawing!  We've found a solution - we can access the data stream!

All we need to do now is run those variables past the candidate scripts and see if we can find just one where everything matches, if we can then we can set that as our m_Script!

//Make a copy of our script serialized property
//for later
var script = iterator.Copy();
//Get a copy of all of the scripts
var candidates = scripts.ToList();
//Step through the remaining properties
//while we have anything that might match
while(iterator.NextVisible(false) && candidates.Count>0)
{
	//Set candidates to the subset that contain
	//the current property
	candidates = candidates.Where(c=>c.properties.ContainsKey(iterator.name)).ToList();
}
//If we have only 1 candidate remaining
//then use it
if(candidates.Count==1)
{
	//Set the script reference
	script.objectReferenceValue = candidates[0].script;
	//Update the data stream
	serializedObject.ApplyModifiedProperties();
	serializedObject.UpdateIfDirtyOrScript();
}
//If we have multiple matches then give
//the user a choice
else if(candidates.Count > 0)
{
	foreach(var candidate in candidates)
	{
		if(GUILayout.Button("Use " + candidate.script.name))
		{
			//Configure the script
			script.objectReferenceValue = candidate.script;

			serializedObject.ApplyModifiedProperties();
			serializedObject.UpdateIfDirtyOrScript();
		}
	}
}
//Otherwise tell them we failed
else
{

	GUILayout.Label("> No suitable scripts were found");
}

Update the existing broken object to point to the new script

You can see this in the code above - we simply set the m_Script SerializeProperty's objectReferenceValue equal to our new script and then call ApplyModifiedProperties() on the parent SerializeObject.

The noble art of bodging

Ok, some of you might have noticed that the problem with all this is that it's just fixing one script at a time!  How can we work through all the scripts and fix all of them, rather than having to select them?

That'll be a bodge then.

  • The only place we have access to the actual data is in the Inspector
  • The inspector can only fix what has been selected in the Editor
So we are going to have to convince the Editor to select each of our missing objects, wait for it to actually run the code above and then move on to the next one.  That takes some dancing around using an Update function and a bunch of extra code in our inspector to tell it when the current target object has been considered by are earlier code!
	                if(GUILayout.Button("Fix Now", GUILayout.Width(80)))
			{
				FixMissingScripts.tried = false;
				EditorPrefs.SetBool("Fix", true);
				processList.AddRange(
					Resources.FindObjectsOfTypeAll(typeof(GameObject)).Cast<GameObject>().Where(c=>c.GetComponents<Component>().Any(o=>o==null))
					);
			}

When we click the fix button we move our broken scripts into a processList which is dealt with in the custom editor's Update function.  Note how we are passing a false to the static variable tried in the inspector code.

Then in Update we do this:

		if(!trying)
		{
			if(processList.Count > 0)
			{
				FixMissingScripts.tried = false;
				var first = processList[0];
				FixMissingScripts.tryThisObject = first;
				processList.RemoveAt(0);
				Selection.activeObject = first;
				if(processList.Count==0)
				{
					nextTime = 10;
				}
				Repaint();
				trying = true;

			}
		}
		if(trying && FixMissingScripts.tried)
			trying = false;

We check if we are already trying to fix something, if we aren't then we queue up the next one and tell the inspector to update its tried variable when the job is done.

If we are trying we just check to see if the process for the current script is complete and if it is then we move on to the next item.

Conclusion

It's a bodge, but it works.  Sometimes this kind of lateral thinking - in this case How on Earth do I get to that data stream? requires a bit of staring at the screen until, in this case, the Eureka moment of thinking If the default inspector can get it, how can I become the inspector? The rest is unfortunate bodging to make that initial work around perform well with 100s of scripts.

You can download the working version from here.  Open the fixing window using Window > Fix Missing Scripts.

, , ,

18 Responses to "Lateral Thinking – Fixing Missing Scripts"

  • CBirke
    November 16, 2012 - 10:19 am Reply

    Very well done.

  • Nick
    January 1, 2013 - 3:27 pm Reply

    Brilliant! I had this problem while using an svn, and I had a similar idea to you about how to fix it. Nice to see that you’ve already done it (and well done man, would have taken me a while), and thanks for giving it away! Life saver mate!

  • DaveA
    January 15, 2013 - 5:39 pm Reply

    Well done. An age-old problem. I too got as far as reflection and ‘I know the Editor knows this, but how to get at that data’ and moved on. Thanks for sticking with it.

  • Chris
    March 16, 2013 - 3:16 pm Reply

    Brilliant! This problem has plagued me for ages (another reason why unity is such a steaming pile of… I don’t know why they just don’t fix it?). Thanks!

  • farfpeuf
    March 28, 2013 - 4:31 pm Reply

    Very usefull, thank you, I modified it a little in order to have scripts appear in alphabetical order

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

    [ExecuteInEditMode]
    [CustomEditor(typeof(MonoBehaviour))]
    public class FixMissingScripts : Editor
    {
    public static bool tried;
    public static GameObject tryThisObject;

    public override void OnInspectorGUI ()
    {

    if(target.GetType()!=typeof(MonoBehaviour) && target.GetType() != typeof(UnityEngine.Object))
    {
    base.OnInspectorGUI();
    return;
    }
    EditorPrefs.SetBool(“Fix”, GUILayout.Toggle(EditorPrefs.GetBool(“Fix”, true), “Fix broken scripts”));
    if(!EditorPrefs.GetBool(“Fix”, true))
    {
    GUILayout.Label(“*** SCRIPT MISSING ***”);
    return;
    }
    Initialize();
    var iterator = this.serializedObject.GetIterator();
    var first = true;
    while(iterator.NextVisible(first))
    {
    first = false;
    if(iterator.name == “m_Script” && iterator.objectReferenceValue == null)
    {
    if(tryThisObject == (target as Component).gameObject)
    tried = true;
    var script = iterator.Copy();
    var candidates = scripts.ToList();
    while(iterator.NextVisible(false) && candidates.Count>0)
    {
    candidates = candidates.Where(c=>c.properties.ContainsKey(iterator.name)).ToList();
    }
    if(candidates.Count==1)
    {
    script.objectReferenceValue = candidates[0].script;

    serializedObject.ApplyModifiedProperties();
    serializedObject.UpdateIfDirtyOrScript();
    }
    else if(candidates.Count > 0)
    {
    ScriptComparer sc = new ScriptComparer();
    candidates.Sort(sc);
    foreach(var candidate in candidates)
    {
    if(GUILayout.Button(candidate.script.name))
    {
    script.objectReferenceValue = candidate.script;

    serializedObject.ApplyModifiedProperties();
    serializedObject.UpdateIfDirtyOrScript();
    }
    }
    }
    else
    {
    GUILayout.Label(“> No suitable scripts were found”);
    }
    break;
    }
    }
    base.OnInspectorGUI ();

    }

    class ScriptComparer: IComparer
    {
    public int Compare(ScannedScript sa, ScannedScript sb)
    {
    return StringComparer.OrdinalIgnoreCase.Compare(sa.script.name,sb.script.name);
    }
    }

    public class ScannedScript
    {
    public Dictionary properties;
    public int id;
    public MonoScript script;
    }
    static List scripts;
    static bool _initialized = false;
    bool _localInit;
    void Initialize()
    {
    if(!_localInit)
    {
    EditorApplication.projectWindowChanged += ()=>{
    Repaint();
    };
    _localInit = true;
    }
    if(_initialized)
    return;
    _initialized = true;

    ScanAll();

    }

    void ScanAll()
    {
    scripts = Resources.FindObjectsOfTypeAll(typeof(MonoScript))
    .Cast()
    .Where(c=>c.hideFlags == 0)
    .Where(c=>c.GetClass() != null)
    .Select(c=>new ScannedScript { id = c.GetInstanceID(),
    script = c,
    properties = c.GetClass().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
    .Where(p=>p.IsPublic || (!p.IsPublic && p.IsDefined(typeof(SerializeField), false)))
    .ToDictionary(p=>p.Name)
    })
    .ToList();
    }

    }

  • farfpeuf
    April 3, 2013 - 3:01 pm Reply

    I also modified the scanall function in order to find scripts within dlls (there seems to be a bug with FindObjectsOfTypeAll (typeof(MonoScript)) because not all returned objects are monoscripts so i also added a type test to prevent this bus)

    void ScanAll ()
    {
    scripts = new List ();

    UnityEngine.Object[] monoScripts = Resources.FindObjectsOfTypeAll (typeof(MonoScript));

    for (int i=0; i p.IsPublic || (!p.IsPublic && p.IsDefined (typeof(SerializeField), false)))
    .ToDictionary (p => p.Name)
    };

    scripts.Add (scannedScript);

    }
    }

    }

  • farfpeuf
    April 3, 2013 - 3:03 pm Reply

    void ScanAll ()
    {
    scripts = new List ();

    UnityEngine.Object[] monoScripts = Resources.FindObjectsOfTypeAll (typeof(MonoScript));

    for (int i=0; i p.IsPublic || (!p.IsPublic && p.IsDefined (typeof(SerializeField), false)))
    .ToDictionary (p => p.Name)
    };

    scripts.Add (scannedScript);

    }
    }

    }

    • metafa
      February 10, 2014 - 2:45 pm Reply

      Hi farfpeuf! Exactly I wanted to get it working when replacing DLLs with normal code (and the other way around).

      The line

      for (int i=0; i p.IsPublic || (!p.IsPublic && p.IsDefined (typeof(SerializeField), false)))

      obviously got cut by the webform validator. Can you still post this somewhere? I have no idea what to fill in the blanks with :)

  • farfpeuf
    April 3, 2013 - 3:04 pm Reply

    sorry ther is a bug when writting code in the ccomment field

  • oakus
    August 8, 2013 - 3:36 pm Reply

    Looks like your script can’t handle inheritance (ie. missing script inherits from MonoBehaviour through one or more classes) … Have you tested it?

    • whydoidoit
      August 9, 2013 - 9:01 am Reply

      Yes I do use it for scripts that inherit through base classes – most of mine do that.

  • simon
    August 26, 2013 - 7:25 am Reply

    What if all the monoscripts are missing

    • whydoidoit
      August 26, 2013 - 12:31 pm Reply

      I’m afraid it’s toast in that case. You have to have something that matches the serialized data.

  • Benoit FOULETIER
    September 17, 2013 - 11:57 am Reply

    Using the same trick, I found a way to get the SerializedObject representation of a missing/null script synchronously and from anywhere (no need to be inside an Editor class, no need to Repaint, etc):

    public static SerializedObject getSerializedComponent( GameObject _go, int _iComponent )
    {
    var prevSelection = Selection.objects;
    Selection.activeObject = _go;

    var tracker = ActiveEditorTracker.sharedTracker;
    tracker.RebuildIfNecessary();

    Selection.objects = prevSelection;

    var editor = tracker.activeEditors[_iComponent + 1]; // +1 because the first one is the GO editor itself
    return editor.serializedObject;
    }

    I think there could be gotchas, like if your inspector is locked, maybe it’d be better to use a separate tracker instead of the shared one, not tested all that much but it worked in my case…

  • Imi
    November 27, 2013 - 10:04 am Reply

    Unfortunately, this does not seem to work anymore (as of Unity 4.3).

    Seems like the Editor does not list the properties of missing components anymore via Next() or NextVisisble() (at least after an editor restart).

    They are still in the .scene file, so the next step would be an homemade yaml parser.

    (Or, unity just fix this stupid and unnecessary problem by themself and store and expose some meta information about missing components. DOH!)

  • Jason Brackman
    December 19, 2013 - 1:07 am Reply

    I’ve noticed on 4.2 & 4.3 that Missing Monobehavior components look different on your image above than on my machine.

    I get a Script variable that states “Missing (Mono Script)” and a warning tag that tells me that the script cannot be loaded and to fix compile errors and to assign a valid script.

    “other object” and “values” or anything else is no where to be found :) — have you done something to your scripts to ensure you have these vars hanging around even if the script goes missing? I”m probably missing something important here :)

  • TangoChen
    August 19, 2014 - 2:00 am Reply

    I love you, dude! YOU JUST SAVED ME!

Leave a Reply

%d bloggers like this: