Bindable Enumerations

As much as I love BindableValues, there are still some features that are frustratingly out of reach. First and foremost, I wish we lived in a world where enumerations in C# implemented IEquatable, so they could be made bindable. Despite throwing a couple of workarounds their way, nothing was ever good enough to stick, and my enumerated values continued to loll about, unbound. I couldn’t take it anymore! I settled on a solution that was disappointingly duplicative, but practical: clone the functionality of BindableValues to create a new wrapper specifically for enumerations.

// --------------------------------
// <copyright file="BindableEnum.cs" company="Rumor Games">
//     Copyright (C) Rumor Games, LLC.  All rights reserved.
// </copyright>
// --------------------------------

using System;
using System.Collections.Generic;

/// <summary>
/// BindableEnum class.
/// </summary>
/// <typeparam name="T">Backing value type.</typeparam>
public class BindableEnum<T> where T : struct, IComparable, IFormattable, IConvertible
{
    /// <summary>
    /// Backing value, serialize to show in Inspector.
    /// </summary>
    [UnityEngine.SerializeField]
    private T backingValue;

    /// <summary>
    /// Event raised when bindable value changes.
    /// </summary>
    public Action<T> OnValueChanged;

    /// <summary>
    /// Gets or sets the bindable value.
    /// </summary>
    public T Value
    {
        get
        {
            return this.backingValue;
        }
        set
        {
            if (!this.Equals(value))
            {
                this.backingValue = value;
                if (this.OnValueChanged != null)
                {
                    this.OnValueChanged(this.backingValue);
                }
            }
        }
    }

    /// <summary>
    /// Initializes a new instance of the BindableEnum class.
    /// </summary>
    /// <param name="value">Initial value.</param>
    public BindableEnum(T value)
    {
        if (!typeof(T).IsEnum)
        {
            throw new ArgumentException("Argument must be an enumerable type.");
        }

        this.backingValue = value;
    }

    /// <summary>
    /// Implicitly convert BindableEnum to backing value type.
    /// </summary>
    /// <param name="bindableEnum">BindableEnum to convert.</param>
    /// <returns>Backing value.</returns>
    public static implicit operator T(BindableEnum<T> bindableEnum)
    {
        return bindableEnum.Value;
    }

    /// <summary>
    /// Determines whether one BindableEnum is equal to another.
    /// </summary>
    /// <param name="a">First BindableEnum with value of type T to compare.</param>
    /// <param name="b">Second BindableEnum with value of type T to compare.</param>
    /// <returns>True if the BindableEnums are equal.</returns>
    public static bool operator ==(BindableEnum<T> a, BindableEnum<T> b)
    {
        if (ReferenceEquals(a, null))
        {
            return ReferenceEquals(b, null);
        }

        return a.Equals(b);
    }

    /// <summary>
    /// Determines whether one BindableEnum type is not equal to another.
    /// </summary>
    /// <param name="x">First BindableEnum with value of type T to compare.</param>
    /// <param name="y">Second BindableEnum with value of type T to compare.</param>
    /// <returns>True if the BindableEnums are not equal.</returns>
    public static bool operator !=(BindableEnum<T> x, BindableEnum<T> y)
    {
        return !(x == y);
    }

    /// <summary>
    /// Determines whether the specified object is equal to the current object.
    /// </summary>
    /// <returns>True if the specified object is equal to the current object; otherwise, false.</returns>
    /// <param name="obj">The object to compare with the current object.</param>
    public override bool Equals(object obj)
    {
        // is the object null?
        if (ReferenceEquals(obj, null))
        {
            return false;
        }

        // are they the same instance?
        if (ReferenceEquals(this, obj))
        {
            return true;
        }

        // are the values equal?
        if (obj is T)
        {
            return this.Equals((T)obj);
        }

        // are they the same type?
        if (obj.GetType() != this.GetType())
        {
            return false;
        }

        // are they otherwise equal?
        return this.Equals((BindableEnum<T>)obj);
    }

    /// <summary>
    /// Serves as a hash function for a particular type. 
    /// </summary>
    /// <returns>A hash code for the current object.</returns>
    public override int GetHashCode()
    {
        unchecked
        {
            return (EqualityComparer<T>.Default.GetHashCode(this.backingValue) * 397) ^ (this.OnValueChanged != null ? this.OnValueChanged.GetHashCode() : 0);
        }
    }

    /// <summary>
    /// Determines whether the specified object is equal to the current object.
    /// </summary>
    /// <returns>True if the specified object is equal to the current object; otherwise, false.</returns>
    /// <param name="other">The object to compare with the current object.</param>
    protected bool Equals(BindableEnum<T> other)
    {
        return this.Equals(other.backingValue) && Equals(this.OnValueChanged, other.OnValueChanged);
    }

    /// <summary>
    /// Determines whether the specified object is equal to the current object.
    /// </summary>
    /// <returns>True if the specified object is equal to the current object; otherwise, false.</returns>
    /// <param name="other">The object to compare with the current object.</param>
    protected bool Equals(T other)
    {
        return EqualityComparer<T>.Default.Equals(this.backingValue, other);
    }
}

And some basics on how to use it…

Let’s start with an enumeration, something that might change in some way during a game. Say, which weapon is currently equipped:

/// <summary>
/// WeaponType enumeration.
/// </summary>
public enum WeaponType
{
    Glaive,
    Halberd,
    Pike,
    Trident
}

To make this type bindable, we need to wrap it in class that implements BindableEnum. As a reminder, the generic type is what gives the class its functionality; wrapping it in a serializable class of its own is only necessary for it to appear as desired in the Inspector.

/// <summary>
/// BindableWeaponType class.
/// </summary>
[Serializable]
public class BindableWeaponType : BindableEnum<WeaponType>
{
    /// <summary>
    /// Initializes a new instance of the BindableWeaponType class.
    /// </summary>
    /// <param name="value">Initial value.</param>
    public BindableWeaponType(WeaponType value)
        : base(value)
    {
    }
}

In practice, we treat this much like a regular enumeration, except in two key ways. First, assignment needs to be applied to this.field.Value, rather than this.field. Second, we can now subscribe to the variable’s OnValueChanged event. It’s bindable!

Subscribers are notified whenever the value changes, and are passed the updated value.

    /// <summary>
    /// This function is called when the object becomes enabled and active.
    /// </summary>
    internal void OnEnable()
    {
        this.player.weapon.OnValueChanged += this.OnValueChanged;
    }

    /// <summary>
    /// This function is called when the behaviour becomes disabled or inactive.
    /// </summary>
    internal void OnDisable()
    {
        this.player.weapon.OnValueChanged -= this.OnValueChanged;
    }

    /// <summary>
    /// Handles a change in value.
    /// </summary>
    /// <param name="value">New value after change.</param>
    private void OnValueChanged(WeaponType value)
    {
        switch (value)
        {
            case WeaponType.Glaive:
                // glaive'n
                break;
            case WeaponType.Halberd:
                // halberd is the word
                break;
            case WeaponType.Pike:
                // i like pike
                break;
            case WeaponType.Trident:
                // the sugar-free pole weapon dentists prefer
                break;
        }
    }

Now, finally, I can wire up UI, gameplay, or whatever to respond to changes in enum variables, just like any other bindable value!

Bookmark the permalink.

Comments are closed