Critical instability with Prefabs, VectorImage, and MaterialUIScaler

+1 vote
asked Mar 3, 2017 in Critical bug by cymatic-labs (170 points)

I'm using Unity 5.5.1f1 and I'm running into a really bad bug that prevents you from making prefabs out of MaterialUI components.

When MaterialUI components are turned into prefabs some sort of instability/bug occurs that prevents you from editing any values on the prefab instance in the scene. Trying to enable/disable the prefab or any of its children in the inspector has no effect. The active state of any game object will always remain whatever state it was when the prefab was created. Editing any inspector values will be of no use as they will automatically reset back to their prefab values immediately.

This also leads to instability in the editor where it prevents you from renaming any instance in the scene, including instances that are not prefabs. Clicking an object to rename it or using the right-click context menu Rename command do nothing. This seems like a pretty bad instability that is affecting the unity Editor itself in unexpected ways.

To reproduce the bug.

  1. Start a new Unity project.
  2. Import MaterialUI
  3. In a new scene, create a UI/Canvas
  4. On the newly created Canvas add MaterialUI/MaterialUIScaler script
  5. On the Canvas add MateriulUI/VectorImage
  6. Turn the Canvas game object into a prefab.

Once you have followed these steps try interacting with the prefab in the inspector. You will not be able to change any values. Also try renaming any game object in the scene - it will not work.

I've narrowed the bug down to some sort of interaction between VectorImage, MaterialUIScaler, and turning it into a prefab. Those are the minimum components needed to cause this issue. If you do not have a VectorImage or do not have a MaterialUIScaler on the Canvas, you do not see this issue when creating a prefab.

This is pretty lame as it prevents the ability to create MaterialUI prefabs; that's a pretty big deal. Now I have to maintain separate copies of my MaterialUI designs in each scene that contains the same UI.

commented Mar 3, 2017 by cymatic-labs (170 points)

I've narrowed this down further. It seems the offending call is VectorImage.cs line 353:

EditorUtility.SetDirty(this);

This line doesn't offend when the UI components are not inside of a prefab, but this breaks the UI/Editor when this code appears from within a prefab.

Furthermore I've narrowed it down to the fact that in all of your components you are initializing the EditorUtility.cs class and attaching your editor update delegate in the public constructors of your components:

#if UNITY_EDITOR
public VectorImage()
{
    EditorUpdate.Init();
    EditorUpdate.onEditorUpdate += OnEditorUpdate;
}
#endif

Unity recommends never using the constructors of Unity components like MonoBehaviours, etc. because of the way Unity handles serialization. You are making extensive use of constructors for initialization purposes:

http://answers.unity3d.com/questions/32413/using-constructors-in-unity-c.html

https://blogs.unity3d.com/2016/06/06/serialization-monobehaviour-constructors-and-unity-5-4/

MonoBehaviour constructors are called before the game/editor is necessarily ready. That's why you must call initialization in Awake() instead. When used in prefabs combined with how Unity does serialization/deserialization some kind of instability is occurring with the way the delegates are getting hooked up in conjunction with the EditorUtility.SetDirty() call from within the constructors.

You might want to say this is a bug in Unity, but clearly they've told us never to use constructors for initialization and to always use Awake() and Start().

Out of curiosity I changed the constructors of the two scripts to:

/// <summary>
/// Initializes a new instance of the <see cref="MaterialUIScaler"/> class.
/// </summary>
protected override void Awake()
{
    base.Awake();
    EditorUpdate.Init();
    EditorUpdate.onEditorUpdate += CheckScaleFactor;
}

and

// VectorImage.cs
protected override void Awake()
{
    base.Awake();    
    EditorUpdate.Init();
    EditorUpdate.onEditorUpdate += OnEditorUpdate;
}

... and it resolved the issue I was experiencing with prefabs.

By looking at the MaterialUI scripts though it's quite clear that a lot of the UI components are initializing things in their constructors.

Please refactor all of your constructor initialization code to use the override of Awake() instead and release a patch update for MaterialUI. This is the correct Unity best practice and it will allow prefabs of MaterialUI components to be created.

Just so you know I love MaterialUI, it's probably one of the best Unity UI packages out-of-the-box. I have experienced some bad corruption/instability when using it, but I'm going to keep trying to work with it. If you could fix this issue (and hey I've already debugged it for you!) that would be amazing.

Thanks!

commented Mar 3, 2017 by cymatic-labs (170 points)
Also, I've run into instability with MaterialUI where things will break, like the dialogs will stop working with calls to DialogManager.ShowAlert() - they just stop appearing. Or a button on the app bar that toggles the nav drawer will just stop working between plays in the Unity editor.

If I take my canvas that has the MaterialUI components on it, copy it, delete it from the scene, and then paste it from the clipboard things will work again for a while. And then they will stop and I will have to repeat the process of copy, delete, paste. It's maddening.

I wouldn't doubt it if weird instability like this was also being caused by the use of initialization in constructors.

2 Answers

0 votes
answered Mar 3, 2017 by cymatic-labs (170 points)

OK, I've gone through all script files in MaterialUI as best as I could and looked for instances where you were using the constructor incorrectly for initialization. Here is the list of files I found:

  • MaterialUIScaler.cs
  • MaterialButton.cs
  • MaterialSlider.cs
  • VectorImage.cs
  • MaterialInputField.cs
  • MaterialSlider.cs

Please convert the constructors in each to this code:

protected override void Awake()
{
    base.Awake();
    EditorUpdate.Init();
    EditorUpdate.onEditorUpdate += OnEditorUpdate;
}

That should hopefully fix the instability issues.

commented Mar 6, 2017 by admin (31,720 points)
Hi cymatic-labs,

Thanks very much for bringing this to my attention, it looks to be a bit of an oversight on my part while I was trying to make everything work as well in the editor as in play mode.

I'm not 100% certain, but I believe the reason I avoided using Awake for some of those classes was because it isn't called when scripts are recompiled (eg. when you change+save a script and go back to the editor), and I needed a way to initialize some things regardless of whether it was starting play mode, edit mode, or just a recompilation/deserialization.

I'm a tad busy right now, but I'll definitely look into fixing this as soon as I can. Thanks again for the help :)

~ Declan.
0 votes
answered Mar 7, 2017 by admin (31,720 points)
Hi cymatic-labs,

I've just finished modifying the behaviour of some of the components so they don't need EditorUpdate at all, thus removing the need for anything to happen in the constructor, which should solve the issues you've been experiencing. These changes will be present with the next update, which we will be pushing out as soon as we can.

Thanks again for your help with this :)

~ Declan.
commented Mar 7, 2017 by cymatic-labs (170 points)
Awesome! Thanks again for looking into this and making a new update for it. I was going to say that if you were utilizing the constructor for something specific (which it sounds like maybe you were, and is usually the case) then maybe Unity would have an official answer as to how to achieve what you needed without using the constructor (maybe they've run into the same use case before). But it sounds like you found a new resolution. Fantastic. I appreciate the effort on this.
Welcome to MaterialUI support! Ask us anything :)
...