Lifehacks Editor Unity 3D. Part 1: Attributes


Content


  • Part 0. List of GUI elements used in articles
  • Part 1. Attributes
  • Part 2. Windows
  • Part 3. Class editor, inheritor from MonoBehavior or ScriptableObject
  • Part 4. Class editor, custom editor for serializable variable

Foreword


Hello, friends! I noticed that many programmers miss the rich features of Unity customizing the editor interface for one reason or another. In this series of articles, I will write down some very simple examples that make life easier for game designers and artists, as well as a couple of more complicated examples, but also easy to implement.

Most of it is taken from the experience of use, where it came from the native documentation of the engine. You can easily find the information you need by looking at the Unity 3D documentation. Simply, from my own experience I will say that many programmers either do not have time or do not want to delve into mantras. Therefore, I am posting the most short guide on the main editorial capabilities that I used at work and in my projects.

Inline attributes


I will not paint all the attributes, I will only outline briefly those that I myself had to use.

Method Attributes


Unity Menu Item
- Scripting API: MenuItem
Screenshots


[MenuItem("Tools/Initialization Project")]

Allows you to create a menu to access the static method. Through “/” the hierarchy is indicated. You can place new buttons in the standard engine main menu, specifying a path, for example, “File / Create New Asset”.

In total, it can contain three parameters.
string path //полный путь в меню
bool valudate //является ли данный метода валидатором функции (делает пункт меню неактивным)
int order //порядок расположения элемента в рамках одной иерархии

[MenuItem("Tools/Initialization Project", true)]
public static bool ValidateInitialization()
{
    //просто проверка на то, что выделен любой объект
    return Selection.gameObjects.Length > 0;
}

[MenuItem("Tools/Initialization Project")]
public static void Initialization()
{
    //do something...
}

Also, if you use the elements of the main menu, the additional button will appear not only there, but also in the context menu on the right mouse button. For example, in my project, I added copying the path to the asset.

In addition, hotkeys can be assigned to methods. To do this, right in the path to the menu you need to write the necessary combination. To do this, use one of the service characters + letter.
% - ctrl on Windows or cmd on OSX
# - shift
& - alt
In my project, with copying the path to the asset, it looks like this
[MenuItem("Assets/Copy Path %&c")]
private static void CopyAssetPath()
{
}


Unity Context Menu Element
- Scripting API: ContextMenu
public class UnitController : MonoBehavior 
{
    [SerializeField]
    private new Transform transform = null; //храним собственный трансформ в отдельной переменной, во имя оптимизации
    //инициализируем переменную при помощи вызова через контекстное меню
    [ContextMenu("Initialization")]
    public void Initialization()
    {
        tranform = GetComponent();
    }
}


Attributes to Variables


Signature, tooltip, and clamper example


Limiting the input value
Unity - Scripting API: RangeAttribute
[Range(float min, float max)]

We can say that this is a custom editor for the attribute, which allows you to set the boundaries of the set value through the inspector. Not klampit in real time - only in the inspector. Useful if you set, for example, the probability of falling items from 0 to 1 or from 0 to 100.

Signature
Unity - Scripting API: HeaderAttribute
[Header(string title)]

Specifies the caption for the serializable field that appears in the inspector.

Indent
Unity - Scripting API: SpaceAttribute
[Space]

Sets the indent in the inspector. Unity

Tooltip
- Scripting API: TooltipAttribute
[Tooltip(string tip)]

Specifies a hint in the inspector when hovering over a serializable variable.

Serializing Unity Variables
- Scripting API: SerializeField
[SerializeField]

Allows you to serialize variables regardless of their scope. A very useful attribute that allows you to make all class variables private, but customizable in the inspector. Disabling Unity

Serialization
- Scripting API: NonSerializable
[NonSerializable]

