Skip to content

Instantly share code, notes, and snippets.

@aarthificial
Last active July 3, 2024 22:17
Show Gist options
  • Save aarthificial/f2dbb58e4dbafd0a93713a380b9612af to your computer and use it in GitHub Desktop.
Save aarthificial/f2dbb58e4dbafd0a93713a380b9612af to your computer and use it in GitHub Desktop.
using System;
using UnityEngine;
[Serializable]
/// Requires Unity 2020.1+
public struct Optional<T>
{
[SerializeField] private bool enabled;
[SerializeField] private T value;
public bool Enabled => enabled;
public T Value => value;
public Optional(T initialValue)
{
enabled = true;
value = initialValue;
}
}
using UnityEditor;
using UnityEngine;
namespace Editor
{
[CustomPropertyDrawer(typeof(Optional<>))]
public class OptionalPropertyDrawer : PropertyDrawer
{
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
var valueProperty = property.FindPropertyRelative("value");
return EditorGUI.GetPropertyHeight(valueProperty);
}
public override void OnGUI(
Rect position,
SerializedProperty property,
GUIContent label
)
{
var valueProperty = property.FindPropertyRelative("value");
var enabledProperty = property.FindPropertyRelative("enabled");
EditorGUI.BeginProperty(position, label, property);
position.width -= 24;
EditorGUI.BeginDisabledGroup(!enabledProperty.boolValue);
EditorGUI.PropertyField(position, valueProperty, label, true);
EditorGUI.EndDisabledGroup();
int indent = EditorGUI.indentLevel;
EditorGUI.indentLevel = 0;
position.x += position.width + 24;
position.width = position.height = EditorGUI.GetPropertyHeight(enabledProperty);
position.x -= position.width;
EditorGUI.PropertyField(position, enabledProperty, GUIContent.none);
EditorGUI.indentLevel = indent;
EditorGUI.EndProperty();
}
}
}
@daniel-skalik
Copy link

Nice script, but for everyone whose wondering why it isn't working for them, this works only from version of Unity 2020.1.0a18. Maybe you could added as comment.

@Sayama3
Copy link

Sayama3 commented May 20, 2021

Nice script, but for everyone whose wondering why it isn't working for them, this works only from version of Unity 2020.1.0a18. Maybe you could added as comment.

That's nice, thanks you, but I'm really curious to know why it won't work bellow this version, if someone know and can reply, me and my curiosity will be grateful !

@aarthificial
Copy link
Author

That's nice, thanks you, but I'm really curious to know why it won't work bellow this version, if someone know and can reply, me and my curiosity will be grateful !

Before 2020.1, serializing fields of generic types was not possible.

@Sayama3
Copy link

Sayama3 commented May 20, 2021

Before 2020.1, serializing fields of generic types was not possible.

Oww, that's why, thanks you very much !

@NOVIO60229
Copy link

very useful!

@jmhoubre
Copy link

Very clever, and helpful. Thank you !

@longtran2904
Copy link

There a problem with your code. When you want an optional field A inside a custom field B and have a field B in your class then the drawer will calculate the width relative to B rather than to A. I'm not familiar with the property drawer so can someone fix it?

class B
{
     public Optional<float> a;
}

class C : MonoBehaviour
{
     public B b; // The enabled toggle of b.a will not display correctly.
}

@aarthificial
Copy link
Author

There a problem with your code. When you want an optional field A inside a custom field B and have a field B in your class then the drawer will calculate the width relative to B rather than to A. I'm not familiar with the property drawer so can someone fix it?

Should work now

@longtran2904
Copy link

Thanks, man!

@MathiasYde
Copy link

Is it possible to make this work with the Range property attribute?

@INeatFreak
Copy link

Is it possible to make this work with the Range property attribute?

Unfortunately it's not possible currently, at least not while using the PropertyDrawer class.
Might be possible to use an extra [Optional] attribute and draw with attribute drawer.

@INeatFreak
Copy link

INeatFreak commented Jun 28, 2022

I've added C# operators support so we don't have to write extra parts.
You can find the gist here or also on "Forks" tab of this gist.

// Assigning default values without writing extra new Optional<float>(100.95f); part.
[SerializeField] Optional<float> optionalFloat = 100.95f;
[SerializeField] Optional<int> optionalInt = 50;
[SerializeField] Optional<Transform> optionalTransform = null;

// Add, subtract, multiply etc values without writing extra .Value part
optionalFloat += 1.1f;
optionalTransform = null;

// Comparisons & Null checks
if (optionalTransform == null) { Debug.Log("optionalTransform is null"); }
if (optionalInt == optionalInt2) { Debug.Log("optionalInt and optionalInt2 have the same value."); }

// Check if it's enabled without needing to write .Enabled
if (optionalInt) { Debug.Log("optionalInt is enabled."); }

@MathiasYde
Copy link

Can you provide code examples of how you use Optional?

@MathiasYde
Copy link

Can you provide code examples? It's hard to help when you don't show anything to fix.

@pippinsmith
Copy link

@MathiasYde the video by author of this gist contains a decent code example as well as the reason for creating the system: Optional Variables - Unity Tips [2020.1+] , hope that helps!

@MathiasYde
Copy link

I was trying to help another person, but it seems he deleted his comments. I am fully aware of this situation, no further elaboration is necessary :)

