├── .gitignore ├── CHANGELOG.md ├── CHANGELOG.md.meta ├── Documentation~ └── Images │ ├── Inspector-FruitExample-1.png │ ├── Inspector-FruitExample-2.png │ └── Inspector-FruitExample-3.png ├── Editor.meta ├── Editor ├── EditorScriptableObjectContainerUtility.cs ├── EditorScriptableObjectContainerUtility.cs.meta ├── ScriptableObjectContainerEditor.cs ├── ScriptableObjectContainerEditor.cs.meta ├── ScriptableObjectContainerRenameEditorWindow.cs ├── ScriptableObjectContainerRenameEditorWindow.cs.meta ├── Unity.ScriptableObjectContainer.Editor.asmdef └── Unity.ScriptableObjectContainer.Editor.asmdef.meta ├── LICENSE.md ├── LICENSE.md.meta ├── README.md ├── README.md.meta ├── Runtime.meta ├── Runtime ├── Attributes.meta ├── Attributes │ ├── CreateSubAssetMenuAttribute.cs │ ├── CreateSubAssetMenuAttribute.cs.meta │ ├── DisallowMultipleSubAssetAttribute.cs │ ├── DisallowMultipleSubAssetAttribute.cs.meta │ ├── SubAssetOwnerAttribute.cs │ ├── SubAssetOwnerAttribute.cs.meta │ ├── SubAssetToggleAttribute.cs │ └── SubAssetToggleAttribute.cs.meta ├── ScriptableObjectContainer.cs ├── ScriptableObjectContainer.cs.meta ├── Unity.ScriptableObjectContainer.Runtime.asmdef └── Unity.ScriptableObjectContainer.Runtime.asmdef.meta ├── Tests.meta ├── Tests ├── Editor.meta └── Editor │ ├── Editor Resources.meta │ ├── Editor Resources │ ├── Test_001.asset │ ├── Test_001.asset.meta │ ├── Test_002.asset │ ├── Test_002.asset.meta │ ├── Test_003.asset │ ├── Test_003.asset.meta │ ├── Test_004.asset │ ├── Test_004.asset.meta │ ├── Test_005.asset │ ├── Test_005.asset.meta │ ├── Test_006.asset │ ├── Test_006.asset.meta │ ├── Test_007.asset │ ├── Test_007.asset.meta │ ├── Test_008.asset │ └── Test_008.asset.meta │ ├── Fruit.cs │ ├── Fruit.cs.meta │ ├── FruitContainer.cs │ ├── FruitContainer.cs.meta │ ├── Meat.cs │ ├── Meat.cs.meta │ ├── ScriptableObjectContainerTests.cs │ ├── ScriptableObjectContainerTests.cs.meta │ ├── SingleFruit.cs │ ├── SingleFruit.cs.meta │ ├── SubAssetWithToggle.cs │ ├── SubAssetWithToggle.cs.meta │ ├── Unity.ScriptableObjectContainer.Editor.Tests.asmdef │ └── Unity.ScriptableObjectContainer.Editor.Tests.asmdef.meta ├── package.json └── package.json.meta /.gitignore: -------------------------------------------------------------------------------- 1 | [Aa]rtifacts/ 2 | [Bb]uild/ 3 | [Ll]ibrary/ 4 | [Oo]bj/ 5 | [Tt]emp/ 6 | [Ll]og/ 7 | [Ll]ogs/ 8 | .vs 9 | .vscode 10 | .idea 11 | .DS_Store 12 | *.aspx 13 | *.browser 14 | *.csproj 15 | *.exe 16 | *.ini 17 | *.map 18 | *.mdb 19 | *.npmrc 20 | *.pyc 21 | *.resS 22 | *.sdf 23 | *.sln 24 | *.sublime-project 25 | *.sublime-workspace 26 | *.suo 27 | *.userprefs 28 | .npmrc 29 | *.leu 30 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this package are documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | 8 | ## [1.0.0] - 2023-08-26 9 | - First public preview 10 | -------------------------------------------------------------------------------- /CHANGELOG.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: cd7b06090eb107949a81d6be7506799a 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Documentation~/Images/Inspector-FruitExample-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschraut/UnityScriptableObjectContainer/ed1a56bba8fd8fd4ef0f64a2c696ed4142379c46/Documentation~/Images/Inspector-FruitExample-1.png -------------------------------------------------------------------------------- /Documentation~/Images/Inspector-FruitExample-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschraut/UnityScriptableObjectContainer/ed1a56bba8fd8fd4ef0f64a2c696ed4142379c46/Documentation~/Images/Inspector-FruitExample-2.png -------------------------------------------------------------------------------- /Documentation~/Images/Inspector-FruitExample-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschraut/UnityScriptableObjectContainer/ed1a56bba8fd8fd4ef0f64a2c696ed4142379c46/Documentation~/Images/Inspector-FruitExample-3.png -------------------------------------------------------------------------------- /Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1dd7a9600a950c9479ad0147d58a3c6f 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/EditorScriptableObjectContainerUtility.cs: -------------------------------------------------------------------------------- 1 | // 2 | // ScriptableObject Container for Unity. Copyright (c) 2020-2023 Peter Schraut (www.console-dev.de). See LICENSE.md 3 | // https://github.com/pschraut/UnityScriptableObjectContainer 4 | // 5 | #pragma warning disable IDE0079 // Remove unnecessary suppression 6 | #pragma warning disable IDE0040 // Add accessibility modifiers 7 | #pragma warning disable IDE0051 // Remove unused private members 8 | #pragma warning disable IDE1006 // Naming Styles 9 | using System.Collections.Generic; 10 | using UnityEngine; 11 | using UnityEditor; 12 | using Oddworm.Framework; 13 | using System.Reflection; 14 | 15 | namespace Oddworm.EditorFramework 16 | { 17 | public static class EditorScriptableObjectContainerUtility 18 | { /// 19 | /// Gets all private and public fields in the specified 20 | /// that are decorated with the . 21 | /// 22 | /// The sub-asset. 23 | /// A list of fields decorated with . 24 | public static List GetObjectToggleFields(ScriptableObject subObject) 25 | { 26 | var result = new List(); 27 | var type = subObject.GetType(); 28 | var loopguard = 0; 29 | 30 | do 31 | { 32 | if (++loopguard > 64) 33 | { 34 | Debug.LogError($"Loopguard kicked in, detected more than {loopguard} levels of inheritence?"); 35 | break; 36 | } 37 | 38 | foreach (var fieldInfo in type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) 39 | { 40 | if (fieldInfo.FieldType != typeof(bool)) 41 | continue; 42 | if (fieldInfo.GetCustomAttribute(true) == null) 43 | continue; 44 | 45 | result.Add(fieldInfo); 46 | } 47 | 48 | type = type.BaseType; 49 | } while (type != null && type != typeof(ScriptableObject)); 50 | 51 | return result; 52 | } 53 | 54 | /// 55 | /// Gets if any of the specified value is set to true. 56 | /// 57 | /// The sub-asset. 58 | /// The result of 59 | /// true if any field is true, false otherwise. 60 | public static bool GetObjectToggleValue(ScriptableObject subObject, List toggleFields) 61 | { 62 | foreach (var fieldInfo in toggleFields) 63 | { 64 | if ((bool)fieldInfo.GetValue(subObject)) 65 | return true; 66 | } 67 | 68 | return false; 69 | } 70 | 71 | /// 72 | /// Sets all to the specified . 73 | /// 74 | /// The sub-asset. 75 | /// The result of 76 | /// The value to set all to. 77 | public static void SetObjectToggleValue(ScriptableObject subObject, List toggleFields, bool value) 78 | { 79 | foreach (var fieldInfo in toggleFields) 80 | { 81 | fieldInfo.SetValue(subObject, value); 82 | } 83 | } 84 | 85 | /// 86 | /// Gets whether a sub-asset of the specied can be added to the . 87 | /// 88 | /// The container. 89 | /// The type of the sub-asset. 90 | /// Whether to display an error dialog if the object can't be added. 91 | /// true when it can be added, false otherwise. 92 | /// 93 | /// For example, if the container contains a sub-asset that uses the [], 94 | /// it can't add another sub-asset of the same type. 95 | /// 96 | public static bool CanAddObjectOfType(ScriptableObjectContainer container, System.Type type, bool displayDialog) 97 | { 98 | if (type.IsAbstract) 99 | { 100 | if (displayDialog) 101 | { 102 | var title = $"Can't add object!"; 103 | var message = $"The object of type '{type.Name}' cannot be added, because the type is abstract."; 104 | EditorUtility.DisplayDialog(title, message, "OK"); 105 | } 106 | return false; 107 | } 108 | 109 | if (type.IsGenericType) 110 | { 111 | if (displayDialog) 112 | { 113 | var title = $"Can't add object!"; 114 | var message = $"The object of type '{type.Name}' cannot be added, because the type is a Generic."; 115 | EditorUtility.DisplayDialog(title, message, "OK"); 116 | } 117 | return false; 118 | } 119 | 120 | if (!type.IsSubclassOf(typeof(ScriptableObject))) 121 | { 122 | if (displayDialog) 123 | { 124 | var title = $"Can't add object!"; 125 | var message = $"The object of type '{type.Name}' cannot be added, because it doesn't inherit from '{nameof(ScriptableObject)}'."; 126 | EditorUtility.DisplayDialog(title, message, "OK"); 127 | } 128 | return false; 129 | } 130 | 131 | if (type == typeof(ScriptableObjectContainer) || type.IsSubclassOf(typeof(ScriptableObjectContainer))) 132 | { 133 | if (displayDialog) 134 | { 135 | var title = $"Can't add object!"; 136 | var message = $"The object of type '{type.Name}' cannot be added, because it inherits from '{nameof(ScriptableObjectContainer)}'.\n\nContainer objects can't be nested."; 137 | EditorUtility.DisplayDialog(title, message, "OK"); 138 | } 139 | return false; 140 | } 141 | 142 | var addedObj = container.GetObject(type); 143 | if (addedObj != null) 144 | { 145 | var disallow = GetCustomAttribute(type, typeof(DisallowMultipleSubAssetAttribute)); 146 | if (disallow != null) 147 | { 148 | if (displayDialog) 149 | { 150 | var title = $"Can't add the same object multiple times!"; 151 | var message = $"The object of type '{type.Name}' cannot be added, because '{container.name}' already contains an object of the same type.\n\nRemove the [{nameof(DisallowMultipleSubAssetAttribute)}] from class '{type.Name}' to be able to add multiple objects of the same type."; 152 | EditorUtility.DisplayDialog(title, message, "OK"); 153 | } 154 | 155 | return false; 156 | } 157 | } 158 | 159 | return true; 160 | } 161 | 162 | /// 163 | /// Gets the specified in the specified or any of its base class. 164 | /// 165 | /// The type to look for the attribute. 166 | /// The type of attribute to search for. 167 | /// true to search the types' inheritance chain to find the attributes; otherwise, false. 168 | /// The attribute on success, null otherwise. 169 | static System.Attribute GetCustomAttribute(System.Type type, System.Type attributeType, bool inherit = true) 170 | { 171 | var loopguard = 0; 172 | 173 | do 174 | { 175 | if (++loopguard > 64) 176 | { 177 | Debug.LogError($"Loopguard kicked in, detected more than {loopguard} levels of inheritence?"); 178 | break; 179 | } 180 | 181 | var attribute = type.GetCustomAttribute(attributeType, inherit); 182 | if (attribute != null) 183 | return attribute; 184 | 185 | type = type.BaseType; 186 | } while (type != null && type != typeof(UnityEngine.Object)); 187 | 188 | return null; 189 | } 190 | 191 | /// 192 | /// Moves the specified above the specified . 193 | /// 194 | /// The container. 195 | /// The sub-asset to move in the Inspector above or below another sub-asset. 196 | /// The target sub-asset or null. If null then the moveObject is moved to the bottom of the list. 197 | public static void MoveObject(ScriptableObjectContainer container, ScriptableObject moveObject, ScriptableObject targetObject = null) 198 | { 199 | if (moveObject == targetObject) 200 | return; 201 | 202 | var serContainer = new SerializedObject(container); 203 | serContainer.UpdateIfRequiredOrScript(); 204 | 205 | var subObjProperty = FindObjectsProperty(serContainer); 206 | 207 | // Create a copy of the current m_SubObjects array 208 | var objects = new List(); 209 | for (var n = 0; n < subObjProperty.arraySize; ++n) 210 | { 211 | var element = subObjProperty.GetArrayElementAtIndex(n); 212 | objects.Add(element.objectReferenceValue as ScriptableObject); 213 | } 214 | 215 | objects.Remove(moveObject); 216 | var insertAt = targetObject == null ? -1 : objects.IndexOf(targetObject); 217 | if (insertAt == -1) 218 | insertAt = objects.Count; 219 | objects.Insert(insertAt, moveObject); 220 | 221 | // and we have our new array 222 | subObjProperty.ClearArray(); 223 | for (var n = 0; n < objects.Count; ++n) 224 | { 225 | subObjProperty.InsertArrayElementAtIndex(n); 226 | var element = subObjProperty.GetArrayElementAtIndex(n); 227 | element.objectReferenceValue = objects[n]; 228 | } 229 | 230 | serContainer.ApplyModifiedPropertiesWithoutUndo(); 231 | } 232 | 233 | /// 234 | /// Finds the SerializedProperty that is used to serialize the sub-assets array. 235 | /// 236 | /// The container. 237 | /// The SerializedProperty. 238 | public static SerializedProperty FindObjectsProperty(SerializedObject container) 239 | { 240 | return container.FindProperty("m_SubObjects"); 241 | } 242 | 243 | /// 244 | /// Adds the specified to the specified . 245 | /// 246 | /// The container. 247 | /// The object to add as sub-asset. 248 | /// true on success, false otherwise. 249 | /// 250 | /// In order to remove an object from a container, you would use: 251 | /// UnityEditor.Undo.DestroyObjectImmediate(subObject); 252 | /// EditorScriptableObjectContainerUtility.Sync(container); 253 | /// 254 | public static bool AddObject(ScriptableObjectContainer container, ScriptableObject subObject) 255 | { 256 | if (container == null || subObject == null) 257 | return false; 258 | 259 | if (!CanAddObjectOfType(container, subObject.GetType(), false)) 260 | return false; 261 | 262 | AssetDatabase.AddObjectToAsset(subObject, container); 263 | Sync(container); 264 | 265 | return true; 266 | } 267 | 268 | /// 269 | /// Syncronize the internal sub-objects array with the actual content that Unity manages. 270 | /// 271 | /// The container to syncronize. 272 | /// 273 | /// If you remove a sub-object from a container, the container must update its internal sub-objects array too. 274 | /// If you use UnityEditor.AssetDatabase.RemoveObject, you have to syncronize the container afterwards. Otherwise 275 | /// the container holds a reference to a missing sub-object, the one that was removed. 276 | /// 277 | public static void Sync(ScriptableObjectContainer container) 278 | { 279 | var objs = new List(); 280 | 281 | // load all objects in the container asset 282 | var assetPath = AssetDatabase.GetAssetPath(container); 283 | foreach (var obj in AssetDatabase.LoadAllAssetsAtPath(assetPath)) 284 | { 285 | if (obj is not ScriptableObject) 286 | continue; 287 | if (obj is ScriptableObjectContainer) 288 | continue; 289 | 290 | objs.Add(obj as ScriptableObject); 291 | } 292 | 293 | var serObj = new SerializedObject(container); 294 | serObj.UpdateIfRequiredOrScript(); 295 | 296 | var subObjProperty = FindObjectsProperty(serObj); 297 | 298 | // Create a copy of the current m_SubObjects array 299 | var objects = new List(); 300 | var added = new List(); 301 | for (var n = 0; n < subObjProperty.arraySize; ++n) 302 | { 303 | var element = subObjProperty.GetArrayElementAtIndex(n); 304 | objects.Add(element.objectReferenceValue as ScriptableObject); 305 | } 306 | 307 | // add all objects that are currently not in the m_SubObjects array 308 | foreach (var so in objs) 309 | { 310 | if (objects.IndexOf(so) == -1) 311 | { 312 | objects.Add(so); 313 | added.Add(so); 314 | } 315 | } 316 | 317 | // remove all objects that in the m_SubObjects array, but not in the asset anymore 318 | for (var n = objects.Count - 1; n >= 0; --n) 319 | { 320 | if (objs.IndexOf(objects[n]) == -1) 321 | objects.RemoveAt(n); 322 | } 323 | 324 | // and we have our new array 325 | subObjProperty.ClearArray(); 326 | for (var n = 0; n < objects.Count; ++n) 327 | { 328 | subObjProperty.InsertArrayElementAtIndex(n); 329 | var element = subObjProperty.GetArrayElementAtIndex(n); 330 | element.objectReferenceValue = objects[n]; 331 | 332 | // If this object has just been added to the container, 333 | // expand its view in the Inspector 334 | if (added.IndexOf(objects[n]) != -1) 335 | element.isExpanded = true; 336 | } 337 | 338 | serObj.ApplyModifiedPropertiesWithoutUndo(); 339 | 340 | //container.GetType().GetMethod("OnValidate", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).Invoke(container, null); 341 | } 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /Editor/EditorScriptableObjectContainerUtility.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 73560fd2a7c69454ca4facb1d5c8d825 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/ScriptableObjectContainerEditor.cs: -------------------------------------------------------------------------------- 1 | // 2 | // ScriptableObject Container for Unity. Copyright (c) 2020-2023 Peter Schraut (www.console-dev.de). See LICENSE.md 3 | // https://github.com/pschraut/UnityScriptableObjectContainer 4 | // 5 | #pragma warning disable IDE0079 // Remove unnecessary suppression 6 | #pragma warning disable IDE0040 // Add accessibility modifiers 7 | #pragma warning disable IDE0051 // Remove unused private members 8 | #pragma warning disable IDE1006 // Naming Styles 9 | #pragma warning disable IDE0002 // Name can be simplified 10 | #pragma warning disable IDE0019 // Use Pattern matching 11 | #pragma warning disable IDE0017 // Object initialization can be simplified 12 | #pragma warning disable IDE0062 // Local function can be made static 13 | #pragma warning disable IDE0074 // Use compound assignment 14 | using System.Collections.Generic; 15 | using UnityEngine; 16 | using UnityEditor; 17 | using Oddworm.Framework; 18 | using System.Reflection; 19 | using UnityEditor.Presets; 20 | 21 | namespace Oddworm.EditorFramework 22 | { 23 | [CustomEditor(typeof(ScriptableObjectContainer), editorForChildClasses: true, isFallback = false)] 24 | public class ScriptableObjectContainerEditor : Editor 25 | { 26 | readonly List m_Editors = new(); 27 | Script m_MissingScriptObject; // If a sub-object is null, use the m_MissingScriptObject as object to draw the titlebar 28 | string m_SearchText = ""; 29 | UnityEditor.IMGUI.Controls.SearchField m_SearchField; 30 | Rect m_AddObjectButtonRect; // the layout rect of the "Add Object" button. We use it to display the popup menu at the proper position 31 | 32 | class Script : ScriptableObject { } 33 | 34 | protected virtual void OnEnable() 35 | { 36 | m_SearchText = ""; 37 | m_MissingScriptObject = ScriptableObject.CreateInstance