Unity Helpers provides serializable wrappers for types that Unity can't serialize natively: GUIDs, dictionaries, sets, type references, and nullable values. All types include custom property drawers for a seamless inspector experience and support JSON/Protobuf serialization.
// Generate new GUIDWGuidid1=WGuid.NewGuid();// From System.GuidSystem.GuidsysGuid=System.Guid.NewGuid();WGuidid2=(WGuid)sysGuid;// Parse from stringWGuidid3=WGuid.Parse("12345678-1234-1234-1234-123456789abc");// Try parse (safe)if(WGuid.TryParse("...",outWGuidid4)){Debug.Log($"Parsed: {id4}");}// Empty GUIDWGuidempty=WGuid.EmptyGuid;
Problem: Unity doesn't serialize Dictionary<TKey, TValue>Solution:SerializableDictionary<TKey, TValue> maintains synchronized arrays for Unity serialization and a runtime dictionary for fast lookups
usingUnityEngine;usingWallstopStudios.UnityHelpers.Core.DataStructure.Adapters;publicclassPrefabRegistry:MonoBehaviour{publicGameObjectenemyPrefab;publicGameObjectplayerPrefab;publicSerializableDictionary<string,GameObject>prefabs;privatevoidStart(){// Access entriesif(prefabs.TryGetValue("Enemy",outGameObjectprefab)){Instantiate(prefab);}// CountDebug.Log($"Prefab count: {prefabs.Count}");// Iterationforeach(varkvpinprefabs){Debug.Log($"{kvp.Key}: {kvp.Value}");}/* Add entries (or overwrite) WARNING: This is just for demo purposes! SerializableDictionary is meant for editor-mode persistence. Nothing stops you from changing this at runtime, but it will be lost on next playthrough. */prefabs["Player"]=playerPrefab;prefabs["Enemy"]=enemyPrefab;}}
Problem: Unity doesn't serialize HashSet<T> or SortedSet<T>Solution:SerializableHashSet<T> and SerializableSortedSet<T> maintain a serialized array and runtime set for fast lookups
Duplicate detection with visual highlighting (shake animation + color)
Null entry highlighting (red background)
Pagination for large sets
Move Up/Down buttons
Current selection badge for items on other pages
New Entry foldout to stage values before adding them to the runtime set (tune its animation via Project Settings ▸ Wallstop Studios ▸ Unity Helpers ▸ Set Foldouts)
Visual Reference
Visual feedback for duplicate entries (yellow shake) and null values (red background)
// Implements ISet<T> and IReadOnlyCollection<T>SerializableHashSet<int>set=new();// Add (returns true if new)booladded=set.Add(42);// Readboolcontains=set.Contains(42);intcount=set.Count;// Removeboolremoved=set.Remove(42);set.Clear();// Set operationsHashSet<int>other=newHashSet<int>{1,2,3};set.UnionWith(other);// Add all from otherset.IntersectWith(other);// Keep only common elementsset.ExceptWith(other);// Remove elements in otherbooloverlaps=set.Overlaps(other);// Iterationforeach(intiteminset){Debug.Log(item);}
Expandable "New Entry" controls let you configure the exact value that will be inserted, which is especially helpful for complex structs, managed references, or ScriptableObjects. The foldout supports the same field variety as the inline list and respects your duplicate/null validation. Animation for the New Entry foldout is governed by the Serializable Set Foldouts settings; adjust tweening and speed independently for SerializableHashSet<T> and SerializableSortedSet<T>.
By default, SerializableSet inspectors start collapsed until you open them. This baseline comes from Project Settings ▸ Wallstop Studios ▸ Unity Helpers via the Serializable Set Start Collapsed toggle (and the equivalent Serializable Dictionary Start Collapsed toggle for dictionaries). You can override the default per-field with [WSerializableCollectionFoldout]:
Project setting establishes the initial state only.
[WSerializableCollectionFoldout] can request expanded or collapsed behavior for specific collections.
Explicit changes to SerializedProperty.isExpanded (scripts, custom inspectors, or tests) take ultimate precedence. The drawer now respects those manual decisions, so opting-in via code no longer gets undone by the attribute or the global default.
The attribute applies to both SerializableHashSet<T>/SerializableSortedSet<T> and the dictionary equivalents, making it straightforward to mix project-wide defaults with per-field intentions.
Problem: Unity doesn't serialize System.Type, and type names break when refactoring Solution:SerializableType stores assembly-qualified names with fallback resolution on rename/namespace changes
usingSystem;usingSystem.Collections.Generic;usingSystem.Linq;usingUnityEngine;usingWallstopStudios.UnityHelpers.Core.Attributes;usingWallstopStudios.UnityHelpers.Core.DataStructure.Adapters;usingWallstopStudios.UnityHelpers.Core.Helper;publicclassBehaviorSpawner:MonoBehaviour{[WValueDropDown(typeof(BehaviorSpawner), nameof(GetAllMonoBehaviourNames))]publicSerializableTypebehaviorType;privatevoidSpawnBehavior(){if(behaviorType.IsEmpty){Debug.LogWarning("No behavior type assigned!");return;}Typetype=behaviorType.Value;if(type!=null){GameObjectgo=newGameObject(type.Name);go.AddComponent(type);}}privatestaticIEnumerable<Type>GetAllMonoBehaviourNames(){returnReflectionHelpers.GetAllLoadedTypes().Where(type=>typeof(MonoBehaviour).IsAssignableFrom(type)&&!type.IsAbstract);}}
Problem: Unity doesn't serialize Nullable<T> (e.g., int?, float?) Solution:SerializableNullable<T> wraps any value type with HasValue and Value properties
// Create with valueSerializableNullable<int>nullableInt=newSerializableNullable<int>(42);// Create without value (null)SerializableNullable<int>nullInt=newSerializableNullable<int>();// Checkif(nullableInt.HasValue){intvalue=nullableInt.Value;Debug.Log($"Value: {value}");}// Implicit conversion from TSerializableNullable<float>nullableFloat=3.14f;// Conversion to Nullable<T>int?systemNullable=nullableInt.HasValue?nullableInt.Value:null;
// ✅ GOOD: WGuid for entity IDspublicWGuidentityId=WGuid.NewGuid();// ✅ GOOD: SerializableDictionary for key/value mappingspublicSerializableDictionary<string,GameObject>prefabRegistry;// ✅ GOOD: SerializableHashSet for unique collectionspublicSerializableHashSet<string>uniqueItemIds;// ✅ GOOD: SerializableSortedSet for ordered unique valuespublicSerializableSortedSet<int>scoreThresholds;// ✅ GOOD: SerializableType for type referencespublicSerializableTypebehaviorType;// ✅ GOOD: SerializableNullable for optional valuespublicSerializableNullable<float>overrideSpeed;// ❌ BAD: String-based GUIDpublicstringentityId=System.Guid.NewGuid().ToString();// Use WGuid!// ❌ BAD: Parallel arrays instead of dictionarypublicstring[]keys;publicGameObject[]values;// Use SerializableDictionary!// ❌ BAD: List with manual duplicate checkingpublicList<string>uniqueItems;// Use SerializableHashSet!
// ✅ GOOD: SortedSet for ordered prioritiespublicSerializableSortedSet<int>unlockLevels;// ✅ GOOD: SortedDictionary for ordered displaypublicSerializableSortedDictionary<string,string>alphabeticalNames;// ❌ BAD: HashSet for ordered data (no guaranteed order!)publicSerializableHashSet<int>unlockLevels;// Order is random!
// ✅ GOOD: Generate once, then immutablepublicWGuidentityId=WGuid.NewGuid();// ✅ GOOD: Generate in Awake if neededprivatevoidAwake(){if(entityId==WGuid.EmptyGuid){entityId=WGuid.NewGuid();}}// ❌ BAD: Regenerating on every accesspublicWGuidEntityId=>WGuid.NewGuid();// New GUID every time!
usingUnityEngine;usingWallstopStudios.UnityHelpers.Core.DataStructure.Adapters;publicclassPlayerProfile:MonoBehaviour{publicWGuidplayerId=WGuid.NewGuid();publicSerializableHashSet<string>unlockedAchievements;publicSerializableSortedSet<int>highScores;publicvoidUnlockAchievement(stringachievementId){if(unlockedAchievements.Add(achievementId)){Debug.Log($"Unlocked: {achievementId}");// Trigger UI notification, etc.}}publicvoidRecordScore(intscore){highScores.Add(score);// Keep only top 10while(highScores.Count>10){highScores.Remove(highScores.Min);}}}