@pippinsmith
Copy link

Ahh, makes sense, only (re)discovering this entire project now and binging all the videos 😛

@Daniel-Pham831
Copy link

Daniel-Pham831 commented May 19, 2024

Hi, I did make a custom editor for this Optional which also support passing attributes down to the "T value".
Just want to share this to the community.
Oh, you also need to have Odin Inspector packet

Here are some examples.
image
image

OptionalAttributeProcessor.cs

using System.Collections.Generic;
using System.Reflection;
using Newtonsoft.Json;
using Sirenix.OdinInspector;
using Sirenix.OdinInspector.Editor;
using UnityEngine;

namespace Maniac.Utils.Editor
{
    public class OptionalAttributeProcessor<T> : OdinAttributeProcessor<Optional<T>>
    {
        private static readonly List<Type> KeepAttributeTypes = new List<Type>
        {
            typeof(SerializeField),
            typeof(SerializableAttribute),
            typeof(HeaderAttribute),
            typeof(InfoBoxAttribute),
            typeof(JsonIgnoreAttribute),
        };

        private List<Attribute> parentAttributesToPassDown = new List<Attribute>();
        public override void ProcessSelfAttributes(InspectorProperty property, List<Attribute> attributes)
        {
            // Store attributes to pass down
            parentAttributesToPassDown.Clear();
            foreach (var attribute in attributes)
            {
                if (!KeepAttributeTypes.Contains(attribute.GetType()))
                {
                    parentAttributesToPassDown.Add(attribute);
                }
            }

            // Remove attributes that are not to be kept on the parent
            attributes.RemoveAll(attr => !KeepAttributeTypes.Contains(attr.GetType()));
            
            base.ProcessSelfAttributes(property, attributes);
        }

        public override void ProcessChildMemberAttributes(InspectorProperty parentProperty, MemberInfo member, List<Attribute> attributes)
        {
            // If the member is the 'value' field of the Optional<T> class, propagate selected attributes to it
            if (member.Name == "value")
            {
                attributes.AddRange(parentAttributesToPassDown);
            }
            
            base.ProcessChildMemberAttributes(parentProperty,member, attributes);
        }
    }
}

OptionalDrawer.cs


using Sirenix.OdinInspector.Editor;
using Sirenix.Utilities;
using Sirenix.Utilities.Editor;
using UnityEditor;
using UnityEngine;

namespace Maniac.Utils.Editor
{
    public class OptionalDrawer<T> : OdinValueDrawer<Optional<T>>
    {
        private PropertyTree propertyTree;
        private readonly string enabledLabel = nameof(Optional<T>.Enabled).AddColor(Color.cyan);
        private readonly string disabledLabel = nameof(Optional<T>.Enabled).AddColor(Color.black);

        protected override void DrawPropertyLayout(GUIContent label)
        {
            var valueEntry = this.ValueEntry;
            var optional = valueEntry.SmartValue;

            EditorGUILayout.BeginVertical(GUI.skin.box);
            {
                EditorGUILayout.BeginHorizontal();
                {
                    EditorGUILayout.LabelField(label.AddColor(Color.white), new GUIStyle
                    {
                        alignment = TextAnchor.MiddleLeft,
                        richText = true
                    });
                    GUILayout.FlexibleSpace();
                    var correctLabel = optional.Enabled ? enabledLabel : disabledLabel;
                    var userClicked = GUILayout.Button(correctLabel, new GUIStyle(GUI.skin.button)
                    {
                        richText = true
                    });
                    if (userClicked)
                    {
                        optional.Enabled = !optional.Enabled;
                    }
                }
                EditorGUILayout.EndHorizontal();

                if (optional.Enabled)
                {
                    var valueProperty = GetValueChildren(valueEntry.Property);
                    if (valueProperty == null)
                    {
                        SirenixEditorGUI.WarningMessageBox($"Cannot find value in {valueEntry.Property}. Please ensure that it's serializable!");
                    }
                    else
                    {
                        EditorGUILayout.BeginVertical(GUI.skin.box);
                        {
                            var childLabel = valueProperty.Label;
                            childLabel.text = string.Empty;
                            valueProperty.Draw(childLabel);
                        }
                        EditorGUILayout.EndVertical();
                    }
                }
            }
            EditorGUILayout.EndVertical();

            valueEntry.SmartValue = optional;
        }

        private InspectorProperty GetValueChildren(InspectorProperty parent)
        {
            foreach (var child in parent.Children)
            {
                if (child.Name == "value")
                {
                    return child;
                }
            }
            return null;
        }
    }
    
    public static class Extension
    {
        private static readonly string colorFormat = "<color={1}>{0}</color>";

        public static string AddColor(this object origin, string colorInHex)
        {
            return string.Format(colorFormat, origin.ToString(), colorInHex);
        }
        
        public static string AddColor(this object origin, Color color)
        {
            return string.Format(colorFormat, origin.ToString(), ToRGBHex(color));
        }
        
        public static string ToRGBHex(Color c)
        {
            return $"#{ToByte(c.r):X2}{ToByte(c.g):X2}{ToByte(c.b):X2}";
        }
        
        private static byte ToByte(float f)
        {
            f = Mathf.Clamp01(f);
            return (byte)(f * 255);
        }
    }
}


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment