📜 ⬆️ ⬇️

Life hacking editor Unity 3D. Part 1: Attributes


Content



Foreword


Hello, friends! I noticed that many programmers skip the rich possibilities for Unity to customize the editorial interface for one reason or another. In this series of articles, I will write out a few very simple examples that allow game designers and artists to make life easier, as well as a couple of more complicated examples, but also easily implemented.

The most part 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 flipping the Unity 3D documentation. I simply say from my own experience that many programmers either have no time or no desire to delve into mantras. Therefore, I am spreading the briefest guide on the main editorial features that I used at work and in my projects.

Inline Attributes


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

Attributes to methods


Menu item
Unity - Scripting API: MenuItem
Screenshots


[MenuItem("Tools/Initialization Project")] 

Allows you to create a menu to access the static method. The “/” indicates the hierarchy. You can place new buttons in the standard main menu of the engine, indicating the path, for example, “File / Create New Asset”.
')
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 main menu items, then an additional button will appear not only there, but also in the context menu of the right mouse button. For example, in my project, I added copying the path to an asset.

In addition, you can assign hot keys to methods. For this, right on the way 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 an asset, it looks like this
 [MenuItem("Assets/Copy Path %&c")] private static void CopyAssetPath() { } 


Context menu item
Unity - Scripting API: ContextMenu
 public class UnitController : MonoBehavior { [SerializeField] private new Transform transform = null; //     ,    //        [ContextMenu("Initialization")] public void Initialization() { tranform = GetComponent<Transform>(); } } 


Attributes to variables


Sample Signature, Tips, and Clamper


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

You can say, this is a custom editor for an attribute that allows you to set the boundaries of the specified value through the inspector. Not klampit in realtime - only in the inspector. Useful if you specify, for example, the probability of falling out of objects from 0 to 1 or from 0 to 100.

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

Specifies the caption over the field to be serialized, which is displayed in the inspector.

Indent
Unity - Scripting API: SpaceAttribute
 [Space] 

Sets the indent in the inspector.

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

Sets a hint to the inspector when hovering over a variable to be serialized.

Variable serialization
Unity - Scripting API: SerializeField
 [SerializeField] 

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

Disable serialization
Unity - Scripting API: NonSerializable
 [NonSerializable] 

Allows you to remove the serialization of public variables. I highly recommend the data approach. It is better to define the property get; set; and get data on it. In addition, the property can be made virtual and reloaded, if necessary, in the classes of heirs. And the fact that it is public allows it to be used in interfaces.

Hiding a variable in the inspector
Unity - 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 also work as with prefabs, 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 with your type in the project, along the path where you opened the context menu.

Execution in the editor
Unity - 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 you can use for other purposes.

For example, as an initializer for 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 to write an editorial script, but sometimes it is convenient.

The need for another component
Unity - Scripting API: RequireComponent
 [RequireComponent(System.Type type)] 

Causes 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 components of the specified type on the same object. Also prohibits deleting an already added component.

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

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

At this, the simple part ends and add quite a bit of moderately complex.

Custom Attributes (CustomPropertyDrawer)


Unity - Scripting API: PropertyAttribute
Unity - Scripting API: PropertyDrawer
If the attributes listed 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 the creation
own attribute to a variable.

First, you need to define a descendant 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 newest 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 in order to simply show how to use customizable ones.

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 enumeration capabilities are not enough, but you need to draw a drop-down list with a choice.

For example, you have an effects database that has an id → effect match. You store this base somewhere, no matter in a ScriptableObject or on some kind of prefab. Here is the simplest implementation of the “repository”

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

Code


For classes that I interact with “from the outside,” I always write the interface. Everyone has his own approach to this moment, but this approach will easily allow, in which case, to replace the class only in one place with another, and the rest will work with the interface. Moreover, the unit supports working with interfaces in such methods 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 take them from there. You can locate and just in the project and where you need to define links. But I am going along a simpler path that has already been tested 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<int> idents = null; protected virtual void Init(SerializedProperty property) { if (attribute != null) { IntAttribute intAttribute = (IntAttribute)attribute; //    null, ,  ,    IResource[] resources = Resources.Load<IContainer>(intAttribute.Path).Resources; values = new string[resources.Length + 1]; idents = new List<int>(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; } 

Screenshot with attribute


Conclusion


All the above lifehacks can simplify not only your work (especially when the project is developed for 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, good organization and discipline can help and document each component in this way, but this is not always the case, especially with independent developers.

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

CustomEditor;
CustomPropertyDrawer;
EditorWindow;
Class debug and how to eat it;
Class Gizmos.

And also I will add examples with a window and the user editor. Write in the comments if you need such articles or you can get by with what is already on Habré.

Source: https://habr.com/ru/post/331042/


All Articles