В этой публикации я постараюсь вкратце рассказать о типе перечисления в 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.

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


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


  1. red_perez
    20.06.2019 10:55

    Спасибо, в закладках, для таких чайников как я самое то.


  1. vizgl
    20.06.2019 23:28

    Открыл для себя Odin Inspector, больше нет надобности в таких drawer'ах


    1. YLahin Автор
      21.06.2019 11:22

      Плагин мощный, но:
      1) Для некоторых людей такое решение может показаться избыточным.
      2) Платный.


  1. svkozlov
    21.06.2019 10:53

    Спасибо, можно примеры использования?


    1. YLahin Автор
      21.06.2019 11:02

      Навскидку, флаги можно использовать в качестве тегов. Допустим есть игра в жанре 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().