3
\$\begingroup\$

I recall in a certain code, there were named constants with exponential values. Two constants' values could be added two include both of them in one go.

Example:

{ STR = 1, AGI = 2, INT = 4} From this, combining the enumerations would always give a unique number, which lets us identify "3" as "STR&AGI" and 5 as "STR&INT".

How I would implement this is somewhat confusing.

\$\endgroup\$
-1
\$\begingroup\$

There's a blog post by Alan Zucconi about this argument:

http://www.alanzucconi.com/2015/07/26/enum-flags-and-bitwise-operators/

You'll get all the answers you need about this topic there.

\$\endgroup\$
  • 5
    \$\begingroup\$ Link only answers are not well received. If that link dies and goes 404, your answer does not contain the necessary information to answer the question. \$\endgroup\$ – Draco18s no longer trusts SE Feb 24 '18 at 19:01
9
\$\begingroup\$

This is called a Flags enumeration.

You mark it with the System.Flags attribute, and give it a backing type that's some kind of integer:

[System.Flags]
public enum Characteristics : int {
   None    = 0x00,
   Heavy   = 0x01,
   Hot     = 0x02,
   Shiny   = 0x04,
   Squishy = 0x08,
   Loud    = 0x10,
   Large   = 0x20
}

You can then stack multiple flags in a single variable with an OR operator:

var myCharacteristics = Characteristics.Loud | Characteristics.Squishy;

or stack on more later with myCharacteristics |= Characteristics.Shiny;

You can check for the presence of a particular flag with an AND operator:

if((myCharacteristics & Characteristics.Loud) != Characteristics.None)
    Debug.Log("I am loud");

or for any of a group of flags

var conspicuous = Characteristics.Shiny | Characteristics.Large | Characteristics.Loud;

if((myCharacteristics & conspicuous) != Characteristics.None)
    Debug.Log("I am conspicuous");

To edit this properly in the Unity inspector, you can define a custom PropertDrawer like the one shown here (modified below to work with the new EnumFlagsField instead of EnumMaskField) and mark your variable with an attribute:

[EnumFlags]
public Characteristics myCharacteristics;

Image of Unity Inspector showing multi-select of flags in an enum

Here are the scripts to make this work:

EnumFlagsAttribute - place in your Assets folder.

using UnityEngine;

public class EnumFlagsAttribute : PropertyAttribute {
    public string enumName;

    public EnumFlagsAttribute() { }

    public EnumFlagsAttribute(string name) {
        enumName = name;
    }
}

EnumFlagsDrawer - place in an Assets sub-folder called "Editor" so it's not compiled into the game:

using System;
using System.Reflection;
using UnityEditor;
using UnityEngine;

[CustomPropertyDrawer(typeof(EnumFlagsAttribute))]
public class EnumFlagsDrawer : PropertyDrawer {
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
        EnumFlagsAttribute flagSettings = (EnumFlagsAttribute)attribute;
        Enum targetEnum = GetBaseProperty<Enum>(property);

        string propName = flagSettings.enumName;
        if (string.IsNullOrEmpty(propName))
            propName = property.name;

        EditorGUI.BeginProperty(position, label, property);
        var enumNew = EditorGUI.EnumFlagsField(position, propName, targetEnum);
        property.intValue = (int)Convert.ChangeType(enumNew, targetEnum.GetType());
        EditorGUI.EndProperty();
    }

    static T GetBaseProperty<T>(SerializedProperty prop) {
        // Separate the steps it takes to get to this property
        string[] separatedPaths = prop.propertyPath.Split('.');

        // Go down to the root of this serialized property
        System.Object reflectionTarget = prop.serializedObject.targetObject as object;
        // Walk down the path to get the target object
        foreach (var path in separatedPaths) {
            FieldInfo fieldInfo = reflectionTarget.GetType().GetField(path);
            reflectionTarget = fieldInfo.GetValue(reflectionTarget);
        }
        return (T)reflectionTarget;
    }
}

Edit: based on a link from the article Galandil shared, here's a different way to draw the flags in the inspector. I souped up the linked version to support different enum backing types, multi-object editing, and multiple rows in a single enum. It will also skip over "None" or compound flags. Still experimental, but it seems to work.

Flags displayed as toggle buttons

using UnityEditor;
using UnityEngine;
using System.Collections.Generic;

[CustomPropertyDrawer(typeof(EnumFlagsAttribute))]
public class EnumFlagsAttributeDrawer : PropertyDrawer {


    [System.Flags]
    enum TriBool {
        Unset = 0,
        False =  1,
        True = 2,
        Both = 3
    }

    struct Entry {
        public string label;
        public int mask;
        public TriBool currentValue;
    }

    List<SerializedProperty> _properties;
    List<Entry> _entries;
    int _rowCount;
    int _columnCount;

    public override float GetPropertyHeight(SerializedProperty property, GUIContent label) {
        if (_properties == null)
            Initialize(property);
        return _rowCount * EditorGUIUtility.singleLineHeight;
    }

    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
        label = EditorGUI.BeginProperty(position, label, property);

