├── Runtime ├── Unity.ScriptableObjectContainer.Runtime.asmdef ├── Attributes.meta ├── Unity.ScriptableObjectContainer.Runtime.asmdef.meta ├── ScriptableObjectContainer.cs.meta ├── Attributes │ ├── SubAssetOwnerAttribute.cs.meta │ ├── CreateSubAssetMenuAttribute.cs.meta │ ├── SubAssetToggleAttribute.cs.meta │ ├── DisallowMultipleSubAssetAttribute.cs.meta │ ├── SubAssetToggleAttribute.cs │ ├── DisallowMultipleSubAssetAttribute.cs │ ├── SubAssetOwnerAttribute.cs │ └── CreateSubAssetMenuAttribute.cs └── ScriptableObjectContainer.cs ├── Documentation~ └── Images │ ├── Inspector-FruitExample-1.png │ ├── Inspector-FruitExample-2.png │ └── Inspector-FruitExample-3.png ├── CHANGELOG.md.meta ├── LICENSE.md.meta ├── README.md.meta ├── package.json.meta ├── Editor.meta ├── Runtime.meta ├── Tests.meta ├── Tests ├── Editor.meta └── Editor │ ├── Editor Resources.meta │ ├── Unity.ScriptableObjectContainer.Editor.Tests.asmdef.meta │ ├── Editor Resources │ ├── Test_001.asset.meta │ ├── Test_002.asset.meta │ ├── Test_003.asset.meta │ ├── Test_004.asset.meta │ ├── Test_005.asset.meta │ ├── Test_006.asset.meta │ ├── Test_007.asset.meta │ ├── Test_008.asset.meta │ ├── Test_005.asset │ ├── Test_004.asset │ ├── Test_001.asset │ ├── Test_006.asset │ ├── Test_007.asset │ ├── Test_008.asset │ ├── Test_002.asset │ └── Test_003.asset │ ├── Fruit.cs.meta │ ├── Meat.cs.meta │ ├── SingleFruit.cs.meta │ ├── FruitContainer.cs.meta │ ├── SubAssetWithToggle.cs.meta │ ├── ScriptableObjectContainerTests.cs.meta │ ├── Unity.ScriptableObjectContainer.Editor.Tests.asmdef │ ├── FruitContainer.cs │ ├── Fruit.cs │ ├── Meat.cs │ ├── SingleFruit.cs │ ├── SubAssetWithToggle.cs │ └── ScriptableObjectContainerTests.cs ├── Editor ├── Unity.ScriptableObjectContainer.Editor.asmdef.meta ├── ScriptableObjectContainerEditor.cs.meta ├── EditorScriptableObjectContainerUtility.cs.meta ├── ScriptableObjectContainerRenameEditorWindow.cs.meta ├── Unity.ScriptableObjectContainer.Editor.asmdef ├── ScriptableObjectContainerRenameEditorWindow.cs ├── EditorScriptableObjectContainerUtility.cs └── ScriptableObjectContainerEditor.cs ├── CHANGELOG.md ├── .gitignore ├── package.json ├── LICENSE.md └── README.md /Runtime/Unity.ScriptableObjectContainer.Runtime.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Unity.ScriptableObjectContainer.Runtime" 3 | } 4 | -------------------------------------------------------------------------------- /Documentation~/Images/Inspector-FruitExample-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschraut/UnityScriptableObjectContainer/HEAD/Documentation~/Images/Inspector-FruitExample-1.png -------------------------------------------------------------------------------- /Documentation~/Images/Inspector-FruitExample-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschraut/UnityScriptableObjectContainer/HEAD/Documentation~/Images/Inspector-FruitExample-2.png -------------------------------------------------------------------------------- /Documentation~/Images/Inspector-FruitExample-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschraut/UnityScriptableObjectContainer/HEAD/Documentation~/Images/Inspector-FruitExample-3.png -------------------------------------------------------------------------------- /CHANGELOG.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: cd7b06090eb107949a81d6be7506799a 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /LICENSE.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 9f43c535946f2b74a9b12398b1c52e0e 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 709827b2b0cd9b04095b39ae99acb1e7 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /package.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c50ade3cd1e61b949aca77e39ad0b364 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1dd7a9600a950c9479ad0147d58a3c6f 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Runtime.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: dc780965a7037d44d8afb8c5aacc1b78 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Tests.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b8dd0f57137997b47aab2121d9862d9c 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Tests/Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ac029501e15974e4f81a905832c42f6a 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Runtime/Attributes.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 29c27a4dc5a407a48ae60c176c8b8169 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Tests/Editor/Editor Resources.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f07a2a99328f0c74fa791bb5d36d1960 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/Unity.ScriptableObjectContainer.Editor.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e6711ee95ee562f4499d2c95c4dadef2 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Runtime/Unity.ScriptableObjectContainer.Runtime.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f60f765a85ae24643aff9de06832a458 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Tests/Editor/Unity.ScriptableObjectContainer.Editor.Tests.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d0cfb8bf5ccf62b41aa338806f6c9630 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Tests/Editor/Editor Resources/Test_001.asset.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: dc7a714e8fdc8014393cd8a7641cf954 3 | NativeFormatImporter: 4 | externalObjects: {} 5 | mainObjectFileID: 11400000 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Tests/Editor/Editor Resources/Test_002.asset.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d4de0e16641e83b4490b38c8e190e829 3 | NativeFormatImporter: 4 | externalObjects: {} 5 | mainObjectFileID: 11400000 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Tests/Editor/Editor Resources/Test_003.asset.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 283c4d8dfda39914db6d0a56dec98a98 3 | NativeFormatImporter: 4 | externalObjects: {} 5 | mainObjectFileID: 11400000 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Tests/Editor/Editor Resources/Test_004.asset.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c4f6acf66ba68414b9393d08ea1a819c 3 | NativeFormatImporter: 4 | externalObjects: {} 5 | mainObjectFileID: 11400000 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Tests/Editor/Editor Resources/Test_005.asset.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: eaf655f2cebb65a4f9e63dab5a41c36c 3 | NativeFormatImporter: 4 | externalObjects: {} 5 | mainObjectFileID: 11400000 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Tests/Editor/Editor Resources/Test_006.asset.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 139dc730aefebd145bfa5da9335b21d0 3 | NativeFormatImporter: 4 | externalObjects: {} 5 | mainObjectFileID: 11400000 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Tests/Editor/Editor Resources/Test_007.asset.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 17a5657cc184a9144a4f6e0323c65fd8 3 | NativeFormatImporter: 4 | externalObjects: {} 5 | mainObjectFileID: 11400000 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Tests/Editor/Editor Resources/Test_008.asset.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0772d2bfc1456b248bd23d9a7c2da7d3 3 | NativeFormatImporter: 4 | externalObjects: {} 5 | mainObjectFileID: 11400000 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Tests/Editor/Fruit.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1360f0b0a64f16b4bb8a81fabdfb3d00 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Tests/Editor/Meat.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 02cc9ca69e7435549a7e93d90b79198d 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Tests/Editor/SingleFruit.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ec43497dff0d929469b5c456bb1a5bcc 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Tests/Editor/FruitContainer.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ac9dad67ca91bfb4a956cdf7f6473a2b 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Tests/Editor/SubAssetWithToggle.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: de0186abe4dc5ce47a14f83bad33e571 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/ScriptableObjectContainer.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 4186eab6ae73578419a08549f3290894 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.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 335dc800861814146a8d1a039d7c2506 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/Attributes/SubAssetOwnerAttribute.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b04b635091a85a94d9d42c0d2f18d718 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Runtime/Attributes/CreateSubAssetMenuAttribute.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1fd43ffb69491954f877fe4999e085e7 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/Attributes/SubAssetToggleAttribute.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d33256029f65d534d9b66b72cb4fcc15 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Tests/Editor/ScriptableObjectContainerTests.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c07897e71c9046b44a3e3cd5c0efabb7 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Editor/ScriptableObjectContainerRenameEditorWindow.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5d57d85dc3af6e34db772a1ed86a45da 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/Attributes/DisallowMultipleSubAssetAttribute.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e0de1f7c7d03ea54fa31c2694838d5ec 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /Tests/Editor/Editor Resources/Test_005.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!114 &11400000 4 | MonoBehaviour: 5 | m_ObjectHideFlags: 0 6 | m_CorrespondingSourceObject: {fileID: 0} 7 | m_PrefabInstance: {fileID: 0} 8 | m_PrefabAsset: {fileID: 0} 9 | m_GameObject: {fileID: 0} 10 | m_Enabled: 1 11 | m_EditorHideFlags: 0 12 | m_Script: {fileID: 11500000, guid: 4186eab6ae73578419a08549f3290894, type: 3} 13 | m_Name: Test_005 14 | m_EditorClassIdentifier: 15 | m_SubObjects: [] 16 | -------------------------------------------------------------------------------- /Editor/Unity.ScriptableObjectContainer.Editor.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Unity.ScriptableObjectContainer.Editor", 3 | "references": [ 4 | "GUID:f60f765a85ae24643aff9de06832a458" 5 | ], 6 | "includePlatforms": [ 7 | "Editor" 8 | ], 9 | "excludePlatforms": [], 10 | "allowUnsafeCode": false, 11 | "overrideReferences": false, 12 | "precompiledReferences": [], 13 | "autoReferenced": true, 14 | "defineConstraints": [], 15 | "versionDefines": [], 16 | "noEngineReferences": false 17 | } -------------------------------------------------------------------------------- /Tests/Editor/Unity.ScriptableObjectContainer.Editor.Tests.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Unity.ScriptableObjectContainer.Editor.Tests", 3 | "references": [ 4 | "GUID:f60f765a85ae24643aff9de06832a458", 5 | "GUID:e6711ee95ee562f4499d2c95c4dadef2" 6 | ], 7 | "optionalUnityReferences": [ 8 | "TestAssemblies" 9 | ], 10 | "includePlatforms": [ 11 | "Editor" 12 | ], 13 | "excludePlatforms": [], 14 | "allowUnsafeCode": false, 15 | "overrideReferences": false, 16 | "precompiledReferences": [], 17 | "autoReferenced": true, 18 | "defineConstraints": [], 19 | "versionDefines": [], 20 | "noEngineReferences": false 21 | } -------------------------------------------------------------------------------- /Runtime/Attributes/SubAssetToggleAttribute.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; 10 | 11 | namespace Oddworm.Framework 12 | { 13 | [AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)] 14 | public sealed class SubAssetToggleAttribute : Attribute 15 | { 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Tests/Editor/FruitContainer.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 | using UnityEngine; 9 | using Oddworm.Framework; 10 | 11 | namespace Oddworm.EditorFramework.Tests.ScriptableObjectContainerTest 12 | { 13 | #if SCRIPTABLEOBJECTCONTAINER_ENABLE_TESTS 14 | [CreateAssetMenu(menuName = "ScriptableObjectContainer Tests/Fruit Container")] 15 | #endif 16 | internal class FruitContainer : ScriptableObjectContainer 17 | { 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Tests/Editor/Fruit.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 | using UnityEngine; 9 | using Oddworm.Framework; 10 | 11 | namespace Oddworm.EditorFramework.Tests.ScriptableObjectContainerTest 12 | { 13 | #if SCRIPTABLEOBJECTCONTAINER_ENABLE_TESTS 14 | [CreateSubAssetMenu(typeof(FruitContainer), menuName = "ScriptableObjectContainer Tests/Fruit")] 15 | #endif 16 | internal class Fruit : ScriptableObject 17 | { 18 | public int number; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Tests/Editor/Meat.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 | using UnityEngine; 9 | using Oddworm.Framework; 10 | 11 | namespace Oddworm.EditorFramework.Tests.ScriptableObjectContainerTest 12 | { 13 | #if SCRIPTABLEOBJECTCONTAINER_ENABLE_TESTS 14 | [CreateSubAssetMenu(typeof(ScriptableObjectContainer), menuName = "ScriptableObjectContainer Tests/Meat")] 15 | #endif 16 | internal class Meat : ScriptableObject 17 | { 18 | public int number; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Tests/Editor/SingleFruit.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 | using UnityEngine; 9 | using Oddworm.Framework; 10 | 11 | namespace Oddworm.EditorFramework.Tests.ScriptableObjectContainerTest 12 | { 13 | #if SCRIPTABLEOBJECTCONTAINER_ENABLE_TESTS 14 | [CreateSubAssetMenu(typeof(ScriptableObjectContainer), menuName = "ScriptableObjectContainer Tests/Single Fruit")] 15 | #endif 16 | [DisallowMultipleSubAsset] 17 | internal class SingleFruit : ScriptableObject 18 | { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Tests/Editor/SubAssetWithToggle.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 | using UnityEngine; 9 | using Oddworm.Framework; 10 | 11 | namespace Oddworm.EditorFramework.Tests.ScriptableObjectContainerTest 12 | { 13 | #if SCRIPTABLEOBJECTCONTAINER_ENABLE_TESTS 14 | [CreateSubAssetMenu(typeof(ScriptableObjectContainer), menuName = "ScriptableObjectContainer Tests/SubAsset With Toggle")] 15 | #endif 16 | internal class SubAssetWithToggle : ScriptableObject 17 | { 18 | [SubAssetToggle] 19 | [SerializeField] bool m_MyToggle; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Runtime/Attributes/DisallowMultipleSubAssetAttribute.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; 10 | 11 | namespace Oddworm.Framework 12 | { 13 | /// 14 | /// Prevents ScriptableObjects of same type (or subtype) to be added more than once to a . 15 | /// 16 | [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] 17 | public sealed class DisallowMultipleSubAssetAttribute : Attribute 18 | { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Tests/Editor/Editor Resources/Test_004.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!114 &-3186156268429412971 4 | MonoBehaviour: 5 | m_ObjectHideFlags: 0 6 | m_CorrespondingSourceObject: {fileID: 0} 7 | m_PrefabInstance: {fileID: 0} 8 | m_PrefabAsset: {fileID: 0} 9 | m_GameObject: {fileID: 0} 10 | m_Enabled: 1 11 | m_EditorHideFlags: 0 12 | m_Script: {fileID: 11500000, guid: ec43497dff0d929469b5c456bb1a5bcc, type: 3} 13 | m_Name: SingleFruit 14 | m_EditorClassIdentifier: 15 | --- !u!114 &11400000 16 | MonoBehaviour: 17 | m_ObjectHideFlags: 0 18 | m_CorrespondingSourceObject: {fileID: 0} 19 | m_PrefabInstance: {fileID: 0} 20 | m_PrefabAsset: {fileID: 0} 21 | m_GameObject: {fileID: 0} 22 | m_Enabled: 1 23 | m_EditorHideFlags: 0 24 | m_Script: {fileID: 11500000, guid: 4186eab6ae73578419a08549f3290894, type: 3} 25 | m_Name: Test_004 26 | m_EditorClassIdentifier: 27 | m_SubObjects: 28 | - {fileID: -3186156268429412971} 29 | -------------------------------------------------------------------------------- /Tests/Editor/Editor Resources/Test_001.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!114 &-2245045354878169941 4 | MonoBehaviour: 5 | m_ObjectHideFlags: 0 6 | m_CorrespondingSourceObject: {fileID: 0} 7 | m_PrefabInstance: {fileID: 0} 8 | m_PrefabAsset: {fileID: 0} 9 | m_GameObject: {fileID: 0} 10 | m_Enabled: 1 11 | m_EditorHideFlags: 0 12 | m_Script: {fileID: 11500000, guid: 1360f0b0a64f16b4bb8a81fabdfb3d00, type: 3} 13 | m_Name: Fruit 14 | m_EditorClassIdentifier: 15 | number: 0 16 | --- !u!114 &11400000 17 | MonoBehaviour: 18 | m_ObjectHideFlags: 0 19 | m_CorrespondingSourceObject: {fileID: 0} 20 | m_PrefabInstance: {fileID: 0} 21 | m_PrefabAsset: {fileID: 0} 22 | m_GameObject: {fileID: 0} 23 | m_Enabled: 1 24 | m_EditorHideFlags: 0 25 | m_Script: {fileID: 11500000, guid: ac9dad67ca91bfb4a956cdf7f6473a2b, type: 3} 26 | m_Name: Test_001 27 | m_EditorClassIdentifier: 28 | m_SubObjects: 29 | - {fileID: -2245045354878169941} 30 | -------------------------------------------------------------------------------- /Tests/Editor/Editor Resources/Test_006.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!114 &11400000 4 | MonoBehaviour: 5 | m_ObjectHideFlags: 0 6 | m_CorrespondingSourceObject: {fileID: 0} 7 | m_PrefabInstance: {fileID: 0} 8 | m_PrefabAsset: {fileID: 0} 9 | m_GameObject: {fileID: 0} 10 | m_Enabled: 1 11 | m_EditorHideFlags: 0 12 | m_Script: {fileID: 11500000, guid: 4186eab6ae73578419a08549f3290894, type: 3} 13 | m_Name: Test_006 14 | m_EditorClassIdentifier: 15 | m_SubObjects: 16 | - {fileID: 1793690072751849465} 17 | --- !u!114 &1793690072751849465 18 | MonoBehaviour: 19 | m_ObjectHideFlags: 0 20 | m_CorrespondingSourceObject: {fileID: 0} 21 | m_PrefabInstance: {fileID: 0} 22 | m_PrefabAsset: {fileID: 0} 23 | m_GameObject: {fileID: 0} 24 | m_Enabled: 1 25 | m_EditorHideFlags: 0 26 | m_Script: {fileID: 11500000, guid: de0186abe4dc5ce47a14f83bad33e571, type: 3} 27 | m_Name: SubAssetWithToggle 28 | m_EditorClassIdentifier: 29 | m_MyToggle: 1 30 | -------------------------------------------------------------------------------- /Tests/Editor/Editor Resources/Test_007.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!114 &11400000 4 | MonoBehaviour: 5 | m_ObjectHideFlags: 0 6 | m_CorrespondingSourceObject: {fileID: 0} 7 | m_PrefabInstance: {fileID: 0} 8 | m_PrefabAsset: {fileID: 0} 9 | m_GameObject: {fileID: 0} 10 | m_Enabled: 1 11 | m_EditorHideFlags: 0 12 | m_Script: {fileID: 11500000, guid: 4186eab6ae73578419a08549f3290894, type: 3} 13 | m_Name: Test_007 14 | m_EditorClassIdentifier: 15 | m_SubObjects: 16 | - {fileID: 1793690072751849465} 17 | --- !u!114 &1793690072751849465 18 | MonoBehaviour: 19 | m_ObjectHideFlags: 0 20 | m_CorrespondingSourceObject: {fileID: 0} 21 | m_PrefabInstance: {fileID: 0} 22 | m_PrefabAsset: {fileID: 0} 23 | m_GameObject: {fileID: 0} 24 | m_Enabled: 1 25 | m_EditorHideFlags: 0 26 | m_Script: {fileID: 11500000, guid: de0186abe4dc5ce47a14f83bad33e571, type: 3} 27 | m_Name: SubAssetWithToggle 28 | m_EditorClassIdentifier: 29 | m_MyToggle: 0 30 | -------------------------------------------------------------------------------- /Tests/Editor/Editor Resources/Test_008.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!114 &11400000 4 | MonoBehaviour: 5 | m_ObjectHideFlags: 0 6 | m_CorrespondingSourceObject: {fileID: 0} 7 | m_PrefabInstance: {fileID: 0} 8 | m_PrefabAsset: {fileID: 0} 9 | m_GameObject: {fileID: 0} 10 | m_Enabled: 1 11 | m_EditorHideFlags: 0 12 | m_Script: {fileID: 11500000, guid: 4186eab6ae73578419a08549f3290894, type: 3} 13 | m_Name: Test_008 14 | m_EditorClassIdentifier: 15 | m_SubObjects: 16 | - {fileID: 1793690072751849465} 17 | --- !u!114 &1793690072751849465 18 | MonoBehaviour: 19 | m_ObjectHideFlags: 0 20 | m_CorrespondingSourceObject: {fileID: 0} 21 | m_PrefabInstance: {fileID: 0} 22 | m_PrefabAsset: {fileID: 0} 23 | m_GameObject: {fileID: 0} 24 | m_Enabled: 1 25 | m_EditorHideFlags: 0 26 | m_Script: {fileID: 11500000, guid: de0186abe4dc5ce47a14f83bad33e571, type: 3} 27 | m_Name: SubAssetWithToggle 28 | m_EditorClassIdentifier: 29 | m_MyToggle: 0 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.oddworm.scriptableobjectcontainer", 3 | "version": "1.0.0", 4 | "displayName": "ScriptableObject Container", 5 | "description": "The ScriptableObject Container package provides the ability to nest ScriptableObjects and work with them in a similar manner as Components.\n\nYou can add a ScriptableObject to another ScriptableObject Container just like how you add a Component to a GameObject.\n\nOn the scripting side, you retrieve ScriptableObjects from the Container in a similar way to how you retrieve Components from a GameObject.", 6 | "unity": "2021.3", 7 | "documentationUrl": "https://github.com/pschraut/UnityScriptableObjectContainer", 8 | "dependencies": { 9 | }, 10 | "keywords": [ 11 | "unity", 12 | "debug" 13 | ], 14 | "author": { 15 | "name": "Peter Schraut", 16 | "url": "http://console-dev.de" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/pschraut/UnityScriptableObjectContainer.git" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2023 Peter Schraut (http://www.console-dev.de) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Runtime/Attributes/SubAssetOwnerAttribute.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; 10 | 11 | namespace Oddworm.Framework 12 | { 13 | /// 14 | /// Use the [] on a field in your sub-asset to let the editor 15 | /// automatically assign a reference to its owner, the . 16 | /// 17 | /// 18 | /// 19 | /// using UnityEngine; 20 | /// using Oddworm.Framework; 21 | /// 22 | /// [CreateSubAssetMenu(menuName = "Example")] 23 | /// public class Example : ScriptableObject 24 | /// { 25 | /// [SubAssetOwner] 26 | /// [SerializeField] ScriptableObjectContainer m_Container; 27 | /// } 28 | /// 29 | /// 30 | [AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)] 31 | public sealed class SubAssetOwnerAttribute : Attribute 32 | { 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Tests/Editor/Editor Resources/Test_002.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!114 &-5834706919153656172 4 | MonoBehaviour: 5 | m_ObjectHideFlags: 0 6 | m_CorrespondingSourceObject: {fileID: 0} 7 | m_PrefabInstance: {fileID: 0} 8 | m_PrefabAsset: {fileID: 0} 9 | m_GameObject: {fileID: 0} 10 | m_Enabled: 1 11 | m_EditorHideFlags: 0 12 | m_Script: {fileID: 11500000, guid: 1360f0b0a64f16b4bb8a81fabdfb3d00, type: 3} 13 | m_Name: Fruit 14 | m_EditorClassIdentifier: 15 | number: 2 16 | --- !u!114 &-2245045354878169941 17 | MonoBehaviour: 18 | m_ObjectHideFlags: 0 19 | m_CorrespondingSourceObject: {fileID: 0} 20 | m_PrefabInstance: {fileID: 0} 21 | m_PrefabAsset: {fileID: 0} 22 | m_GameObject: {fileID: 0} 23 | m_Enabled: 1 24 | m_EditorHideFlags: 0 25 | m_Script: {fileID: 11500000, guid: 1360f0b0a64f16b4bb8a81fabdfb3d00, type: 3} 26 | m_Name: Fruit 27 | m_EditorClassIdentifier: 28 | number: 0 29 | --- !u!114 &11400000 30 | MonoBehaviour: 31 | m_ObjectHideFlags: 0 32 | m_CorrespondingSourceObject: {fileID: 0} 33 | m_PrefabInstance: {fileID: 0} 34 | m_PrefabAsset: {fileID: 0} 35 | m_GameObject: {fileID: 0} 36 | m_Enabled: 1 37 | m_EditorHideFlags: 0 38 | m_Script: {fileID: 11500000, guid: ac9dad67ca91bfb4a956cdf7f6473a2b, type: 3} 39 | m_Name: Test_002 40 | m_EditorClassIdentifier: 41 | m_SubObjects: 42 | - {fileID: -2245045354878169941} 43 | - {fileID: 5518851017741398026} 44 | - {fileID: -5834706919153656172} 45 | --- !u!114 &5518851017741398026 46 | MonoBehaviour: 47 | m_ObjectHideFlags: 0 48 | m_CorrespondingSourceObject: {fileID: 0} 49 | m_PrefabInstance: {fileID: 0} 50 | m_PrefabAsset: {fileID: 0} 51 | m_GameObject: {fileID: 0} 52 | m_Enabled: 1 53 | m_EditorHideFlags: 0 54 | m_Script: {fileID: 11500000, guid: 1360f0b0a64f16b4bb8a81fabdfb3d00, type: 3} 55 | m_Name: Fruit 56 | m_EditorClassIdentifier: 57 | number: 1 58 | -------------------------------------------------------------------------------- /Runtime/Attributes/CreateSubAssetMenuAttribute.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; 10 | 11 | namespace Oddworm.Framework 12 | { 13 | /// 14 | /// Mark a -derived type to be automatically listed in the "Add Object" submenu, 15 | /// so that instances of the type can be easily created and added to the particular . 16 | /// 17 | [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] 18 | public sealed class CreateSubAssetMenuAttribute : Attribute 19 | { 20 | /// 21 | /// The type where the menu item is being created at. 22 | /// 23 | public Type type 24 | { 25 | get; 26 | private set; 27 | } 28 | 29 | /// 30 | /// Whether to add the menu item to sub-classes of instances that are of the specified . 31 | /// 32 | public bool allowSubClass 33 | { 34 | get; 35 | set; 36 | } 37 | 38 | /// 39 | /// Whether to create a menu item for the specifically. It's useful when you add 40 | /// the to a base-class where you want all sub-classes to 41 | /// have menu items generated for, except for the base-class. 42 | /// 43 | public bool excludeThisClass 44 | { 45 | get; 46 | set; 47 | } 48 | 49 | /// 50 | /// The display name for this type shown in the "Add Object" menu. 51 | /// If is empty, it uses the type name instead. 52 | /// 53 | /// 54 | /// As with other menu item code, use a forward-slash ("/") to group items into submenus. 55 | /// For example, specifying "Gameplay/Objective" as will cause the menu item 56 | /// 'Objective' to be inside a 'Gameplay' submenu of the "Add Object" menu. 57 | /// 58 | public string menuName 59 | { 60 | get; 61 | set; 62 | } 63 | 64 | public CreateSubAssetMenuAttribute(Type containerType) 65 | : base() 66 | { 67 | this.type = containerType; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Editor/ScriptableObjectContainerRenameEditorWindow.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 UnityEngine; 10 | using UnityEditor; 11 | 12 | namespace Oddworm.EditorFramework 13 | { 14 | internal class ScriptableObjectContainerRenameEditorWindow : EditorWindow 15 | { 16 | string m_NewText; 17 | Object m_ObjectToRename; 18 | bool m_FirstUpdate; 19 | 20 | public void Show(Object objectToRename) 21 | { 22 | m_ObjectToRename = objectToRename; 23 | m_NewText = m_ObjectToRename.name; 24 | m_FirstUpdate = true; 25 | 26 | ShowModalUtility(); 27 | } 28 | 29 | void OnEnable() 30 | { 31 | titleContent = new GUIContent("Rename"); 32 | minSize = new Vector2(300, 120); 33 | maxSize = new Vector2(300, 120); 34 | } 35 | 36 | void OnGUI() 37 | { 38 | var cancel = false; 39 | var rename = false; 40 | 41 | var e = Event.current; 42 | if (e.type == EventType.KeyDown && e.keyCode == KeyCode.Escape) 43 | cancel = true; 44 | 45 | if (e.type == EventType.KeyDown && e.keyCode == KeyCode.Return) 46 | rename = !string.IsNullOrEmpty(m_NewText); 47 | 48 | EditorGUILayout.Space(); 49 | GUI.SetNextControlName("RenameField"); 50 | m_NewText = EditorGUILayout.TextField(m_NewText); 51 | if (m_FirstUpdate && e.type == EventType.Layout) 52 | { 53 | m_FirstUpdate = false; 54 | GUI.FocusControl("RenameField"); 55 | EditorGUI.FocusTextInControl("RenameField"); 56 | } 57 | EditorGUILayout.Space(); 58 | 59 | EditorGUILayout.HelpBox("Press Return to rename or Escape to cancel.", MessageType.Info, true); 60 | EditorGUILayout.Space(); 61 | GUILayout.FlexibleSpace(); 62 | 63 | EditorGUILayout.BeginHorizontal(); 64 | GUILayout.FlexibleSpace(); 65 | 66 | EditorGUI.BeginDisabledGroup(string.IsNullOrEmpty(m_NewText)); 67 | if (GUILayout.Button(new GUIContent("Rename", "Return")) || rename) 68 | { 69 | if (m_ObjectToRename != null && m_ObjectToRename.name != m_NewText) 70 | { 71 | Undo.RecordObject(m_ObjectToRename, "Rename"); 72 | m_ObjectToRename.name = m_NewText; 73 | } 74 | Close(); 75 | } 76 | EditorGUI.EndDisabledGroup(); 77 | 78 | if (GUILayout.Button(new GUIContent("Cancel", "Escape")) || cancel) 79 | Close(); 80 | 81 | EditorGUILayout.EndHorizontal(); 82 | EditorGUILayout.Space(); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Tests/Editor/Editor Resources/Test_003.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!114 &-5834706919153656172 4 | MonoBehaviour: 5 | m_ObjectHideFlags: 0 6 | m_CorrespondingSourceObject: {fileID: 0} 7 | m_PrefabInstance: {fileID: 0} 8 | m_PrefabAsset: {fileID: 0} 9 | m_GameObject: {fileID: 0} 10 | m_Enabled: 1 11 | m_EditorHideFlags: 0 12 | m_Script: {fileID: 11500000, guid: 1360f0b0a64f16b4bb8a81fabdfb3d00, type: 3} 13 | m_Name: Fruit 14 | m_EditorClassIdentifier: 15 | number: 2 16 | --- !u!114 &-2245045354878169941 17 | MonoBehaviour: 18 | m_ObjectHideFlags: 0 19 | m_CorrespondingSourceObject: {fileID: 0} 20 | m_PrefabInstance: {fileID: 0} 21 | m_PrefabAsset: {fileID: 0} 22 | m_GameObject: {fileID: 0} 23 | m_Enabled: 1 24 | m_EditorHideFlags: 0 25 | m_Script: {fileID: 11500000, guid: 1360f0b0a64f16b4bb8a81fabdfb3d00, type: 3} 26 | m_Name: Fruit 27 | m_EditorClassIdentifier: 28 | number: 0 29 | --- !u!114 &11400000 30 | MonoBehaviour: 31 | m_ObjectHideFlags: 0 32 | m_CorrespondingSourceObject: {fileID: 0} 33 | m_PrefabInstance: {fileID: 0} 34 | m_PrefabAsset: {fileID: 0} 35 | m_GameObject: {fileID: 0} 36 | m_Enabled: 1 37 | m_EditorHideFlags: 0 38 | m_Script: {fileID: 11500000, guid: ac9dad67ca91bfb4a956cdf7f6473a2b, type: 3} 39 | m_Name: Test_003 40 | m_EditorClassIdentifier: 41 | m_SubObjects: 42 | - {fileID: -2245045354878169941} 43 | - {fileID: 256508848403889015} 44 | - {fileID: 5518851017741398026} 45 | - {fileID: 2869424647571882560} 46 | - {fileID: -5834706919153656172} 47 | - {fileID: 3674869790121034100} 48 | --- !u!114 &256508848403889015 49 | MonoBehaviour: 50 | m_ObjectHideFlags: 0 51 | m_CorrespondingSourceObject: {fileID: 0} 52 | m_PrefabInstance: {fileID: 0} 53 | m_PrefabAsset: {fileID: 0} 54 | m_GameObject: {fileID: 0} 55 | m_Enabled: 1 56 | m_EditorHideFlags: 0 57 | m_Script: {fileID: 11500000, guid: 02cc9ca69e7435549a7e93d90b79198d, type: 3} 58 | m_Name: Meat 59 | m_EditorClassIdentifier: 60 | number: 0 61 | --- !u!114 &2869424647571882560 62 | MonoBehaviour: 63 | m_ObjectHideFlags: 0 64 | m_CorrespondingSourceObject: {fileID: 0} 65 | m_PrefabInstance: {fileID: 0} 66 | m_PrefabAsset: {fileID: 0} 67 | m_GameObject: {fileID: 0} 68 | m_Enabled: 1 69 | m_EditorHideFlags: 0 70 | m_Script: {fileID: 11500000, guid: 02cc9ca69e7435549a7e93d90b79198d, type: 3} 71 | m_Name: Meat 72 | m_EditorClassIdentifier: 73 | number: 1 74 | --- !u!114 &3674869790121034100 75 | MonoBehaviour: 76 | m_ObjectHideFlags: 0 77 | m_CorrespondingSourceObject: {fileID: 0} 78 | m_PrefabInstance: {fileID: 0} 79 | m_PrefabAsset: {fileID: 0} 80 | m_GameObject: {fileID: 0} 81 | m_Enabled: 1 82 | m_EditorHideFlags: 0 83 | m_Script: {fileID: 11500000, guid: 02cc9ca69e7435549a7e93d90b79198d, type: 3} 84 | m_Name: Meat 85 | m_EditorClassIdentifier: 86 | number: 2 87 | --- !u!114 &5518851017741398026 88 | MonoBehaviour: 89 | m_ObjectHideFlags: 0 90 | m_CorrespondingSourceObject: {fileID: 0} 91 | m_PrefabInstance: {fileID: 0} 92 | m_PrefabAsset: {fileID: 0} 93 | m_GameObject: {fileID: 0} 94 | m_Enabled: 1 95 | m_EditorHideFlags: 0 96 | m_Script: {fileID: 11500000, guid: 1360f0b0a64f16b4bb8a81fabdfb3d00, type: 3} 97 | m_Name: Fruit 98 | m_EditorClassIdentifier: 99 | number: 1 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ScriptableObject Container for Unity 2 | 3 | The [ScriptableObject](https://docs.unity3d.com/Manual/class-ScriptableObject.html) type in Unity is a powerful concept with many different applications. 4 | However, it lacks a feature that would greatly increase its usefulness for me: the ability to add "Components" to it. 5 | While Unity allows you to add "Components" to a ScriptableObject asset through code, it does not provide functionality to do so through the Inspector. 6 | 7 | The ScriptableObject Container package aims to solve this issue. 8 | It allows you to work with ScriptableObjects in a similar manner to how you work with Components and GameObjects. 9 | 10 | To add a ScriptableObject to a ```ScriptableObjectContainer```, you can use the Inspector, just like adding a Component to a GameObject. 11 | 12 | On the scripting side, you can access the ScriptableObject from the ```ScriptableObjectContainer``` in a similar way to how you access a Component from a GameObject. 13 | 14 | | ScriptableObjectContainer | GameObject | 15 | |----------|---------------| 16 | | ```ScriptableObject GetObject(Type type)``` | ```Component GetComponent(Type type)``` | 17 | | ```void GetObjects(Type type, List results)``` | ```void GetComponents(Type type, List results)``` | 18 | | ```T GetObject()``` | ```T GetComponent``` | 19 | | ```void GetObjects(List results)``` | ```void GetComponents(List results)``` | 20 | 21 | You can think of a ScriptableObjectContainer as "GameObject" and its sub-assets (or objects) would be the Components on a GameObject. 22 | 23 | # Installation 24 | 25 | Requires Unity 2021.3 and newer. 26 | From the Unity main menu choose Window > Package Manager. 27 | In the Package Manager window, choose "Add package from git URL" and insert one of the Package URL's you can find below. 28 | 29 | ## Package URL's 30 | 31 | | Version | Link | 32 | |----------|---------------| 33 | | 1.0.0 | https://github.com/pschraut/UnityScriptableObjectContainer.git#1.0.0 | 34 | 35 | # Credits 36 | 37 | If this package is useful to you, please mention my name in your credits screen. 38 | Something like "ScriptableObject Container by Peter Schraut" or "Thanks to Peter Schraut" would be very much appreciated. 39 | 40 | # Contact 41 | 42 | Please post in the Unity forums thread that I created for this package: https://forum.unity.com/threads/scriptableobject-container-package.1484589/ 43 | 44 | # Example 45 | 46 | FruitContainer.cs: 47 | ```CSharp 48 | using UnityEngine; 49 | using Oddworm.Framework; 50 | 51 | // Inherit a new container type from ScriptableObjectContainer. 52 | // The CreateAssetMenu attribute lets you create the container from the Assets/Create menu. 53 | [UnityEngine.CreateAssetMenu(menuName = "FruitContainer")] 54 | public class FruitContainer : Oddworm.Framework.ScriptableObjectContainer 55 | { 56 | } 57 | ``` 58 | 59 | Fruit.cs: 60 | ```CSharp 61 | using UnityEngine; 62 | using Oddworm.Framework; 63 | 64 | // Create a new type that adds itself to the "FruitContainer" Inspector. 65 | // The CreateSubAssetMenu attribute lets you add objects to the container through in the Inspector. 66 | [Oddworm.Framework.CreateSubAssetMenu(typeof(FruitContainer))] 67 | public class Fruit : UnityEngine.ScriptableObject 68 | { 69 | [SerializeField] int m_HelloWorld = 123; 70 | } 71 | ``` 72 | 73 | When you create the ```FruitContainer``` asset via "Assets/Create/FruitContainer" it displays an empty ScriptableObjectContainer in 74 | a similar manner as a GameObject without Components. 75 | 76 | ![alt text](Documentation~/Images/Inspector-FruitExample-1.png "ScriptableObject Container") 77 | 78 | You can then add sub-assets via the "Add Object" button. The menu displays all ScriptableObject that have been added to the 79 | container via the ```CreateSubAssetMenu``` attribute. 80 | 81 | ![alt text](Documentation~/Images/Inspector-FruitExample-2.png "ScriptableObject Container") 82 | 83 | After adding the sub-asset it's shown in the Inspector as demonstrated in the image below. 84 | 85 | ![alt text](Documentation~/Images/Inspector-FruitExample-3.png "ScriptableObject Container") 86 | 87 | 88 | # How it works 89 | 90 | The package introduces the type ```ScriptableObjectContainer``` that inherits from ScriptableObject. 91 | 92 | A custom inspector implements the magic that allows to add a ScriptableObject as sub-asset, 93 | to remove such sub-asset as well as to change properties of such sub-asset through the Inspector. 94 | 95 | The ScriptableObjectContainer Inspector attempts to mimic the look and feel of Unity's built-in 96 | Inspector when working with GameObjects and Components. 97 | 98 | The ScriptableObjectContainer itself is rather light-weight. It contains an array with 99 | references to its sub-assets. 100 | This allows you to retrieve these sub-assets through code, similar to how you work with the 101 | ```GameObject.GetComponent``` and ```GameObject.GetComponents``` methods. 102 | 103 | Beside the sub-assets array and its corresponding getter methods, 104 | the ScriptableObjectContainer does not contain more code that's required in a build. 105 | It implements ```OnValidate``` to update fields in sub-assets that use the 106 | ```SubAssetOwnerAttribute``` attribute, but this code runs in the editor only. 107 | 108 | # Test Runner integration 109 | 110 | The ScriptableObjectContainer package comes with several tests that run in 111 | [Unity's Test Runner](https://docs.unity3d.com/Packages/com.unity.test-framework@latest). 112 | 113 | The tests can be enabled through the 114 | ```SCRIPTABLEOBJECTCONTAINER_ENABLE_TESTS``` scripting define symbol. 115 | Add this scripting define symbol to the Player Settings and the tests appear in 116 | Unity's Test Runner. 117 | 118 | Additionally to the tests in the Test Runner window, 119 | it adds various context menu items to create test assets, 120 | which is the reason why it's disabled by default, to avoid cluttering your project with things you most likely don't need. 121 | 122 | # Context Menu integration 123 | 124 | The package adds a "ScriptableObject Container" item to the 125 | assets context menu, which allows to create a new 126 | ScriptableObject Container asset. 127 | 128 | In case you don't want to allow the use of a ScriptableObject Container, 129 | but only allow your specific derived containers, use the 130 | ```SCRIPTABLEOBJECTCONTAINER_DISABLE_MENUITEM``` 131 | scripting define symbol to remove the context-menuitem. 132 | 133 | 134 | # More Examples 135 | 136 | ## CreateSubAssetMenuAttribute 137 | 138 | A ScriptableObjectContainer shows an "Add Object" button in the Inspector, 139 | much like a GameObject shows a "Add Component" button. It allows you to add 140 | objects that inherit from ScriptableObject to a specific container. 141 | 142 | In order to add a ScriptableObject to the "Add Object" menu, you need to 143 | add the ```CreateSubAssetMenuAttribute``` to the ScriptableObject type. 144 | 145 | You can add multiple ```CreateSubAssetMenuAttribute``` to add it to different 146 | containers. You can also specify the base-container type to add a menu item 147 | to all types that inherit from the base-container. 148 | ```CSharp 149 | [CreateSubAssetMenu(typeof(FruitContainer), menuName = "Fruit")] 150 | class Fruit : ScriptableObject 151 | { 152 | // ... 153 | } 154 | ``` 155 | 156 | 157 | ## DisallowMultipleSubAssetAttribute 158 | 159 | If you want to prevent to add the same ScriptableObject type (or subtype) 160 | more than once to the same container, you can use the ```DisallowMultipleSubAssetAttribute```. 161 | 162 | This works similar to how you use Unity's 163 | [DisallowMultipleComponentAttribute](https://docs.unity3d.com/ScriptReference/DisallowMultipleComponent.html) 164 | to prevent a MonoBehaviour of same type (or subtype) to be added more than once to a GameObject. 165 | ```CSharp 166 | [DisallowMultipleSubAsset] 167 | class Fruit : ScriptableObject 168 | { 169 | // ... 170 | } 171 | ``` 172 | [![](http://img.youtube.com/vi/QnjTcPqM0sg/0.jpg)](http://www.youtube.com/watch?v=QnjTcPqM0sg "") 173 | 174 | 175 | ## SubAssetOwnerAttribute 176 | 177 | If you need a reference to the ScriptableObjectContainer inside your ScriptableObject 178 | sub-asset, you can use the ```SubAssetOwnerAttribute``` for the system to automatically 179 | setup the reference for you. The code that sets up references runs in the editor only, 180 | there is no performance penalty in a build. 181 | ```CSharp 182 | class Fruit : ScriptableObject 183 | { 184 | [SubAssetOwner] 185 | [SerializeField] ScriptableObjectContainer m_Container; 186 | } 187 | ``` 188 | If you know that a sub-asset lives inside a specific container type only, 189 | you can also use the specific container type. 190 | ```CSharp 191 | class Fruit : ScriptableObject 192 | { 193 | [SubAssetOwner] 194 | [SerializeField] Basket m_Container; // The Basket type must inherit 195 | // from ScriptableObjectContainer 196 | } 197 | ``` 198 | [![](http://img.youtube.com/vi/Ex9FQ3yXhBw/0.jpg)](http://www.youtube.com/watch?v=Ex9FQ3yXhBw "") 199 | 200 | 201 | ## SubAssetToggleAttribute 202 | 203 | Unity does not support the concept of enabling and disabling a ScriptableObject, 204 | but I often found myselfing wanting a simple way to expose an "enabled" toggle 205 | for whatever use-case I have. 206 | 207 | Using the ```SubAssetToggleAttribute``` on a ```bool``` field causes the 208 | ScriptableObjectContainer Inspector to display a toggle (checkbox) like you can find 209 | in Components on a GameObject. 210 | ```CSharp 211 | class Fruit : ScriptableObject 212 | { 213 | [SubAssetToggle] 214 | [SerializeField] bool m_IsEnabled; 215 | } 216 | ``` 217 | You can use any field name you like, it doesn't have to be ```m_IsEnabled```. 218 | However, it's worth noting that you can't use ```m_Enabled``` as field name, because 219 | it conflicts with a field that Unity implements too (but seemingly Unity isn't using it). 220 | 221 | [![](http://img.youtube.com/vi/tMfqDenY1pc/0.jpg)](http://www.youtube.com/watch?v=tMfqDenY1pc "") 222 | 223 | -------------------------------------------------------------------------------- /Tests/Editor/ScriptableObjectContainerTests.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 IDE0062 // Make local function 'static' 9 | #pragma warning disable IDE0074 // Use compound assignment 10 | using UnityEngine; 11 | using NUnit.Framework; 12 | using UnityEditor; 13 | using Oddworm.Framework; 14 | 15 | namespace Oddworm.EditorFramework.Tests.ScriptableObjectContainerTest 16 | { 17 | #if SCRIPTABLEOBJECTCONTAINER_ENABLE_TESTS 18 | class ScriptableObjectContainerTests 19 | { 20 | const string k_PackageTestsFolderAssetGUID = "f07a2a99328f0c74fa791bb5d36d1960"; 21 | 22 | [Test] 23 | public void Test_FindContainerAssets() 24 | { 25 | var folder = AssetDatabase.GUIDToAssetPath(k_PackageTestsFolderAssetGUID); 26 | var assets = FindAllPipelineAssets(new[] { folder }); 27 | Assert.IsTrue(assets.Length > 0); 28 | } 29 | 30 | [Test] 31 | public void Test_FindContainerAsset() 32 | { 33 | Assert.IsNotNull(FindContainerAsset("Test_001")); 34 | Assert.IsNotNull(FindContainerAsset("Test_002")); 35 | Assert.IsNotNull(FindContainerAsset("Test_003")); 36 | Assert.IsNotNull(FindContainerAsset("Test_004")); 37 | Assert.IsNotNull(FindContainerAsset("Test_005")); 38 | Assert.IsNotNull(FindContainerAsset("Test_006")); 39 | Assert.IsNotNull(FindContainerAsset("Test_007")); 40 | Assert.IsNotNull(FindContainerAsset("Test_008")); 41 | } 42 | 43 | [Test] 44 | public void Test_001_GetObject() 45 | { 46 | var container = FindContainerAsset("Test_001"); 47 | Assert.IsNotNull(container.GetObject(typeof(Fruit))); 48 | Assert.IsNotNull(container.GetObject()); 49 | 50 | Assert.IsNotNull(container.GetObject(typeof(ScriptableObject))); 51 | Assert.IsNotNull(container.GetObject()); 52 | } 53 | 54 | [Test] 55 | public void Test_001_GetObjects() 56 | { 57 | var container = FindContainerAsset("Test_001"); 58 | Assert.IsNotNull(container.GetObjects(typeof(Fruit))); 59 | Assert.IsNotNull(container.GetObjects()); 60 | 61 | Assert.AreEqual(1, container.GetObjects(typeof(Fruit)).Length); 62 | Assert.AreEqual(1, container.GetObjects().Length); 63 | 64 | 65 | Assert.IsNotNull(container.GetObjects(typeof(ScriptableObject))); 66 | Assert.IsNotNull(container.GetObjects()); 67 | 68 | Assert.AreEqual(1, container.GetObjects(typeof(ScriptableObject)).Length); 69 | Assert.AreEqual(1, container.GetObjects().Length); 70 | } 71 | 72 | [Test] 73 | public void Test_002_GetObject() 74 | { 75 | var container = FindContainerAsset("Test_002"); 76 | Assert.IsNotNull(container.GetObject(typeof(Fruit))); 77 | Assert.IsNotNull(container.GetObject()); 78 | 79 | Assert.IsNotNull(container.GetObject(typeof(ScriptableObject))); 80 | Assert.IsNotNull(container.GetObject()); 81 | } 82 | 83 | [Test] 84 | public void Test_002_GetObjects() 85 | { 86 | var container = FindContainerAsset("Test_002"); 87 | 88 | Test(container.GetObjects(typeof(Fruit))); 89 | Test(container.GetObjects()); 90 | 91 | Test(container.GetObjects(typeof(ScriptableObject))); 92 | Test(container.GetObjects()); 93 | 94 | void Test(System.Collections.Generic.IEnumerable objs) 95 | { 96 | Assert.IsNotNull(objs); 97 | if (objs == null) 98 | return; 99 | 100 | // Tests if "GetObjects" retrieves objects in the expected order 101 | var n = 0; 102 | foreach(var o in objs) 103 | { 104 | var obj = o as Fruit; 105 | Assert.NotNull(obj); 106 | if (obj == null) 107 | return; 108 | 109 | Assert.AreEqual(n, obj.number); 110 | n++; 111 | } 112 | 113 | Assert.AreEqual(3, n); 114 | } 115 | } 116 | 117 | [Test] 118 | public void Test_003_GetObjects() 119 | { 120 | var container = FindContainerAsset("Test_003"); 121 | 122 | Test(container.GetObjects(typeof(Fruit)), 3, 0); 123 | Test(container.GetObjects(), 3, 0); 124 | 125 | Test(container.GetObjects(typeof(Meat)), 0, 3); 126 | Test(container.GetObjects(), 0, 3); 127 | 128 | Test(container.GetObjects(typeof(ScriptableObject)), 3, 3); 129 | Test(container.GetObjects(), 3, 3); 130 | 131 | void Test(System.Collections.Generic.IEnumerable objs, int expectedFruitNumber, int expectedMeatNumber) 132 | { 133 | Assert.IsNotNull(objs); 134 | if (objs == null) 135 | return; 136 | 137 | // Tests if "GetObjects" retrieves objects in the expected order 138 | var fruitNumber = 0; 139 | var meatNumber = 0; 140 | foreach (var o in objs) 141 | { 142 | var fruit = o as Fruit; 143 | var meat = o as Meat; 144 | Assert.IsTrue(meat != null || fruit != null); 145 | 146 | if (fruit != null) 147 | { 148 | Assert.AreEqual(fruitNumber, fruit.number); 149 | fruitNumber++; 150 | } 151 | 152 | if (meat != null) 153 | { 154 | Assert.AreEqual(meatNumber, meat.number); 155 | meatNumber++; 156 | } 157 | } 158 | 159 | Assert.AreEqual(expectedFruitNumber, fruitNumber); 160 | Assert.AreEqual(expectedMeatNumber, meatNumber); 161 | } 162 | } 163 | 164 | [Test] 165 | public void Test_004_CanAddObjectOfType() 166 | { 167 | var container = FindContainerAsset("Test_004"); 168 | 169 | var canAdd = EditorScriptableObjectContainerUtility.CanAddObjectOfType(container, typeof(SingleFruit), false); 170 | Assert.IsFalse(canAdd); 171 | } 172 | 173 | [Test] 174 | public void Test_005_CanAddObjectOfType() 175 | { 176 | var container = FindContainerAsset("Test_005"); 177 | 178 | var canAdd = EditorScriptableObjectContainerUtility.CanAddObjectOfType(container, typeof(SingleFruit), false); 179 | Assert.IsTrue(canAdd); 180 | } 181 | 182 | [Test] 183 | public void Test_006() 184 | { 185 | var container = FindContainerAsset("Test_006"); 186 | Assert.NotNull(container); 187 | 188 | var subObj = container.GetObject(); 189 | Assert.NotNull(subObj); 190 | 191 | var fields = EditorScriptableObjectContainerUtility.GetObjectToggleFields(subObj); 192 | Assert.NotNull(fields); 193 | Assert.IsTrue(fields.Count == 1); 194 | 195 | var value = EditorScriptableObjectContainerUtility.GetObjectToggleValue(subObj, fields); 196 | Assert.IsTrue(value); 197 | } 198 | 199 | [Test] 200 | public void Test_007() 201 | { 202 | var container = FindContainerAsset("Test_007"); 203 | Assert.NotNull(container); 204 | 205 | var subObj = container.GetObject(); 206 | Assert.NotNull(subObj); 207 | 208 | var fields = EditorScriptableObjectContainerUtility.GetObjectToggleFields(subObj); 209 | Assert.NotNull(fields); 210 | Assert.IsTrue(fields.Count == 1); 211 | 212 | var value = EditorScriptableObjectContainerUtility.GetObjectToggleValue(subObj, fields); 213 | Assert.IsFalse(value); 214 | } 215 | 216 | [Test] 217 | public void Test_008_SetObjectToggleValue() 218 | { 219 | var container = FindContainerAsset("Test_008"); 220 | Assert.NotNull(container); 221 | 222 | var subObj = container.GetObject(); 223 | Assert.NotNull(subObj); 224 | 225 | var fields = EditorScriptableObjectContainerUtility.GetObjectToggleFields(subObj); 226 | Assert.NotNull(fields); 227 | Assert.IsTrue(fields.Count == 1); 228 | 229 | Assert.IsFalse(EditorScriptableObjectContainerUtility.GetObjectToggleValue(subObj, fields)); 230 | try 231 | { 232 | EditorScriptableObjectContainerUtility.SetObjectToggleValue(subObj, fields, true); 233 | Assert.IsTrue(EditorScriptableObjectContainerUtility.GetObjectToggleValue(subObj, fields)); 234 | } 235 | finally 236 | { 237 | EditorScriptableObjectContainerUtility.SetObjectToggleValue(subObj, fields, false); 238 | } 239 | } 240 | 241 | ScriptableObjectContainer FindContainerAsset(string assetName) 242 | { 243 | var folder = AssetDatabase.GUIDToAssetPath(k_PackageTestsFolderAssetGUID); 244 | return FindPipelineAsset(assetName, new[] { folder }); 245 | } 246 | 247 | /// 248 | /// Finds the pipeline asset with the specified name. 249 | /// 250 | /// The pipeline asset filename 251 | /// Search for pipeline assets in the specified folders only. Pass null to search entire Assets directory. 252 | /// The asset on success, null otherwise. 253 | /// Thrown when is null. 254 | static ScriptableObjectContainer FindPipelineAsset(string pipelineAssetName, string[] searchInFolders = null) 255 | { 256 | if (string.IsNullOrEmpty(pipelineAssetName)) 257 | return null; 258 | 259 | foreach (var asset in FindAllPipelineAssets(searchInFolders)) 260 | { 261 | var assetPath = AssetDatabase.GetAssetPath(asset); 262 | var assetName = System.IO.Path.GetFileNameWithoutExtension(assetPath); 263 | 264 | if (string.Equals(assetName, pipelineAssetName, System.StringComparison.OrdinalIgnoreCase)) 265 | return AssetDatabase.LoadAssetAtPath(assetPath); 266 | } 267 | 268 | return null; 269 | } 270 | 271 | /// 272 | /// Finds all pipeline asset in the project. 273 | /// 274 | /// Search for pipeline assets in the specified folders only. Pass null to search entire Assets directory. 275 | /// The pipeline assets that were found in the project or an empty array if no pipeline asset was found. 276 | static ScriptableObjectContainer[] FindAllPipelineAssets(string[] searchInFolders = null) 277 | { 278 | if (searchInFolders == null) 279 | searchInFolders = new[] { "Assets" }; 280 | 281 | var guids = AssetDatabase.FindAssets($"t: {nameof(ScriptableObjectContainer)}", searchInFolders); 282 | var pipelineAssets = new ScriptableObjectContainer[guids.Length]; 283 | 284 | for (var n = 0; n < guids.Length; ++n) 285 | { 286 | var assetPath = AssetDatabase.GUIDToAssetPath(guids[n]); 287 | var pipelineAsset = AssetDatabase.LoadAssetAtPath(assetPath); 288 | 289 | pipelineAssets[n] = pipelineAsset; 290 | } 291 | 292 | return pipelineAssets; 293 | } 294 | } 295 | #endif // SCRIPTABLEOBJECTCONTAINER_ENABLE_TESTS 296 | } 297 | -------------------------------------------------------------------------------- /Runtime/ScriptableObjectContainer.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 IDE0019 // Use Pattern matching 6 | #pragma warning disable IDE0079 // Remove unnecessary suppression 7 | #pragma warning disable IDE0040 // Add accessibility modifiers 8 | #pragma warning disable IDE0051 // Remove unused private members 9 | #pragma warning disable IDE1006 // Naming Styles 10 | using System; 11 | using System.Collections.Generic; 12 | using UnityEngine; 13 | 14 | #if UNITY_EDITOR 15 | using System.Reflection; 16 | #endif 17 | 18 | namespace Oddworm.Framework 19 | { 20 | #if !SCRIPTABLEOBJECTCONTAINER_DISABLE_MENUITEM 21 | [CreateAssetMenu(menuName = "ScriptableObject Container", order = 310)] 22 | #endif 23 | public class ScriptableObjectContainer : ScriptableObject 24 | { 25 | [Tooltip("The array holds references to the added objects.")] 26 | [HideInInspector] 27 | [SerializeField] ScriptableObject[] m_SubObjects = Array.Empty(); 28 | 29 | static readonly List s_TempCache = new(); 30 | 31 | /// 32 | /// Gets the first object of the specified type. 33 | /// 34 | /// The type of object to retrieve. The type must derive from ScriptableObject or must be an interface. 35 | /// The object on success, null otherwise. 36 | /// Throws an ArgumentNullException if the specified type is null. 37 | /// Throws an ArgumentException if the specified type does not derive from ScriptableObject or isn't an interface. 38 | public ScriptableObject GetObject(Type type) 39 | { 40 | ThrowIfInvalidType(type, nameof(GetObject)); 41 | 42 | for (var n = 0; n < m_SubObjects.Length; ++n) 43 | { 44 | var subObject = m_SubObjects[n]; 45 | if (subObject == null) 46 | continue; 47 | 48 | var subObjectType = subObject.GetType(); 49 | if (subObjectType == type || subObjectType.IsSubclassOf(type)) 50 | return subObject; 51 | } 52 | 53 | return null; 54 | } 55 | 56 | /// 57 | /// Gets the first object of the specified type. 58 | /// 59 | /// The type of object to retrieve. The type must derive from ScriptableObject or must be an interface. 60 | /// The object on success,null otherwise. 61 | /// Throws an ArgumentNullException if the specified type is null. 62 | /// Throws an ArgumentException if the specified type does not derive from ScriptableObject or isn't an interface. 63 | public T GetObject() where T : class 64 | { 65 | ThrowIfInvalidType(typeof(T), nameof(GetObject)); 66 | 67 | for (var n = 0; n < m_SubObjects.Length; ++n) 68 | { 69 | if (m_SubObjects[n] is T subobj) 70 | return subobj; 71 | } 72 | 73 | return null; 74 | } 75 | 76 | /// 77 | /// Gets the first object of the specified type. 78 | /// 79 | /// The type of object to retrieve. The type must derive from ScriptableObject or must be an interface. 80 | /// The output argument that will contain the object or null. 81 | /// true if the object is found, false otherwise. 82 | /// Throws an ArgumentNullException if the specified type is null. 83 | /// Throws an ArgumentException if the specified type does not derive from ScriptableObject or isn't an interface. 84 | public bool TryGetObject(Type type, out object result) 85 | { 86 | result = GetObject(type); 87 | return result != null; 88 | } 89 | 90 | /// 91 | /// Gets the first object of the specified type. 92 | /// 93 | /// The type of object to retrieve. The type must derive from ScriptableObject or must be an interface. 94 | /// The output argument that will contain the object or null. 95 | /// true if the object is found, false otherwise. 96 | /// Throws an ArgumentNullException if the specified type is null. 97 | /// Throws an ArgumentException if the specified type does not derive from ScriptableObject or isn't an interface. 98 | public bool TryGetObject(out T result) where T : class 99 | { 100 | result = GetObject(); 101 | return result != null; 102 | } 103 | 104 | /// 105 | /// Gets all objects of the specified type. 106 | /// 107 | /// The type of object to retrieve. The type must derive from ScriptableObject or must be an interface. 108 | /// The list where all objects are added to. 109 | /// Throws an ArgumentNullException if the specified type is null. 110 | /// Throws an ArgumentException if the specified type does not derive from ScriptableObject or isn't an interface. 111 | public void GetObjects(List results) where T : class 112 | { 113 | ThrowIfInvalidType(typeof(T), nameof(GetObjects)); 114 | 115 | for (var n = 0; n < m_SubObjects.Length; ++n) 116 | { 117 | if (m_SubObjects[n] is T subobj) 118 | results.Add(subobj); 119 | } 120 | } 121 | 122 | /// 123 | /// Gets all objects of the specified type. 124 | /// 125 | /// The type of object to retrieve. The type must derive from ScriptableObject or must be an interface. 126 | /// 127 | /// A newly allocated array that contains references to the objects, or an empty array if no objects could be found. 128 | /// 129 | /// Throws an ArgumentNullException if the specified type is null. 130 | /// Throws an ArgumentException if the specified type does not derive from ScriptableObject or isn't an interface. 131 | public T[] GetObjects() where T : class 132 | { 133 | s_TempCache.Clear(); 134 | GetObjects(typeof(T), s_TempCache); 135 | 136 | var result = new T[s_TempCache.Count]; 137 | for (var n = 0; n < s_TempCache.Count; ++n) 138 | result[n] = s_TempCache[n] as T; 139 | 140 | s_TempCache.Clear(); 141 | return result; 142 | } 143 | 144 | /// 145 | /// Gets all objects of the specified type. 146 | /// 147 | /// The type of object to retrieve. The type must derive from ScriptableObject or must be an interface. 148 | /// The list where all objects are added to. 149 | /// Throws an ArgumentNullException if the specified type is null. 150 | /// Throws an ArgumentException if the specified type does not derive from ScriptableObject or isn't an interface. 151 | public void GetObjects(Type type, List results) 152 | { 153 | ThrowIfInvalidType(type, nameof(GetObjects)); 154 | 155 | for (var n = 0; n < m_SubObjects.Length; ++n) 156 | { 157 | var subObject = m_SubObjects[n]; 158 | if (subObject == null) 159 | continue; 160 | 161 | var subObjectType = subObject.GetType(); 162 | if (subObjectType == type || subObjectType.IsSubclassOf(type)) 163 | results.Add(subObject); 164 | } 165 | } 166 | 167 | /// 168 | /// Gets all objects of the specified type. 169 | /// 170 | /// The type of object to retrieve. The type must derive from ScriptableObject or must be an interface. 171 | /// 172 | /// A newly allocated array that contains references to the objects, or an empty array if no objects could be found. 173 | /// 174 | /// Throws an ArgumentNullException if the specified type is null. 175 | /// Throws an ArgumentException if the specified type does not derive from ScriptableObject or isn't an interface. 176 | public object[] GetObjects(Type type) 177 | { 178 | s_TempCache.Clear(); 179 | GetObjects(type, s_TempCache); 180 | 181 | var result = new object[s_TempCache.Count]; 182 | for (var n = 0; n < s_TempCache.Count; ++n) 183 | result[n] = s_TempCache[n]; 184 | 185 | s_TempCache.Clear(); 186 | return result; 187 | } 188 | 189 | protected virtual void OnEnable() 190 | { 191 | // In case I need to implement something in OnEnable in the future, lets make it virtual. 192 | } 193 | 194 | protected virtual void OnDisable() 195 | { 196 | // In case I need to implement something in OnDisable in the future, lets make it virtual. 197 | } 198 | 199 | protected virtual void OnValidate() 200 | { 201 | ProcessSubAssetOwnerAttribute(); 202 | } 203 | 204 | /// 205 | /// Check each sub asset if any of its fields uses the and 206 | /// assign that field to this ScriptableObjectContainer. 207 | /// 208 | void ProcessSubAssetOwnerAttribute() 209 | { 210 | #if UNITY_EDITOR 211 | for (var n = 0; n < m_SubObjects.Length; ++n) 212 | { 213 | var so = m_SubObjects[n]; 214 | if (so == null) 215 | continue; 216 | 217 | var soType = so.GetType(); 218 | var loopguard = 0; 219 | 220 | do 221 | { 222 | if (++loopguard > 64) 223 | { 224 | Debug.LogError($"Loopguard kicked in, detected more than {loopguard} levels of inheritence?", this); 225 | break; 226 | } 227 | 228 | foreach (var fieldInfo in soType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy)) 229 | { 230 | var attribute = fieldInfo.GetCustomAttribute(true); 231 | if (attribute == null) 232 | continue; 233 | 234 | // Make sure the field is a reference to a ScriptableObject or derived type 235 | var valid = false; 236 | if (fieldInfo.FieldType == typeof(ScriptableObject)) 237 | valid = true; 238 | if (fieldInfo.FieldType.IsSubclassOf(typeof(ScriptableObject))) 239 | valid = true; 240 | if (!valid) 241 | { 242 | Debug.LogError($"The field '{fieldInfo.Name}' in class '{so.GetType().FullName}' uses the [{nameof(SubAssetOwnerAttribute)}]. The attribute can be used for fields of type '{nameof(ScriptableObject)}' only, but it uses '{fieldInfo.FieldType.FullName}' which does not inherit from '{nameof(ScriptableObject)}'.", this); 243 | continue; 244 | } 245 | 246 | // Make sure the field- and container type are compatible 247 | valid = false; 248 | if (fieldInfo.FieldType == GetType()) 249 | valid = true; 250 | if (GetType().IsSubclassOf(fieldInfo.FieldType)) 251 | valid = true; 252 | if (!valid) 253 | { 254 | Debug.LogError($"The field '{fieldInfo.Name}' in class '{so.GetType().FullName}' uses the [{nameof(SubAssetOwnerAttribute)}], but the container and field type are incompatible. The field is of type '{fieldInfo.FieldType.FullName}', but the container is of type '{GetType().FullName}', which is not a sub-class of '{fieldInfo.FieldType.FullName}'.", this); 255 | continue; 256 | } 257 | 258 | if ((ScriptableObject)fieldInfo.GetValue(so) != this) 259 | { 260 | fieldInfo.SetValue(so, this); 261 | UnityEditor.EditorUtility.SetDirty(so); 262 | } 263 | } 264 | 265 | soType = soType.BaseType; 266 | } while (soType != null && soType != typeof(ScriptableObject)); 267 | } 268 | #endif 269 | } 270 | 271 | /// 272 | /// Throws an exception if the specified type is invalid to be used with any GetSubObject method. 273 | /// 274 | /// The type to verify. 275 | /// The method name of the caller. 276 | void ThrowIfInvalidType(Type type, string methodName) 277 | { 278 | if (type == null) 279 | throw new ArgumentNullException($"Type must not be null."); 280 | 281 | var isValidType = type.IsSubclassOf(typeof(ScriptableObject)) || type == typeof(ScriptableObject); 282 | if (type.IsInterface) 283 | isValidType = true; 284 | 285 | if (!isValidType) 286 | throw new ArgumentException($"{methodName} requires that the requested component '{type.Name}' derives from {nameof(ScriptableObject)} or is an interface."); 287 | } 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /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/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