Automatically respond to asset creation and deletion events.
The [DetectAssetChanged] attribute allows you to annotate methods that should execute automatically when specific asset types are created or deleted in the Unity Editor. Perfect for cache invalidation, autoconfiguration, validation, and maintaining derived data.
[DetectAssetChanged( Type assetType, // Type of asset to monitor (required) AssetChangeFlags flags, // Created, Deleted, or both (required) DetectAssetChangedOptions options = None // IncludeAssignableTypes for inheritance)]
[Flags]publicenumDetectAssetChangedOptions{None=0,IncludeAssignableTypes=1<<0,// Also trigger for derived typesSearchPrefabs=1<<1,// Search prefabs for MonoBehaviour handlersSearchSceneObjects=1<<2,// Search open scenes for MonoBehaviour handlers}
Important:SearchPrefabs and SearchSceneObjects are only applicable to instance methods on MonoBehaviour classes. Static methods work without these options.
[DetectAssetChanged(typeof(ScriptableObject), AssetChangeFlags.Created)]privatestaticvoidOnScriptableObjectCreated(){Debug.Log("A ScriptableObject was created - invalidate cache");}
When to use: Simple cache invalidation that doesn't need asset details
// Triggers for ScriptableObject and ALL derived types[DetectAssetChanged( typeof(ScriptableObject), AssetChangeFlags.Created, DetectAssetChangedOptions.IncludeAssignableTypes)]privatestaticvoidOnAnyScriptableObjectCreated(ScriptableObjectobj){Debug.Log($"ScriptableObject created: {obj.GetType().Name}");}// Only triggers for exact Material type (not derived classes)[DetectAssetChanged(typeof(Material), AssetChangeFlags.Created)]privatestaticvoidOnExactMaterialCreated(Materialmat){Debug.Log("Material (exact type) created");}
publicsealedclassAssetChangeContext{publicTypeAssetType{get;}// The type being watchedpublicAssetChangeFlagsFlags{get;}// Created, Deleted, or bothpublicIReadOnlyList<string>CreatedAssetPaths{get;}// Paths of created assetspublicIReadOnlyList<string>DeletedAssetPaths{get;}// Paths of deleted assetspublicboolHasCreatedAssets{get;}// True if any createdpublicboolHasDeletedAssets{get;}// True if any deleted}
publicclassSpriteCache:MonoBehaviour{[SerializeField]privateList<Sprite>_cachedSprites=new();[DetectAssetChanged( typeof(Sprite), AssetChangeFlags.Created | AssetChangeFlags.Deleted, DetectAssetChangedOptions.SearchPrefabs )]privatevoidOnSpriteChanged(AssetChangeContextcontext){// This instance method is called on the prefab assetDebug.Log($"SpriteCache on prefab received sprite change: {context.Flags}");RefreshCache();}privatevoidRefreshCache(){_cachedSprites.Clear();// Rebuild cache...}}
When to use: When your MonoBehaviour needs instance-specific state or serialized fields
publicclassLiveAssetWatcher:MonoBehaviour{[SerializeField]privatestring_watchedFolder;[DetectAssetChanged( typeof(Texture2D), AssetChangeFlags.Created, DetectAssetChangedOptions.SearchSceneObjects )]privatevoidOnTextureCreated(AssetChangeContextcontext){// Called on every LiveAssetWatcher instance in all open scenesforeach(stringpathincontext.CreatedAssetPaths){if(path.StartsWith(_watchedFolder)){Debug.Log($"{name} detected new texture: {path}");HandleNewTexture(path);}}}privatevoidHandleNewTexture(stringpath){/* ... */}}
When to use: For editor tools that need to react to changes based on scene-specific configuration
publicclassUniversalAssetHandler:MonoBehaviour{[DetectAssetChanged( typeof(AudioClip), AssetChangeFlags.Created | AssetChangeFlags.Deleted, DetectAssetChangedOptions.SearchPrefabs | DetectAssetChangedOptions.SearchSceneObjects )]privatevoidOnAudioClipChanged(AssetChangeContextcontext){// Called on instances in both prefabs AND scene objectsDebug.Log($"{name} (on {gameObject.name}) received audio change");}}
Performance Note: Searching prefabs and scenes has overhead. Use these options only when you need instance-specific behavior. For simple notifications, prefer static methods.
Step 1 above is an all-types / all-methods reflection scan. Running it inside Unity's import phase destabilizes the asset pipeline (a native crash on some Unity versions, multi-minute importer stalls on others), so the watcher declines to initialize where it has nothing to do:
Context
Watcher initializes
Why
Interactive editor
Yes
This is the authoring workflow the feature exists for
Play mode
No
Authoring concern; a play-mode import would recurse into a scan
Batch mode (-batchmode, CI, headless)
No
No author is present to act on a callback
Override the default from an [InitializeOnLoad] static constructor, so it applies before the watcher's own deferred initialization:
usingUnityEditor;usingWallstopStudios.UnityHelpers.Editor.AssetProcessors;[InitializeOnLoad]internalstaticclassAssetWatcherPolicy{staticAssetWatcherPolicy(){// Opt a headless asset pipeline back in...AssetChangeDetectionUtility.Enabled=true;// ...or keep the watcher off in an interactive editor.AssetChangeDetectionUtility.Enabled=false;// Drop the override and restore the defaults in the table above.AssetChangeDetectionUtility.ResetEnabledToDefault();}}
Turning the watcher off after it has already initialized stops further initialization but leaves already-discovered subscriptions in place.
For a temporary change, use the scope instead of assigning and restoring by hand; it captures the current state on construction and puts it back on dispose, so an early return or an exception cannot leak the override:
A prefab is matched by the type of its main asset, which Unity reports as GameObject. It is never opened to see what it contains, so a watcher on some other type will not fire for it, and neither will a watcher on the type of a sub-asset nested into a .prefab. (Nested sub-assets in .asset files are matched normally.)
That is deliberate. Opening a prefab deserializes every component in it, which runs each one's OnValidate, so your own code runs, on every prefab, on every import, and Unity logs SendMessage cannot be called during Awake, CheckConsistency, or OnValidate for any OnValidate that touches an API it relays. Watching a prefab by what it contains is not supported; watch a GameObject and inspect the prefab yourself if you need it.
SearchPrefabs is a different feature and does not change this: it searches prefabs for instances of the handler's own type, so that a non-static [DetectAssetChanged] method can be invoked on them. It has no effect on which assets match a watcher.
If a callback repeatedly creates more matching asset changes, the watcher enters loop protection and skips additional batches until it is reset. After fixing the callback or clearing the bad state, editor tools can resume dispatch without a domain reload: