PEGI (Player & Editor GUI) | Unity | Custom Inspectors

Unity's default inspectors are pretty good at showing you the state of your MonoBehaviours and ScriptableObjects. But to allow your scripts to be used by others it is often better to provide a user-friendly interface for your fellow co-workers... sure we can also write documentation for our classes and keep it updated as things get changed, but it's objectively more preferable to instead bash one's own brain out with a hammer. Unity lets you make your own editors. And as I was making them I would often create a wrapper function to make code shorter. Time went by and I kept adding/removing and changing those wrappers. Many custom inspectors later the PEGI class is now a big helper.


To demonstrate I'll compare the code I'd need to write before with the one I write now to achieve the same result. This will show us a slider that calls a Recalculate() when anything changed, and a button with an icon right next to it that reverts to a default value).


Without PEGI wrapper (Standard Unity GUILayout):

var controller = (MyMaterialController )target;
 
EditorGUILayout.BeginHorizontal();
EditorGUI.BeginChangeCheck();

controller .transparency = EditorGUILayout.IntField("Object's Transparency:", controller .transparency); 
 
if (EditorGUI.EndChangeCheck())
   controller .RecalculateShaderParameters();  
 
GUIContent cont = new GUIContent
   {
        tooltip = "Will load default value",
        image = refreshIcon
   };

if (controller .transparency != 0.5 && GUILayout.Button(cont,  GUILayout.MaxWidth(25), GUILayout.MaxHeight(25)))
    controller .transparency = 0.5;
 
EditorGUILayout.EndHorizontal();

With PEGI:

if ("Object's  Transparency:".edit(ref transparency))
 RecalculateShaderParameters(); 
if (transparency != 0.5f && icon.Refresh.Click("Will load default value",25).nl())
transparency = 0.5f;

The Inspect() function is inside of your class, that is why the target part is not needed. They can access private & static members, and game can build with them.

nl(); or pegi.nl(); means New Line. Before PEGI class, I would often want to hide parts of the inspector when they are not needed, and as a result have a BeginHorizontal without a matching "End", which would throw tons of warnings. Now, everything is in one line by default, and only when I call nl() the EndHorizontal is called.

To actually see the result of Inspect() function, use Unity's [CustomEditor()] Attribute, I have a wrapper for that too.

using PlayerAndEditorGUI;
#if UNITY_EDITOR
    using UnityEditor;
#endif

// YOUR CLASS

#if UNITY_EDITOR
  [CustomEditor(typeof(YourClass))]
  public class YourClassDrawer : PEGI_Inspector_Mono<YourClass> {}
#endif

Usually, I just do it for a couple of manager classes because you can Nested_Inspect() any class from the inspector of any other class. For example, pegi's edit_List will let you inspect all of its elements if that element has an Inspect() function (implements an IPEGI interface).


Custom List Inspector

"Image Datas:".edit_List(imageDatas);

* Lets you reorder elements (right image);

* Inspected element's class can have PEGI_inList function which will tell how it looks in list inspector (first image).

* And in this way I can inspect non-serialized classes which are not Unity Objects.


Another thing that has proven extremely useful is the iNeedAttention interface. Which when implemented will cause a list to show a warning sign when NeedAttention() returns a not empty string and the string is the message that will show when hovering over the warning icon.


I notify myself when reference is not set, list not filled up, basically, any mistake that can be detected automatically is highlighted in this way so I don't have to search the same mistake twice.


Example:

My Tools GitHub repository has plenty of examples. But here is just one more:

class YourCoolClass : PEGI 
{
 
  private int textureSize;
  Texture2D texture;
 
 public void Inspect()
 {
   "Resolution:".edit(ref textureSize).nl();
    if (textureSize <= 8)
     "Texture is too small".writeHint();
   else if (textureSize > 4096)
     "Texture is too big".writeHint();
  else if ("Create new texture".ClickConfirm("newTex","Are you sure?"))
   {
     texture = new Texture2D(textureSize,textureSize);
     Debug.Log("Texture was created");
   }
 }
}

The benefits:

* No reflection or serialization.

* It is just a wrapper on top of Unity native EditorGui so even if something changes dramatically, updating PEGI will be easy.

* It can be seamlessly added into an existing project (Something I actually did as part of my work). Also, it can be easily removed if anything)

* You are letting users control the private and static fields via inspector fields. I even used it to control Unity's render settings such as fog strength and sky colours, also the Main Camera's background colour, all from a single inspector.

* I don't need to worry about mismatching BeginHorizontal() / EndHorizontal() functions, which would often happen as I want to hide and show some buttons under different conditions. For example, I can hide the "Create new" button if the name for a new object is too short or is already included in the list.

* Every function returns true if the value was modified. Unity provides "BeginChangeCheck/EndChangeCheck" to see if values within the block were changed, but I found myself writing less code by having a function return true if the value was changed.

* Inspecting Non-Serialized classes, private, static stuff.

* And finally, I wanted to use the same code to draw class in the inspector, and in build (screenshot of what I'm talking about is somewhere on this page). You can see that the painter tool uses the same function to draw itself in-game and in the inspector. So basically as a bonus you get an inspector in your build - it allowed me to debug my projects very easily.

* In addition to scripts there is also a folder with icons, which I found myself reusing very often. To add your icons, drop them in the folder and add their name to the icon enum.


The PEGI scripts are at my public GitHub.


Maybe it's because I was using them so much that they seem easy to me, but using this class is a difference between doing a tedious job I never want to do again, and something so easy that it feels like a win every time I make an inspector and other people's life easier with it. And it's always a straightforward process, something I don't need to think too much about. Join the Discord if you want to try and use it. Seeing other people using it would help me improve its usability and intuitiveness.