Allows you to remove serialization from public variables. I do not recommend the data approach. It is better to define the get; set property; and receive data on it. In addition, the property can be made virtual and overloaded, if necessary, in the heir classes. And the fact that it is public allows you to use it in interfaces.

Hiding a variable in the Unity inspector
- Scripting API: HideInInspector
[HiddenInInspector]

Allows you to hide the serializable field in the inspector. It doesn’t matter if it is public or private / protected with the SerializeField attribute.

Class Attributes


Creating a child instance from ScriptableObject
Unity - Scripting API: CreateAssetMenuAttribute
[CreateAssetMenu(menuName = "Entity/Weapon")]

ScriptableObject is a very useful class that allows you to store a conditional database in a project without resorting to prefabs. The project creates objects with the type you created. You can work as well as with prefabs, they have their advantages and disadvantages. In general, this class is a topic for a separate article, small but informative.
The above attribute allows you to create an object of your type in the project along the path where you opened the context menu.

Execution in the Unity Editor
- Scripting API: ExecuteInEditMode
[ExecuteInEditMode]

Allows the script to work in the editor. It is mainly useful for post-effects, because it allows you to immediately evaluate the result in the camera without starting the project. But sometimes it can be used for other purposes.

For example, as an initializer of serializable fields of built-in types, such as transform, renderer, rectTranform, etc. I would not recommend it everywhere, it is better to require manual initialization, or write an editorial script, but sometimes it is convenient.

The Need for Another Unity Component
- Scripting API: RequireComponent
[RequireComponent(System.Type type)]

Forces the editor to require the presence of a specific component on the same object on which the script with this attribute hangs. When added, it immediately creates a component of the specified type on the same object. Also prohibits deleting an already added component.

New item in the Unity component add menu
- Scripting API: AddComponentMenu
[AddComponentMenu(string path)]

Adds a submenu to the drop-down list in the menu Components → ... and AddComponent. It is convenient if you have a large code library and need to organize it in the editor.

On this, the simple part ends and add quite a bit to the extent difficult.

Custom Attributes (CustomPropertyDrawer)


Unity - Scripting API: PropertyAttribute
Unity - Scripting API: PropertyDrawer
If the attributes above are not enough for you, you can always use the API to write your own custom attributes. The implementation of this tool is also quite simple and consists of several steps. In this example, I will describe creating
my own attribute for a variable.

First, you need to define an inheritor class from the standard PropertyAttribute class. I will immediately create it with a constructor in which the input parameter will be the path to the list of what we need to use in the attribute.

public class IntAttribute : PropertyAttribute
{
    private string path = “”;
    public IntAttribute(string path)
    {
        this.path = path;
    }
}

Secondly, after that we create an editor script in which we will draw this very new class. You need to inherit it from PropertyDrawer, and also write the CustomPropertyDrawer attribute to it.

[CustomPropertyDrawer(typeof(IntAttribute ))]
public class IntAttributeDrawer : PropertyDrawer
{
}

I call classes the most common names, just to show the principle of using custom.

The base is ready, now we need to draw this attribute in the form in which we need it. Basically, I use attributes in cases where enum is not enough, but I need to draw a drop-down list with a choice.

For example, you have an effect base that has an id → effect match. You store this base somewhere, it doesn't matter in ScriptableObject'e or on some prefab. Here is the simplest implementation of the “repository”

Note - always create the first serializable field in the classes string. Because of this, in the lists, the elements will not be referred to as element 1, element 2 .., but in the way you assign the variable in the inspector.

The code


For classes with which I interact “externally”, I always write an interface. Everyone has their own approach to this point, but this approach will easily allow, in which case, to replace the class only in one place to another, and the rest will work with the interface. Moreover, Unity supports working with interfaces in methods such as GetComponent (s) ..., GetComponent (s) InChildren, etc.

Interface and effect class

