├── .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