Customizable Navigation Bar

Friday, October 4, 2013

Exploring Unity With Reflection And The Assembly Browser

Reflection is a very powerful tool in programming. It allows you to access classes, methods and variables that you wouldn't normally have access to, and gives you an unlimited amount of control over your projects, and the libraries that you use.

 In Unity, there are a large number of classes and methods that are private or internal. Most APIs and tools do the same, and for most people, this is perfectly fine. Not everyone is going to need an extremely specific function that can only be found in an internal class, and only a few people are going to need access to specific internal classes while creating editor tools. Personally, I like to explore, see what things are capable of, and find hidden gems that are throughout the libraries that I am using.

If you are developing with MonoDevelop, one of the extremely nice features is comes with is the assembly browser. In the assembly browser you can look through all of the dlls that your project is referencing, and find out which classes do what, and how exactly they do them. Taking some time to explore through the assembly browser can help give you a better understanding of how Unity does its thing, and how you can approach certain situations. Another option you can take is finding a .Net decompiler, but I'm pretty sure that is against the EULA.

When looking through the assembly browser normally, the only thing you are going to see is the public classes that you already have access to. You can look through all of their functions, and even see a few protected variables and functions that you might not have known about. For the most part those are just helper functions that don't have much of a purpose for the average user.

There is also another option for view available. At the top of the assembly browser you will see a visibility property, and it will default at Only Public Members. If you set it to All Members, suddenly you will see a massive list of new classes appear that you never would have known about beforehand. You can take a look at all of the inspectors for classes like Transforms and Cameras. You can take a look at the Asset Store Window class, or take a look at how all of the animation components work. Just about everything is there for you. The only thing you don't have access to is the C++ code that some of the functions make a reference to.

There are quite a few internal classes to look through. It will take quite a few hours to go through it all.


One of the useful finds I made while looking through the assembly browser was the GameView class. This is the class that represents the game view, or the view you see when you hit play within the editor. While I was working on LGUI, I came across the issue of not having the correct aspect ratio while positioning the 2D objects within the editor. The Screen class was very unreliable, and half of the time I would get a very random aspect ratio that didn't match up at all, or I would get a value of something like 0. 

When I discovered this GameView class, I noticed a function that would become extremely handy to me, called GetSizeOfMainGameView. As you can tell from the name it returns the size of the main game view, which was exactly what I needed when sizing my 2D GUI. Since this was an internal method, reflection came to the rescue. This is what I ended up with:


    public static Vector2 getMainGameViewSize ()
    {
        //The first thing I needed to do was get the GameView type. Since this was an internal type, I needed to make a special call using Type.GetType, which passes the class and the assembly that the class belongs to. The GameView class is within the UnityEditor assembly
        Type gameViewType = Type.GetType ("UnityEditor.GameView,UnityEditor");

        //Next I needed to get the MethodInfo of the GetSizeOfMainGameView function. MethodInfo holds all of the information about a function, including parameters, return types, and attributes. Also since GetSizeOfMainGameView is a static internal function, I needed to pass the NonPublic and Static binding flags so that the code knew where to look
        MethodInfo getSizeOfGameView = gameViewType.GetMethod ("GetSizeOfMainGameView", BindingFlags.NonPublic | BindingFlags.Static);

       //Now that I had the MethodInfo I wanted, I could invoke the function. The Invoke call has two parameters, the first is for the object that the function is being called on, and the second is for the parameters. Since this was a static function, I pass null for the first parameter, and since the function has no parameters, I pass null again for the second parameter
        return (Vector2)getSizeOfGameView.Invoke (null, null);
    }


Now if that function is used in an editor class, it will return a Vector2 that has the width and height of the main game view window. Pretty handy!

Another option that is available with reflection is adding to a pre-existing inspector. The example I am going to show is the Transform inspector. The inspector works the exact same as the original one, but now you can add things and remove things from it, and customize it however you like! Most of this was copied and pasted from the assembly browser, but there were a few instances where I needed reflection to accomplish what the old editor did. 

First create a folder in your project called Editor. Within that folder, create a new C# file called NewTransformInspector, and add this code to it.


using System;
using UnityEngine;
using System.Reflection;
using UnityEditor;

[CustomEditor (typeof(Transform)), CanEditMultipleObjects ]
internal class NewTransformInspector : Editor
{
    private Vector3 m_EulerAngles;
    private Quaternion m_OldQuaternion = new Quaternion (1234f, 1000f, 4321f, -1000f);
    private SerializedProperty m_Position;
    private SerializedProperty m_Scale;
    private SerializedProperty m_Rotation;