public interface IResource
{
    int ID
    {
        get;
    }
    string Name
    {
        get;
    }
}
[System.Serializable]
public class Effect : IResource
{
    [SerializeField]
    private string name = “”;
    [SerializeField]
    private int      id = 0;
    public int ID
    {
        get
        {
            return id;
        }
    }
    public string Name
    {
        get
        {
            return name;
        }
    }
}

Interface and container class

public interface IContainer
{
    IResource[] Resources
    {
        get;
    }
}
public abstract class ResourcesContainer : MonoBehaviour, IContainer
{
    public virtual IResource[] Resources
    {
        get 
        {
            return null;
        }
    }
}
public class EffectsContainer : ResourcesContainer 
{
    [SerializeField]
    private Effect[] effects = null;
    public override IResource[] Resources
    {
        get 
        {
            return effects;
        }
    }
}

Usually, I place objects with such data in resources, then I take from there. You can arrange it simply in the project and where it is necessary to determine the links. But I am going along a simpler and already tested path on more than one platform.

Editor It
remains to add the editor:

[CustomPropertyDrawer(typeof(IntAttribute ))]
public class IntAttributeDrawer : PropertyDrawer
{
    protected string[]  values = null;
    protected List idents = null;
    protected virtual void Init(SerializedProperty property)
    {
        if (attribute != null)
        {
            IntAttribute intAttribute = (IntAttribute)attribute;
            //можно ввести проверки на null, но, я думаю, вы сами справитесь
            IResource[] resources = Resources.Load(intAttribute.Path).Resources;
            values = new string[resources.Length + 1];
            idents = new List(resources.Length + 1);
            //добавляем нулевой элемент для назначения -1 значения
            values[0] = “-1: None”;
            idents.Add(-1);
            for (int i = 0; i < resources.Length; i++)
            {
                values[i+1] = resources[i].ID + “: ” + resources[i].Path;
                idents.Add(resources[i].ID);
            }
        }
    }
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        if (property == null)
        {
            return;
        }
        Init(property);
        EditorGUI.BeginProperty(position, label, property);
        // Draw label
        position = EditorGUI.PrefixLabel(position, GUIUtility.GetControlID(FocusType.Passive), label);
        // Don't make child fields be indented
        int indent = EditorGUI.indentLevel;
        EditorGUI.indentLevel = 0;
        // Calculate rects
        Rect pathRect = new Rect(position.x, position.y, position.width - 6, position.height);
        int intValue = property.intValue;
        intValue = idents[EditorGUI.Popup(pathRect, Mathf.Max(0, idents.IndexOf(intValue)), Values)];
        property.intValue = intValue;
        EditorGUI.indentLevel = indent;
        EditorGUI.EndProperty();
    }
}
Располагаем префаб или ScriptableObject по нужному нам пути (я расположил в Resources/Effects/Container). 
Теперь в любом классе объявляем целочисленную переменную и атрибут к ней с путем до префаба.
public class Bullet : MonoBehavior
{
    [SerializeField]
    [IntAttribute(“Effects/Container”)]
    private int effectId = -1;    
}

Attribute screenshot


Conclusion


All the above “life hacks” can simplify not only your work (especially when the project is developed several months or years), but also the work of beginners, as well as artists and game designers. Not every specialist will get into the code. Of course, a good organization and discipline can help to document each component as such, but this does not always work out, especially for independent developers.

PS: Later, I will write a couple of articles on other types of editor upgrades, which will include:

CustomEditor;
CustomPropertyDrawer;
EditorWindow;
The Debug class and how it is eaten;
Gizmos class.

I will also supplement the examples with a window and a custom editor. Write in comments whether similar articles are necessary or it is possible to manage what is already on Habré.

Only registered users can participate in the survey. Please come in.

A survey to improve the quality of the submitted material, what is missing from the article?

  • 64.6% More Images with Examples 53
  • 21.9% Detailed comments in code 18
  • 20.7% Number of links to the documentation 17
  • 43.9% Overall, all is well 36
  • 3.6% Other (in comments) 3

Also popular now: