Simple observable fields implementation for Unity that works with an inspector.
Open Unity > Window > Package Manager > + > Add package from git URL:
https://github.com/Eyellen/Eyellen.Unity.ObservableFields.git
Paste and press "Add"
ObservableField
- Does not support inspector and uses only pure C# features.
UnityObservableField
- Supports inspector, uses C# and Unity events. You can assign Unity events in the inspector.
IReadOnlyObservableField
- Read only interface for observable fields. Allows to get read only value and subscribe to changes.
IObservableFieldEvents
- Interface that provides methods to subscribe or unsubscribe from changes.
- You must call constructor for observable fields, otherwise it will give a
NullReferenceException
. The only exception is if you're going to useSerializeFieldAttribute
onUnityObservableField
and as a T parameter you pass a type that hasSerializableAttribute
, i.e. Unity is able to serialize that field. - If you want Unity events to be called in edit mode make sure to set
UnityEventCallState
toEditorAndRuntime
in the inspector, it's next to each method reference in the unity event property. If you want C# events to be called in edit mode make sure to addExecuteAlwaysAttribute
to your class. - If you pass your custom type to
UnityObservableField
and want it to appear in the inspector make sure to addSerializableAttribute
to your type. - Remember that reference types are reference types,
ObservableField
will not fire events when reference type's members will be changed, it will fire event only if the reference itself is changed. If you need to fire event when complex object is changed prefer using structs.
using System;
using UnityEngine;
namespace Eyellen.Unity.ObservableFields.Examples
{
public class ObservableFieldDeclaration : MonoBehaviour
{
// ObservableField does not support inspector. If you want it to show up in the inspector, use UnityObservableField.
// Does not appear in the inspector even though it has a SerializeField attribute.
// Uses CSharp events.
[SerializeField]
private ObservableField<bool> ObservableBool = new();
// Displayed in the inspector.
// Uses CSharp events and Unity events. Unity events can be assigned in the inspector.
[SerializeField]
private UnityObservableField<bool> UnityObservableBool = new();
// Displayed in the inspector since a passed type have Serializable attribute.
[SerializeField]
private UnityObservableField<SerializedType> SerializableType = new();
// Does not appear in the inspector because a type passed to a field is not serializable.
[SerializeField]
private UnityObservableField<NonSerializedType> NonSerializableType = new();
}
public class NonSerializedType
{
public string Name;
public string Description;
}
[Serializable]
public class SerializedType
{
public string Name;
public string Description;
}
}
Declared UnityObservableField
s will look like this in the inspector:
using UnityEngine;
namespace Eyellen.Unity.ObservableFields.Examples
{
public class ObservableFieldSubscribing : MonoBehaviour
{
// You can subscribe to observable field changes in code and in inspector.
// NOTE: To subscribe on changes in the inspector use UnityObservableField.
private ObservableField<int> Number;
[SerializeField]
private UnityObservableField<string> String;
private void Awake()
{
// ObservableField.
// Subscribing to C# events.
Number.SubscribeOnChange(OnValueChangedEventArgs);
Number.SubscribeOnChange(OnValueChangedTTArgs);
Number.SubscribeOnChange(OnValueChangedNoArgs);
// UnityObservableField.
// Subscribing to C# events.
String.SubscribeOnChange(OnValueChangedEventArgs);
String.SubscribeOnChange(OnValueChangedTTArgs);
String.SubscribeOnChange(OnValueChangedNoArgs);
}
// Method that takes ObservableField.EventArgs as a parameter.
// ObservableField.EventArgs contain previous and current values.
private void OnValueChangedEventArgs<T>(ObservableField<T>.EventArgs args)
{
Debug.Log(
$"Call {nameof(OnValueChangedEventArgs)}: Previous value - {args.Previous}. Current value - {args.Current}"
);
}
// Method that takes previous and current value as a parameters.
private void OnValueChangedTTArgs<T>(T previous, T current)
{
Debug.Log(
$"Call {nameof(OnValueChangedTTArgs)}: Previous value - {previous}. Current value - {current}"
);
}
// Method that takes no parameters.
private void OnValueChangedNoArgs()
{
Debug.Log($"Call {nameof(OnValueChangedNoArgs)}");
}
}
}
Inspector:
Use Unity Events
- indicates if Unity events will be called. If has false value Unity events will not be called even if they were assigned.
If you want to use Unity events, check Use Unity Events
and Events
foldout will pop up:
Click on Events
foldout and there are 4 Unity events:
- Passes
ObservableField.EventArgs
as a parameter - Passes
Previous
andCurrent
values as a parameters - Passes
Current
value as a parameter - Passes no parameters
using UnityEngine;
namespace Eyellen.Unity.ObservableFields.Examples
{
public class UsingIReadOnlyObservableField : MonoBehaviour
{
// You can use IReadOnlyObservableField if you want to allow readonly value on you observable field
// but want to protect it from changes from outside the class.
// Private observable field.
// Can't be accessed outside the class.
[SerializeField]
private UnityObservableField<float> _float = new();
// Public IReadOnlyObservableField.
// Can be accessed outside the class.
// Provides only readonly Value getter and methods to subscribe to change event.
public IReadOnlyObservableField<float> Float => _float;
}
}
using UnityEngine;
namespace Eyellen.Unity.ObservableFields.Examples
{
public class ObservableFieldGetterSetters : MonoBehaviour
{
// You can add Getter/Setter like behaviour to observable fields and specify Get/Set logic.
// Getter/Setter behaviour works fine with an inspector.
// Getter/Setter's are passed into a field constructor as a Func<T, T>
// where first value is input value and the second one is processed result.
// Here we implemented setter that clamps passed value between 0 and 10.
[SerializeField]
private UnityObservableField<int> ObservableInt =
new(default, value => Mathf.Clamp(value, 0, 10));
// Here we implemented getter that simply returns the passed float number as a negative while keeping original value as positive.
[SerializeField]
private UnityObservableField<float> ObservableFloat = new(default, null, value => -value);
}
}
MIT