    //We need to grab some MethodInfo. There were a few calls that were internal
    MethodInfo tempContentInfo;
    MethodInfo sendScaleMethodInfo;

    public void OnEnable ()
    {
        //There is a few references to a temporary GUIContent object that are not visible to the public, so we get the method for that here. This function is internal and static, so we use the NonPublic and Static binding flags
        tempContentInfo = typeof(EditorGUIUtility).GetMethod ("TextContent", BindingFlags.NonPublic | BindingFlags.Static);

        //There is also an internal call to scale and modify the transform, so we grab that here. This function is used on a specific Transform, so we need to use the Instance binding flag.
        sendScaleMethodInfo = typeof(Transform).GetMethod ("SendTransformChangedScale", BindingFlags.NonPublic | BindingFlags.Instance);
        //Here we just grab the position and scale properties of this transform
        this.m_Position = base.serializedObject.FindProperty ("m_LocalPosition");
        this.m_Scale = base.serializedObject.FindProperty ("m_LocalScale");
    }

    public override void OnInspectorGUI ()
    {
        Transform transform = base.target as Transform;
        base.serializedObject.Update ();
        this.Inspector3D ();
        Vector3 position = transform.position;

        //Floats only have a certain amount of precision, so we throw a warning in here if the number gets too big
        if (Mathf.Abs (position.x) > 100000f || Mathf.Abs (position.y) > 100000f || Mathf.Abs (position.z) > 100000f) {
            EditorGUILayout.HelpBox ("Due to floating-point precision limitations, it is recommended to bring the world coordinates of the GameObject within a smaller range.", MessageType.Warning);
        }
        EditorGUI.showMixedValue = false;
        base.serializedObject.ApplyModifiedProperties ();
    }

    private void Inspector3D ()
    {
        Transform transform = base.target as Transform;
        GUILayout.Space (3f);
        //This uses the call to the temporary GUIContent object. Basically the function takes a string and returns a GUIContent object with that string.
        EditorGUILayout.PropertyField (this.m_Position, (GUIContent)tempContentInfo.Invoke (null, new object[]{"BRAND NEW POSITION" }), new GUILayoutOption[0]);
        Quaternion localRotation = transform.localRotation;
        if (this.m_OldQuaternion.x != localRotation.x || this.m_OldQuaternion.y != localRotation.y || this.m_OldQuaternion.z != localRotation.z || this.m_OldQuaternion.w != localRotation.w) {
            this.m_EulerAngles = transform.localEulerAngles;
            this.m_OldQuaternion = localRotation;
        }
        bool flag = false;
        UnityEngine.Object[] targets = base.targets;
        for (int i = 0; i < targets.Length; i++) {
            Transform transform2 = (Transform)targets [i];
            flag |= (transform2.localEulerAngles != this.m_EulerAngles);
        }
        EditorGUI.showMixedValue = flag;
        EditorGUI.BeginChangeCheck ();
        this.m_EulerAngles = EditorGUILayout.Vector3Field ("BRAND NEW ROTATION", this.m_EulerAngles, new GUILayoutOption[0]);
        if (EditorGUI.EndChangeCheck ()) {
            Undo.RegisterUndo (base.targets, "Inspector");
            UnityEngine.Object[] targets2 = base.targets;
            for (int j = 0; j < targets2.Length; j++) {
                Transform transform3 = (Transform)targets2 [j];
                transform3.localEulerAngles = this.m_EulerAngles;
                if (transform3.parent != null) {
                    //Here is our other reflection call. Basically we check to see if something changed in the transform and if it did we tell the engine to make the change
                    sendScaleMethodInfo.Invoke (transform3, null);
                }
            }
        base.serializedObject.SetIsDifferentCacheDirty ();
        this.m_OldQuaternion = localRotation;
        }
    EditorGUILayout.PropertyField (this.m_Scale, (GUIContent)tempContentInfo.Invoke (null, new object[]{"BRAND NEW SCALE" }), new GUILayoutOption[0]);
    GUILayout.Space (1f);
}


And that is your new transform inspector! Now when you select an object you will see the regular transform, but with new property labels, and since you have the inspector right in your project you can now add to it, and customize the functionality of it to fit your needs.


Well, that's all for this week. Hopefully you guys learned something about reflection, or at least gained some interest in it, and what it is capable of. Hope you guys enjoyed it, until next week!