Saving instances of Scriptable Objects to prefabs

Thanks, I’ll stick to .assets for this then. Heh, I of course meant the derived classes :wink:

You can save derived ScriptableObject class instances to .asset files.

Unity cannot serialize derived instances of non-ScriptableObject classes that are annotated with the System.SerializableAttribute attribute. For these you cannot serialize by abstract or base references.

i think this should explain and probably solve the problem: Can a ScriptableObject contain a List of ScriptableObjects? - Questions & Answers - Unity Discussions

Necroing this post because I wanted to do exactly that: figure out if it’s possible to save a ScriptableObject asset inside a prefab. Sounds crazy? Well, this is actually what Tilemap palettes do, and that’s how I came to wonder how they did it:

8909367--1219770--upload_2023-3-28_16-46-56.png

Rectangular Palette is a prefab. You can drop it in the scene and it’ll be just like any 2D Tilemap. But it also has this subobject “Palette Settings” which happens to be a GridPalette instance (subclass of ScriptableObject):
8909367--1219773--upload_2023-3-28_16-47-58.png

I wrote a little script that manually creates such a “palette” prefab that shows up correctly in the Tile Palette window. You will have to adjust the path but other than that it should work for everyone:

using UnityEditor;
using UnityEngine;
using UnityEngine.Tilemaps;

public static class TestCreateTilePaletteManually
{
   [MenuItem("Tools/Manually create a TilePalette prefab")]
   public static void Create()
   {
      var grid = new GameObject("grid manually");
      grid.AddComponent<Grid>();

      var layer = new GameObject("layer manually");
      layer.AddComponent<Tilemap>();
      layer.AddComponent<TilemapRenderer>();
      layer.transform.parent = grid.transform;

      var palette = ScriptableObject.CreateInstance<GridPalette>();
      palette.name = "gridpalette settings";

      var path = "Assets/Tilemap/!tests/grid palette prefab.prefab";
      var prefab = PrefabUtility.SaveAsPrefabAsset(grid, path, out var success);
      UnityEngine.Object.DestroyImmediate(grid);

      if (success == false)
         throw new System.Exception("could not create prefab");

      AssetDatabase.AddObjectToAsset(palette, prefab);
      AssetDatabase.SaveAssetIfDirty(prefab);
      
      palette.name += " (modified)";
      AssetDatabase.SaveAssetIfDirty(prefab);

      var loaded = AssetDatabase.LoadAllAssetsAtPath(path);
      foreach (var o in loaded)
         Debug.Log($"loaded: {o} - {o.name} - subasset: {AssetDatabase.IsSubAsset(o)}");

      var loadedSubs = AssetDatabase.LoadAllAssetRepresentationsAtPath(path);
      foreach (var o in loadedSubs)
         Debug.Log($"SUBS loaded: {o} - {o.name} - subasset: {AssetDatabase.IsSubAsset(o)}");
   }
}

Noteworthy:

  • AddObjectToAsset must be followed by SaveAssetIfDirty() otherwise the sub-asset is only added to the in memory representation of the prefab.
  • You can modify the scriptableobject instance but naturally you have to call SaveAssetIfDirty() to persist any changes to the scriptableobject.
  • The asset to save is the main asset, not the scriptableobject sub-asset.
  • LoadAllAssetRepresentationsAtPath() is what I would have expected to be called “Load(Sub)ObjectFromAsset”. You use that to get only the sub-assets inside an asset, ie those added via AddObjectToAsset().
  • Of course you can also enumerate over all assets in a given path and find the sub-assets by calling IsSubAsset().
  • HideFlags do not affect whether the prefab subassets are shown, selectable and editable since they are for scene instances, not assets. There is a m_EditorHideFlags in the YAML but even setting 32 bits manually it did not affect how the scriptableobject appeared in the editor. And yes, I ran Reimport on the prefab asset after that change.

This approach seems very useful since the SO provides:

  • A way for the Tile Palette window to identify prefabs that are valid palettes, and persist information inside the prefab.
  • The SO data is static, which makes sense: you wouldn’t want each instantiated tile on the map to have a copy of the palette data like cell size and sort mode as you would have if GridPalette were a MonoBehaviour in the prefab.
  • The user can still drag & drop the asset into the scene, instantiating it or editing it in prefab mode. The SO and its values do not get lost in the process.
  • The user can still edit the Palette Settings on the prefab by selecting it and editing values in the Inspector.
5 Likes

You can save any asset as a sub-asset of any other asset. If you open the asset in a text editor, you’ll see it’s all just serialised flatly into the one file.

Not necessarily. Like most asset database changes, you need to tell Unity it’s happened with AssetDatabase.Refresh(), which happens when saving anyway.

1 Like

Since it was already bumped I just noticed that Unity had a faulty unity3d.com to unity.com redirection when it comes to their old domain unity3d.com. The Unity answers link in the last post (before the necro) should redirect here, but doesn’t:
Can a ScriptableObject contain a List of ScriptableObjects? - Questions & Answers - Unity Discussions

1 Like

To clarify because this is a pet peeve of mine: NO change made through AssetDatabase methods has to be followed up by AssetDatabase.Refresh(). In fact, doing so is bad practice.

You only ever need to call Refresh() when you made filesystem changes that bypass the AssetDatabase such as calling System.IO methods (CreateDirectory or WriteAllBytes) or running an external tool that modifies/creates assets in the project while the Unity Editor window remains in focus.

2 Likes