Упрощаем рисование Enum Flags в Unity



В этой публикации я постараюсь вкратце рассказать о типе перечисления в C#, применении его в качестве флагов, а так же о том, как можно упростить их рисование в инспекторе Unity.

Что такое Enum?


Перечисления являются значимым типом в языке C#, состоящим из набора констант. Для его объявления используется ключевое слово enum. Каждый перечислитель имеет целочисленное значение. Первый по умолчанию 0, а последующие увеличиваются на 1.

enum Color
{
    Red, // 0
    Green, // 1
    Blue // 2
}

Для переопределения значений можно воспользоваться инициализаторами.

enum Color
{
    Red = 1,
    Green = 2,
    Blue = 3
}

Каждый тип перечисления имеет базовый тип, в роли которого может выступать любой целочисленный тип кроме char (по умолчанию используется int). Его также можно указать явно.

enum Color : byte
{
    Red = 1,
    Green = 2,
    Blue = 3
}

Флаги


Порой возникает необходимость наделить сущность рядом свойств. Можно объявить несколько полей или завести список, но иногда достаточно одного перечисления. Для использования перечисления в качестве флагов следует добавить специальный атрибут System.FlagsAttribute. При этом требуется явная инициализация значений, каждое из которых возводится в степень.

[System.FlagsAttribute]
enum Color : byte
{
    None = 0,
    Red = 1, // 2 ^ 0
    Green = 2, // 2 ^ 1
    Blue = 4 // 2 ^ 2
}

С помощью побитовой операции OR можно объединять элементы перечисления, а используя метод HasFlag(Enum) проверять наличие битовых полей в экземпляре.

var color = Color.Red | Color.Green | Color.Blue;
var hasFlag = color.HasFlag(Color.Red | Color.Green); // True

С помощью побитовой операции AND можно также осуществлять проверки.

var aColor = Color.Red | Color.Green;
var bColor = Color.Green | Color.Blue;

// Проверка наличия битовых полей
var contains = (aColor & bColor) == bColor; // False

// Проверка пересечения битовых полей
var overlaps = (aColor & bColor) != 0; // True

Перечисления в Unity


Для примера возьмём нижеприведённый код.

using UnityEngine;

public enum Color
{
    Red,
    Green,
    Blue
}

public class Example : MonoBehaviour
{
    public Color Color;
}

Встроенные средства Unity позволяют отображать перечисления в виде выпадающего списка.



К сожалению, редактор не умеет автоматически рисовать перечисления в виде флагов. Для этих целей требуется переопределение инспектора, что далеко не всегда удобно. Но можно пойти на хитрость и переопределить рисование перечислений глобально. Для начала модифицируем пример.

using System;
using UnityEngine;

[Flags]
public enum Color
{
    Red = 1,
    Green = 2,
    Blue = 4
}

public class Example : MonoBehaviour
{
    public Color Color;
}

Далее нужно реализовать свой PropertyDrawer. Если сериализуемое свойство имеет атрибут Flags, то для рисования будем использовать метод EditorGUI.MaskField, а в противном случае стандартный метод EditorGUI.PropertyField. Также следует учесть, что свойство может являться элементом массива. Приведённый ниже код следует поместить в папку с именем Editor.

[CustomPropertyDrawer(typeof(Enum), true)]
public sealed class EnumPropertyDrawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        using (new EditorGUI.PropertyScope(position, label, property))
        {
            if (HasEnumFlagsAttribute())
            {
                var intValue = EditorGUI.MaskField(position, label, property.intValue, property.enumDisplayNames);

                if (property.intValue != intValue)
                {
                    property.intValue = intValue;
                }
            }
            else
            {
                EditorGUI.PropertyField(position, property, label);
            }
        }

        bool HasEnumFlagsAttribute()
        {
            var fieldType = fieldInfo.FieldType;

            if (fieldType.IsArray)
            {
                var elementType = fieldType.GetElementType();

                return elementType.IsDefined(typeof(FlagsAttribute), false);
            }

            return fieldType.IsDefined(typeof(FlagsAttribute), false);

        }
    }
}

Теперь поле корректно отображается в инспекторе для любого Enum типа.



В дополнение к определённым значениям перечисления редактор добавляет ещё два:

  • Nothing — имеет целочисленное значение 0;
  • Everything — имеет целочисленное значение -1.

Ссылки по теме


  • +11
  • 2,5k
  • 5
Поделиться публикацией

Комментарии 5

    0
    Спасибо, в закладках, для таких чайников как я самое то.
      0
      Открыл для себя Odin Inspector, больше нет надобности в таких drawer'ах
        0
        Плагин мощный, но:
        1) Для некоторых людей такое решение может показаться избыточным.
        2) Платный.
        0
        Спасибо, можно примеры использования?
          0
          Навскидку, флаги можно использовать в качестве тегов. Допустим есть игра в жанре Match3. Элементы на поле могут иметь различные свойства: возможность уничтожения, объединения с другими фишками, способность к перемещению и т.д.

          [Flags]
          public enum Tag
          {
              Breakable = 1,
              Mergeable = 2,
              Movable = 4,
          }
          
          public sealed class Chip : MonoBehaviour
          {
              [SerializeField]
              private Tag _tag;
          
              public bool HasTag(Tag tag) => _tag.HasFlag(tag);
          }
          


          Без флагов подобный функционал будет выглядеть чуть более объёмным и для внесения изменений потребуется больше телодвижений.

          public sealed class Chip : MonoBehaviour
          {
              [SerializeField]
              private bool _breakable;
          
              [SerializeField]
              private bool _mergeable;
          
              [SerializeField]
              private bool _movable;
          
              public bool IsBreakable => _breakable;
          
              public bool IsMergeable => _mergeable;
          
              public bool IsMovable => _movable;
          }
          


          Ещё флаги можно использовать для оптимизаций, используя их вместо проверки существования компонента на объекте через GetComponent().

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое