com.xmobitea.changx.observertype
v1.5.0
Published
XmobiTea Unity Toolkit packages
Readme
XmobiTea ObserverType
Serializable observer wrappers for primitive and custom values, designed mainly for Unity inspector usage plus manual change notifications through UnityEvent<T>.
AI Quick Contract
If you only need the core usage rules, read this section first.
- Each observer wraps one raw value plus one
UnityEvent<T>named_onValueChanged. SetValue(...)updates the raw value and invokes the event.SetValue(...)does not check equality first. It invokes_onValueChangedeven when the new value equals the current value.SetValueWithoutNotify(...)updates the raw value without invoking the event.GetValue()returns the raw value.- Most primitive observers support explicit cast from raw type to observer and implicit cast from observer to raw type.
ObserverFloatadditionally accepts explicit cast fromintandlong.ObserverDoubleadditionally accepts explicit cast fromfloat,int, andlong.- Numeric and char observers also overload
++and--. ObserverBool,ObserverString, andObserverCustom<T>do not overload++or--.ObserverBoolandObserverChardo not implementIFormattable; all other numeric wrappers do.- Important:
++and--mutate the raw value but do not invokeonValueChanged. - Inspector edits through the custom property drawers also update serialized
_rawValuedirectly and do not invokeonValueChanged. ObserverCustom<T>has no custom property drawer.- Most primitive implicit casts are not null-safe. Casting a null observer instance to the raw primitive type will throw.
ObserverStringcan hold a null raw string. Its implicit cast from a null wrapper is safe, but string helper members such asLength,Substring(...),ToString(), andGetHashCode()require_rawValueto be non-null.- Explicit cast from
stringtoObserverStringreturnsnullwhen the source string isnull. ObserverCustom<T>can hold a null raw value for reference types.==,GetHashCode(),ToString(), and some directEquals(...)overload paths require care when either raw value may be null.
AI Support Files
For AI agents and tooling-specific guidance, also read:
AGENTS.md: package-level rules for code generation and maintenance.AI_USAGE.md: short machine-friendly usage rules for using this package without reading the implementation.
What This Package Provides
Primitive/specialized wrappers:
ObserverBoolObserverByteObserverSByteObserverShortObserverUShortObserverIntObserverUIntObserverLongObserverULongObserverFloatObserverDoubleObserverCharObserverString
Generic wrapper:
ObserverCustom<T>
Editor support:
- custom property drawers for the non-generic observer types
AI Capability Matrix
Use this table when generating code quickly:
| Type group | Notify write | Silent write | Implicit read null-safe | ++ / -- | IFormattable | Custom drawer |
| --- | --- | --- | --- | --- | --- | --- |
| Numeric signed/unsigned except ObserverBool | SetValue(...) | SetValueWithoutNotify(...) | No | Yes | Yes | Yes |
| ObserverFloat | SetValue(...) | SetValueWithoutNotify(...) | No | Yes | Yes | Yes |
| ObserverDouble | SetValue(...) | SetValueWithoutNotify(...) | No | Yes | Yes | Yes |
| ObserverChar | SetValue(...) | SetValueWithoutNotify(...) | No | Yes | No | Yes |
| ObserverBool | SetValue(...) | SetValueWithoutNotify(...) | No | No | No | Yes |
| ObserverString | SetValue(...) | SetValueWithoutNotify(...) | Yes, for null wrapper only | No | No | Yes |
| ObserverCustom<T> | SetValue(...) | SetValueWithoutNotify(...) | No | No | No | No dedicated drawer |
Notes:
ObserverFloathas explicit casts fromfloat,int, andlong.ObserverDoublehas explicit casts fromdouble,float,int, andlong.ObserverStringexplicit cast fromnullstring returnsnull, not an observer wrappingnull.ObserverCustom<T>explicit cast fromnullcreates an observer wrappingnull.
Runtime Files
Runtime/IObserverType.csRuntime/ObserverBool.csRuntime/ObserverByte.csRuntime/ObserverSByte.csRuntime/ObserverShort.csRuntime/ObserverUShort.csRuntime/ObserverInt.csRuntime/ObserverUInt.csRuntime/ObserverLong.csRuntime/ObserverULong.csRuntime/ObserverFloat.csRuntime/ObserverDouble.csRuntime/ObserverChar.csRuntime/ObserverString.csRuntime/ObserverCustom.cs
Exact Runtime Behavior
Shared observer pattern
Almost every observer class follows the same structure:
- serialize
_rawValue - serialize
_onValueChanged - initialize
_onValueChangedin the constructor - expose:
SetValue(...)SetValueWithoutNotify(...)GetValue()- conversion operators
- equality/comparison helpers
This package is not a full reactive system.
It is a lightweight value-wrapper with manual notification.
SetValue(...)
SetValue(...):
- updates
_rawValue - invokes
onValueChanged
This is the main API to use when you want listeners to react.
It does not skip duplicate values. Calling SetValue(currentValue) still invokes onValueChanged.
SetValueWithoutNotify(...)
Updates _rawValue only.
No event is invoked.
Use it only when silent mutation is intentional.
GetValue()
Returns the current raw value.
onValueChanged
Each wrapper exposes:
public UnityEvent<T> onValueChanged => _onValueChanged;This is UnityEvent-based, not C# event-based.
Listeners can be:
- wired in inspector
- added by code
++ / -- behavior
Numeric and char wrappers implement ++ and --.
Important behavior:
- they mutate
_rawValuedirectly - they do not call
SetValue(...) - they do not invoke
onValueChanged
So this:
observerInt++;changes the value silently.
If you need notification, use:
observerInt.SetValue(observerInt.GetValue() + 1);Additional overflow note for unsigned observers:
ObserverULong--when_rawValueis0overflows toulong.MaxValue(standard unchecked C# wrap-around).ObserverByte++when_rawValueis255overflows to0.- All unsigned observers follow the same unchecked wrap-around behavior on overflow.
Avoid using -- on an ObserverULong without first verifying the value is greater than zero.
Equality and comparison
Primitive wrappers implement:
==!=Equals(...)CompareTo(...)GetHashCode()ToString()
ObserverCustom<T> implements only ==, !=, Equals(...), GetHashCode(), and ToString(). It does not implement CompareTo(...).
ObserverString implements Equals(ObserverString) as a method but does not implement IEquatable<ObserverString>.
Comparison/equality is based on _rawValue, not on listener state.
The overloaded == / != operators null-check wrapper references. Most typed Equals(...) and CompareTo(...) overloads do not null-check the wrapper argument before reading other._rawValue, so null-check first when the compared observer may be null.
Null-safety
Important behavior:
- most primitive wrappers do not null-check in implicit cast to raw type
- so this can throw:
ObserverInt value = null;
int raw = value;ObserverString is different:
- implicit cast to
stringreturnsnullif the observer isnull - explicit cast from
stringreturnsnullif the source string isnull - if the observer exists but its raw string is
null, string-like helpers can throw
ObserverString
ObserverString provides extra string-like helpers:
Length- indexer
this[int index] Substring(...)StartsWith(...)EndsWith(...)
Important behavior:
- it is still just wrapping a plain string
- there is no actual encryption or obfuscation despite helper naming like
InternalDecryptToString() - notification semantics remain the same as other wrappers
- does not implement
IEquatable<ObserverString>(unlike numeric wrappers);Equals(ObserverString)is available as a method overload only - implicit cast to
stringis null-safe: returnsnullwhen the observer reference isnull - explicit cast from
stringreturnsnullwhen the source string isnull Length, indexer,Substring(...),StartsWith(...),EndsWith(...),ToString(), andGetHashCode()assume_rawValueis non-null
ObserverCustom<T>
ObserverCustom<T> is the generic version.
Important behavior:
- same
SetValue(...)/SetValueWithoutNotify(...)pattern - same
UnityEvent<T>event storage - no custom property drawer
- equality depends on
T.Equals(...) - explicit cast from
Talways creates an observer wrapper, even whenTisnull - implicit cast to
Tis not null-safe - does not implement
IComparableorIComparable<T>- do not callCompareTo(...)on it - does not implement
IFormattable ==,GetHashCode(),ToString(), andEquals(ObserverCustom<T>)are not safe for every null raw-value combination- when
Tcan be null, prefer comparingGetValue()results withEqualityComparer<T>.Default
This type is usable in code and serializable scenarios where Unity supports the generic payload, but editor UX is much weaker than the primitive wrappers.
Safe nullable custom comparison pattern:
using System.Collections.Generic;
bool AreEqual<T>(ObserverCustom<T> left, ObserverCustom<T> right)
{
if (ReferenceEquals(left, right)) return true;
if (left == null || right == null) return false;
return EqualityComparer<T>.Default.Equals(left.GetValue(), right.GetValue());
}ObserverULong notes
ObserverULong has two differences from all other observers:
1. _rawValue is internal instead of private:
internal ulong _rawValue;Code within the same runtime assembly (com.xmobitea.changx.observer-type.runtime) can read and mutate _rawValue directly without going through SetValue(...), bypassing onValueChanged. From external user assemblies (normal game code) this field is not accessible.
2. Editor drawer uses LongField (signed):
The inspector drawer renders _rawValue using EditorGUI.LongField. For ulong values above long.MaxValue (9,223,372,036,854,775,807), the inspector will display a negative number. No clamping is applied. The underlying serialized value is not corrupted, but inspector UX is unreliable for large ulong values.
Editor Behavior
Custom property drawers exist for the non-generic observer types.
They show:
- one field for
_rawValue - one foldout area for
_onValueChanged
Important behavior:
- editor drawers write
_rawValuedirectly throughSerializedProperty - they do not invoke
onValueChanged - inspector edits are therefore silent state changes
This is consistent with serialized editor editing, not runtime reactive notification.
Per-type editor specifics:
ObserverByte: renders asIntSliderclamped to[0, 255].ObserverSByte: renders asIntFieldclamped to[-128, 127].ObserverShort: renders asIntFieldclamped to[-32768, 32767].ObserverUShort: renders asIntFieldclamped to[0, 65535].ObserverUInt: renders asLongFieldclamped to[0, 4294967295].ObserverChar: renders asTextField. Only the first character is stored; empty string stores'\0'.ObserverULong: renders asLongField(signed). Values abovelong.MaxValuedisplay incorrectly. No clamping.
Basic Usage
Runtime notification
using UnityEngine;
using XmobiTea.ObserverType;
public sealed class ExampleObserverUsage : MonoBehaviour
{
[SerializeField] private ObserverInt coins = new ObserverInt();
private void Awake()
{
coins ??= new ObserverInt();
coins.onValueChanged.AddListener(OnCoinsChanged);
}
private void OnDestroy()
{
if (coins != null)
coins.onValueChanged.RemoveListener(OnCoinsChanged);
}
private void Start()
{
coins.SetValue(10);
}
private void OnCoinsChanged(int value)
{
Debug.Log("Coins changed: " + value);
}
}Silent update
coins.SetValueWithoutNotify(100);Primitive conversion
ObserverInt hp = (ObserverInt)10;
int rawHp = hp;Correct increment with notification
coins.SetValue(coins.GetValue() + 1);Do / Don't
Do
- Do use
SetValue(...)when listeners must react. - Do use
SetValueWithoutNotify(...)only when silent mutation is intentional. - Do assume inspector edits are silent.
- Do treat
++/--as silent mutation helpers. - Do null-check observer references before implicit conversion in code where the wrapper may be null.
- Do initialize serialized observer fields inline or guard them before adding listeners when components may be created from code.
- Do remove runtime listeners you add with
AddListener(...)when the owning object is destroyed or disabled.
Don't
- Don't assume
++/--triggersonValueChanged. - Don't assume inspector edits trigger
onValueChanged. - Don't treat this package as a full reactive binding framework.
- Don't assume
ObserverCustom<T>has the same editor support as primitive wrappers. - Don't rely on implicit cast from a null primitive observer wrapper.
- Don't add runtime listeners repeatedly without a matching cleanup path.
Common Mistakes
Mistake 1: Using ++ and expecting notification
Wrong:
coins++;This changes _rawValue silently.
Correct:
coins.SetValue(coins.GetValue() + 1);Mistake 2: Assuming inspector edits invoke listeners
They do not.
The drawers only assign serialized _rawValue.
Mistake 3: Implicit cast from null observer
For most primitive wrappers, this throws.
Mistake 3.1: Adding listeners to a null wrapper
Serialized observer fields are class references. If a component is created from code or data is incomplete, the wrapper reference can be null.
Prefer inline initialization plus a defensive guard:
[SerializeField] private ObserverInt score = new ObserverInt();
private void Awake()
{
score ??= new ObserverInt();
score.onValueChanged.AddListener(OnScoreChanged);
}Remove code-added listeners when the owner is destroyed or disabled:
private void OnDestroy()
{
if (score != null)
score.onValueChanged.RemoveListener(OnScoreChanged);
}Mistake 4: Treating ObserverString as encrypted storage
It is not encrypted.
It is just a wrapper around string.
Mistake 5: Calling string helpers while ObserverString stores null
The implicit cast from a null ObserverString wrapper to string is null-safe, but methods on an existing wrapper assume _rawValue is non-null.
Prefer this when a null string is possible:
string rawName = playerName;
if (!string.IsNullOrEmpty(rawName))
{
Debug.Log(rawName.Substring(0, 1));
}Mistake 6: Expecting SetValue(...) to skip unchanged values
SetValue(...) always invokes onValueChanged, even when the new raw value equals the current raw value.
Add your own equality guard if duplicate notifications matter:
if (coins.GetValue() != newCoins)
{
coins.SetValue(newCoins);
}Mistake 7: Comparing nullable ObserverCustom<T> values with ==
When T can be null, avoid left == right because the operator can call Equals(...) on a null raw value in some one-null/one-non-null cases. Compare raw values through EqualityComparer<T>.Default instead.
Repository Usage Pattern
Current repository usage shows these observer wrappers are intended as:
- serialized fields in MonoBehaviours
- inspector-friendly event sources
- small reactive-style wrappers around primitive values
They are not used as a replacement for a full observable state system.
Decision Table
Use this package when:
- you want serialized value wrappers with UnityEvent callbacks,
- you want inspector-exposed primitive observer fields,
- you accept manual notification semantics.
Do not use this package when:
- you need full reactive streams,
- you need automatic event dispatch for every mutation path,
- you need robust generic inspector support for arbitrary
T, - you need thread-safe observable state.
Expected AI Usage Pattern
When an AI agent generates code using this package, the correct default pattern is:
- Store the observer as a serialized field.
- Subscribe to
onValueChanged. - Use
SetValue(...)for notifying mutations. - Use
GetValue()or implicit cast for reads. - Avoid
++/--unless silent mutation is intended.
An AI agent should not:
- assume every mutation path notifies listeners,
- assume editor edits notify listeners,
- assume generic observer fields have polished custom inspector support,
- assume primitive implicit casts are null-safe.
Namespace
using XmobiTea.ObserverType;Assembly Definitions
Runtime assembly:
com.xmobitea.changx.observer-type.runtimeRuntime assembly details:
- root namespace:
XmobiTea.ObserverType autoReferenced: true- no explicit asmdef references
- available on all platforms
Editor assembly:
com.xmobitea.changx.observer-type.editorEditor assembly details:
- root namespace:
XmobiTea.ObserverType.Editor - references
com.xmobitea.changx.observer-type.runtime - included only on the
Editorplatform autoReferenced: true
Package Metadata
- Package name:
com.xmobitea.changx.observertype - Version:
1.5.0 - Unity version:
2022.3+ - License:
Apache-2.0 - Required dependency:
com.xmobitea.changx.app: 1.5.0
The runtime assembly (com.xmobitea.changx.observer-type.runtime) has autoReferenced: true, so normal user assemblies do not need to add an explicit reference to use this package. If a project assembly disables auto references or uses strict asmdef references, add a reference to com.xmobitea.changx.observer-type.runtime.
