A Unity package that provides stable [SerializeReference] support through source generation, ensuring your polymorphic serialized references survive refactoring, namespace changes, and type modifications.
- Overview
- Requirements
- Installation
- Features
- How It Works
- Usage
- Advanced Examples
- Comparison with Unity's MoveFromAttribute
- Troubleshooting
- API Reference
- Contributing
- License
Unity's [SerializeReference] attribute enables polymorphic serialization, but it has a critical limitation: serialized references break when you refactor your code. Moving a class to a different namespace, renaming it, or restructuring your type hierarchy can cause Unity to lose track of serialized data, resulting in null references and data loss.
Unity Stable Reference solves this problem by using GUIDs to create permanent, refactoring-safe references. Your serialized data remains intact regardless of how you reorganize your code.
// Original code
namespace MyGame.Characters {
public class Warrior : ICharacter { }
}
// After refactoring - Unity loses the reference!
namespace MyGame.Entities.Characters {
public class WarriorClass : ICharacter { }
}With Unity Stable Reference, your references survive any refactoring:
[Guid("8F9CC34B-A30B-48AB-967C-5B3F0CE0793A"), StableWrapperCodeGen]
public class WarriorClass : ICharacter { }
// GUID ensures the reference is maintained- Unity 2022.3 or newer
- .NET Standard 2.0 compatibility
- Roslyn compiler support (included in Unity 2022.3+)
The package is available on the OpenUPM registry. You can install it via openupm-cli:
openupm add com.fullmetalbagel.unity-stable-reference- Open the Package Manager in Unity (Window → Package Manager)
- Click the "+" button in the top-left corner
- Select "Add package from git URL..."
- Enter the following URL:
https://github.com/quabug/unity-stable-reference.git?path=Packages/com.fullmetalbagel.unity-stable-reference
- Clone or download this repository
- Copy the
Packages/com.fullmetalbagel.unity-stable-referencefolder into your project'sPackagesdirectory
- GUID-based Stability: Uses .NET's
[Guid]attribute to maintain references across refactoring - Source Generation: Zero runtime reflection, all wrapper code is generated at compile-time
- Type Safety: Full IntelliSense support and compile-time type checking
- Inspector Support: Custom property drawers for seamless Unity Editor integration
- Minimal Overhead: Thin wrapper pattern with negligible performance impact
- Interface Support: Works with interfaces, abstract classes, and concrete types
- No Dependencies: Self-contained package with no external dependencies
- Roslyn Analyzer: Provides helpful diagnostics and code fixes for missing GUIDs
Unity Stable Reference uses Roslyn source generators to create stable wrapper classes for your types. When you mark a type with the [StableWrapperCodeGen] attribute and a [Guid] attribute, the system generates specialized wrapper classes that can be reliably serialized by Unity.
-
Import the Generated Stable Wrappers sample:
- Open the Package Manager (Window → Package Manager)
- Find "Unity Stable Reference" in your project packages
- In the package details, expand "Samples"
- Click "Import" next to "Generated Stable Wrappers"
-
Configure your assembly:
- Navigate to
Assets/Samples/Unity Stable Reference/{version}/__StableWrapper__/ - Select the
__StableWrapper__.asmdeffile - In the Inspector, add your project's assembly as a reference
- Click "Apply"
- Navigate to
-
Mark your types with attributes:
using System;
using System.Runtime.InteropServices;
using UnityStableReference;
// The Guid ensures your reference survives refactoring
[Guid("8F9CC34B-A30B-48AB-967C-5B3F0CE0793A"), StableWrapperCodeGen]
public class PlayerData : IGameData
{
public string Name { get; set; }
public int Level { get; set; }
}- Use StableReference in your MonoBehaviours:
using System;
using UnityEngine;
using UnityStableReference;
public class GameManager : MonoBehaviour
{
[SerializeField]
private StableReference<IGameData> _playerData;
void Start()
{
// Create new instance
_playerData = new PlayerData { Name = "Hero", Level = 1 };
// Access the value
if (_playerData.Value != null)
{
Debug.Log($"Player: {_playerData.Value}");
}
}
}Method 1: Assembly-level attribute
Add to any C# file in your assembly (outside of any namespace):
[assembly: UnityStableReference.StableWrapperCodeGen]Then ensure "Auto Referenced" is enabled on your assembly definition.
Method 2: Manual assembly setup
- Create your own wrapper assembly
- Reference both your game assembly and StableReference.Runtime
- Add the assembly-level attribute as shown above
// Define your interface
public interface IWeapon
{
float Damage { get; }
void Attack();
}
// Implement concrete types with GUIDs
[Guid("A1B2C3D4-E5F6-7890-ABCD-EF1234567890"), StableWrapperCodeGen]
public class Sword : IWeapon
{
public float Damage => 10f;
public void Attack() => Debug.Log("Swing!");
}
[Guid("B2C3D4E5-F6A7-8901-BCDE-F12345678901"), StableWrapperCodeGen]
public class Bow : IWeapon
{
public float Damage => 8f;
public void Attack() => Debug.Log("Shoot!");
}
// Use in MonoBehaviour
public class Player : MonoBehaviour
{
[SerializeField] private StableReference<IWeapon> _primaryWeapon;
[SerializeField] private StableReference<IWeapon> _secondaryWeapon;
void Start()
{
_primaryWeapon?.Value?.Attack();
_secondaryWeapon?.Value?.Attack();
}
}public class Inventory : MonoBehaviour
{
// Arrays of stable references
[SerializeField] private StableReference<IItem>[] _items;
// Lists work too
[SerializeField] private List<StableReference<IItem>> _equipment;
// Nested in other serializable classes
[Serializable]
public class ItemSlot
{
public StableReference<IItem> item;
public int quantity;
}
[SerializeField] private ItemSlot[] _slots;
}// Base class
[Guid("C3D4E5F6-A7B8-9012-CDEF-234567890123"), StableWrapperCodeGen]
public abstract class Enemy : IEntity
{
public abstract void Attack();
}
// Derived classes with their own GUIDs
[Guid("D4E5F6A7-B890-1234-DEFA-345678901234"), StableWrapperCodeGen]
public class Goblin : Enemy
{
public override void Attack() => Debug.Log("Goblin attacks!");
}
[Guid("E5F6A7B8-9012-3456-EFAB-456789012345"), StableWrapperCodeGen]
public class Dragon : Enemy
{
public override void Attack() => Debug.Log("Dragon breathes fire!");
}Unity's built-in [MoveFrom] attribute provides an alternative approach to handle serialization issues. Understanding its advantages and disadvantages can help determine when to use this package instead.
- Simple to implement with minimal setup (just add the attribute with a path string)
- Built into Unity with no additional dependencies
- No runtime overhead after deserialization is complete
- Works well for straightforward field renames and moves between parent/child classes
- Handles one-time migrations where the serialized path is known
- Native integration with Unity's serialization system
- Limited to handling field renames and simple relocations
- Cannot maintain references when changing between interface implementations
- Doesn't work well with polymorphic types or significant class restructuring
- Path strings can be error-prone and hard to maintain
- Not designed for ongoing type evolution scenarios
UnityStableReference is ideal when you need more robust reference stability, especially when significant refactoring is anticipated.
Problem: You see warnings about missing Guid attributes on types marked with [StableWrapperCodeGen].
Solution: The package includes a Roslyn analyzer that detects this issue. In Visual Studio or Rider:
- Click on the warning
- Use the quick fix action (lightbulb icon)
- Select "Add Guid attribute" to automatically generate a unique GUID
Alternatively, manually add a Guid:
[Guid("YOUR-UNIQUE-GUID-HERE"), StableWrapperCodeGen]
public class YourClass { }Problem: Your serialized references are lost after refactoring.
Possible causes:
- Missing or changed GUID: Ensure the Guid attribute is present and hasn't been modified
- Assembly not referenced: Check that
__StableWrapper__.asmdefreferences your assembly - Generated code not updated: Try reimporting the package or recompiling
Problem: The type selection dropdown in the Inspector doesn't show your types.
Solutions:
- Ensure your types have both
[Guid]and[StableWrapperCodeGen]attributes - Check that the assembly containing your types is referenced in
__StableWrapper__.asmdef - Verify that Unity has compiled successfully (check Console for errors)
- Try right-clicking the
__StableWrapper__folder and selecting "Reimport"
Problem: Runtime errors about missing wrapper types.
Solution:
- Ensure the
__StableWrapper__assembly is included in your build - Check that "Auto Referenced" is enabled on the assembly definition
- Verify the assembly is not being stripped in build settings
- First-time generation: Initial code generation may take a few seconds for large codebases
- Runtime overhead: Minimal - only a thin wrapper indirection
- Memory usage: Each wrapped instance has a small memory overhead (typically 16-24 bytes)
- Generate GUIDs once: Never change a GUID after serialized data exists
- Version control: Always commit GUID attributes with your types
- Assembly organization: Keep wrapped types in dedicated assemblies for faster compilation
- Testing: Test serialization after major refactoring to ensure references are maintained
Marks a type for stable wrapper generation. Must be used with [Guid].
[Guid("..."), StableWrapperCodeGen]
public class MyClass { }Enables wrapper generation for all marked types in an assembly.
[assembly: UnityStableReference.StableWrapperCodeGen]The main wrapper type for creating stable references.
Properties:
Value: Gets or sets the wrapped objectIsValid: Returns true if the reference contains a non-null value
Methods:
- Implicit conversion to/from
T - Supports
==and!=operators
Example:
StableReference<IMyInterface> stableRef = new MyImplementation();
IMyInterface value = stableRef; // Implicit conversionWe welcome contributions! Please follow these guidelines:
- Fork the repository and create a feature branch
- Write tests for any new functionality
- Follow the existing code style (use the .editorconfig file)
- Update documentation as needed
- Submit a pull request with a clear description
- Clone the repository
- Open the Unity project for testing Unity integration
- Use the
dotnet/UnityStableReference.slnsolution for analyzer development
cd dotnet
dotnet testcd dotnet
dotnet build UnityStableReference/UnityStableReference.csproj
dotnet publish UnityStableReference/UnityStableReference.csproj -c ReleaseThe published DLL will be automatically copied to the Unity package.
This project is licensed under the MIT License - see the LICENSE file for details.