        Rect labelRect = new Rect(
            position.x, 
            position.y, 
            EditorGUIUtility.labelWidth, 
            position.height);
        EditorGUI.LabelField(labelRect, label);

        float buttonStride = (position.width - labelRect.width) / _columnCount;
        float buttonWidth = buttonStride * 1.0f;
        Rect buttonRect = new Rect(
            labelRect.max.x, 
            labelRect.y, 
            buttonWidth,
            EditorGUIUtility.singleLineHeight);

        int column = 0;

        GUIStyle mixedButton = new GUIStyle(GUI.skin.button);
        mixedButton.normal.textColor = Color.grey;

        for(int i = 0; i < _entries.Count; i++) {
            //Debug.Log(_entries[i].label + " : " + _entries[i].currentValue);
            var entry = _entries[i];

            EditorGUI.BeginChangeCheck();
            bool pressed = GUI.Toggle(
                buttonRect, 
                entry.currentValue == TriBool.True, 
                entry.label, 
                entry.currentValue == TriBool.Both ? mixedButton : GUI.skin.button);

            if (EditorGUI.EndChangeCheck()) {

                if (pressed) {
                    foreach (var prop in _properties)
                        prop.intValue |= entry.mask;
                    entry.currentValue = TriBool.True;
                } else {
                    foreach (var prop in _properties)
                        prop.intValue &= ~entry.mask;
                    entry.currentValue = TriBool.False;
                }
                _entries[i] = entry;
            }

            buttonRect.x += buttonStride;
            if(++column >= _columnCount) {
                column = 0;
                buttonRect.x = labelRect.max.x;
                buttonRect.y += buttonRect.height;
            }
        }

        foreach (var prop in _properties)
            prop.serializedObject.ApplyModifiedProperties();

        EditorGUI.EndProperty();

        EditorGUI.showMixedValue = false;
    }

    void Initialize(SerializedProperty property) {
        var allTargetObjects = property.serializedObject.targetObjects;
        _properties = new List<SerializedProperty>(allTargetObjects.Length);
        foreach (var targetObject in allTargetObjects) {
            SerializedObject iteratedObject = new SerializedObject(targetObject);
            SerializedProperty iteratedProperty = iteratedObject.FindProperty(property.propertyPath);
            if (iteratedProperty != null)
                _properties.Add(iteratedProperty);
        }

        var parentType = property.serializedObject.targetObject.GetType();
        var fieldInfo = parentType.GetField(property.propertyPath);
        var enumType = fieldInfo.FieldType;
        var trueNames = System.Enum.GetNames(enumType);

        var typedValues = GetTypedValues(property, enumType);
        var display = property.enumDisplayNames;
        var names = property.enumNames;

        _entries = new List<Entry>();

        for (int i = 0; i < names.Length; i++) {
            int sortedIndex = System.Array.IndexOf(trueNames, names[i]);
            int value = typedValues[sortedIndex];
            int bitCount = 0;  

            for (int temp = value; (temp != 0 && bitCount <= 1); temp >>= 1)
                bitCount += temp & 1;

            //Debug.Log(names[i] + ": " + value + " ~ " + bitCount);

            if (bitCount != 1)
                continue;

            TriBool consensus = TriBool.Unset;
            foreach (var prop in _properties) {
                if ((prop.intValue & value) == 0)
                    consensus |= TriBool.False;
                else
                    consensus |= TriBool.True;
            }

            _entries.Add(new Entry { label = display[i], mask = value, currentValue = consensus });
        }

        _rowCount = Mathf.CeilToInt(_entries.Count / 4f);
        _columnCount = Mathf.Min(_entries.Count, (Mathf.CeilToInt(_entries.Count / 3f) == _rowCount) ? 3 : 4);
    }

    int[] GetTypedValues(SerializedProperty property, System.Type enumType) {
        var values = System.Enum.GetValues(enumType);
        var underlying = System.Enum.GetUnderlyingType(enumType);

        if (underlying == typeof(int))
            return ConvertFrom<int>(values);
        else if (underlying == typeof(uint))
            return ConvertFrom<uint>(values);
        else if (underlying == typeof(short))
            return ConvertFrom<short>(values);
        else if (underlying == typeof(ushort))
            return ConvertFrom<ushort>(values);
        else if (underlying == typeof(sbyte))
            return ConvertFrom<sbyte>(values);
        else if (underlying == typeof(byte))
            return ConvertFrom<byte>(values);
        else
            throw new System.InvalidCastException("Cannot use enum backing types other than byte, sbyte, ushort, short, uint, or int.");
    }

    int[] ConvertFrom<T>(System.Array untyped) where T : System.IConvertible {
        var typedValues = new int[untyped.Length];

        for (int i = 0; i < typedValues.Length; i++)
            typedValues[i] = System.Convert.ToInt32((T)untyped.GetValue(i));

        return typedValues;
    }
}
\$\endgroup\$
  • 1
    \$\begingroup\$ When I declare flag Enums I usually use bit shifting rather than hexadecimal as the assignment to make it more obvious what the values are and how they fit together, eg Heavy = 1 << 0, Hot = 1 << 1 etc. \$\endgroup\$ – Ed Marty Feb 25 '18 at 23:18

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.