Search Unity

Generic Create ScriptableObject Attribute

Discussion in 'Immediate Mode GUI (IMGUI)' started by BinaryCats, Feb 16, 2018.

  1. BinaryCats

    BinaryCats

    Joined:
    Feb 8, 2016
    Posts:
    317
    Hi,

    I have decided to share an attribute/ property drawer I have made to be able to create a scriptable of the correct type and set that asset to the fields value, on right click (of the field).



    I finally created this to create a smoother workflow for our designer. It is basic for now, I might update it. one area I could see it being improved is being able to create child classes of the scriptable object, as they would also be valid in that field.


    Here is the code:

    Code (csharp):
    1.  
    2. [AttributeUsage(AttributeTargets.Field, Inherited = true)]
    3. public class CreateAssetAttribute :  PropertyAttribute
    4. {
    5. }
    6.  
    7.  
    8. [CustomPropertyDrawer(typeof(CreateAssetAttribute), true)]
    9. public class CreateAssetAttribute : PropertyDrawer
    10. {
    11.  public override void OnGUI(Rect position,
    12.                                 SerializedProperty property,
    13.                                 GUIContent label)
    14.     {
    15.         if (Event.current.type == EventType.ContextClick)
    16.         {
    17.             if (position.Contains((Event.current.mousePosition)))
    18.             {
    19.                 t = fieldInfo.FieldType;
    20.                 Debug.Log("sucess");
    21.                 GenericMenu menu = new GenericMenu();
    22.                 menu.AddItem(new GUIContent("Create"), false, HandleSelect, property);
    23.                 menu.ShowAsContext();
    24.             }
    25.             else
    26.             {
    27.                 Debug.Log((Event.current.mousePosition) + "" + position);
    28.             }
    29.  
    30.         }
    31.         EditorGUI.PropertyField(position, property, label, true);
    32.  
    33.     }
    34.     static Type t;
    35.   private static void HandleSelect(object val)
    36.     {
    37.         var prop = (val as SerializedProperty);
    38.         ScriptableObject asset = ScriptableObject.CreateInstance(t)  ;
    39.         var p = AssetDatabase.GetAssetPath(prop.serializedObject.targetObject);
    40.         var path = p.Substring(0, p.LastIndexOf("/")+1);
    41.         path = path + t.ToString() + "Data.asset";
    42.         path = AssetDatabase.GenerateUniqueAssetPath(path);
    43.            
    44.         AssetDatabase.CreateAsset(asset, path);
    45.         prop.objectReferenceValue = asset;
    46.         prop.serializedObject.ApplyModifiedProperties();
    47.         AssetDatabase.SaveAssets();
    48.         //optional
    49.         //EditorUtility.FocusProjectWindow();
    50.         //Selection.activeObject = asset;
    51.     }
    52.  
    53. }
    54.  
    Thanks


    --edit--
    added AssetDatabase.GenerateUniqueAssetPath to prevent overwriting
     
    Last edited: Feb 16, 2018
  2. shawn

    shawn

    Unity Technologies

    Joined:
    Aug 4, 2007
    Posts:
    552
    Super handy. Seems like something we should add generically for object fields. Could totally see wanting to create a new material from a material object field too, for instance.
     
  3. BinaryCats

    BinaryCats

    Joined:
    Feb 8, 2016
    Posts:
    317
    I defiantly agree!
     
  4. liortal

    liortal

    Joined:
    Oct 17, 2012
    Posts:
    3,562
    I would clean up the code a bit - remove any Debug.Log calls and commented out stuff.
    Also, you should test that the type of the field you're editing actually derives from ScriptableObject.
     
  5. BinaryCats

    BinaryCats

    Joined:
    Feb 8, 2016
    Posts:
    317
    My mistake about the debug logs, but the commented out stuff is more about functionality the user may or may not want, that's up to you to decide. What the commented out code does is selects the newly created item in your project, for instance if you want to rename it.

    In my opinion it takes away from the ease of creating lots of objects, but on the other hand I can see it being useful, hence why I left it in with the //optional comment
     
  6. Noisecrime

    Noisecrime

    Joined:
    Apr 7, 2010
    Posts:
    2,054
    Thanks for this.

    I was searching for something else concerning ScriptableObjects but this just seemed too good to pass up, so added it to my project framework. Of course as is the way with coders I made some changes ; )

    For one thing the actual code here didn't work, it failed as it expects that the property reference is coming from an object that is itself an asset ( I think that's the case ) so that it has a path on disk, otherwise for a reference in a c# script p = "" and thus no actual path. May also have been the case of there being no existing folder for it to save into.

    In addition I added
    • Basic checking so it only reacts when applied to a property that is a reference for a scriptableObject.
    • Context menu will also popup when left clicking on an empty ( unassigned scriptableObject ) reference.
    • Context menu has two options 'Create' and 'CreateInProjectBrowser'.
    Create
    Will pop up a save file dialog, allowing you to specify where to save the file and rename it.

    CreateInProjectBrowser
    Will automatically save the asset with the scriptableObject class name into the currently selected ProjectBrowser folder. If no folder selected or there is no ProjectBrowser open it will go into the root 'Assets' folder.

    I dropped the original intent of the script as I couldn't really see any point in creating a new asset in the same location as some other asset that had a reference to the scriptableObject. I guess there may be some project specific reason for needing this, but it just didn't make much sense and was confusing. Shouldn't be too hard to add back in if needed, though i'd add another context menu item for it.

    I guess the next step would be to add support other asset types such as Material, Shader, etc.

    As ever use at your own risk, though I've spent a good amount of time doing basic error checking and testing.


    Place 'ContextCreateAssetAttribute.cs' into your project anywhere except an 'Editor' folder.

    Code (CSharp):
    1. using UnityEngine;
    2. using System;
    3.  
    4. [AttributeUsage(AttributeTargets.Field, Inherited = true)]
    5. public class ContextCreateAssetAttribute :  PropertyAttribute
    6. {
    7.  
    8. }

    Place 'ContextCreateAssetAttributeDrawer.cs' into an 'Editor' folder.
    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEditor;
    3. using System;
    4. using System.Reflection;
    5. using System.IO;
    6. using Debug = UnityEngine.Debug;
    7.  
    8. [CustomPropertyDrawer( typeof( ContextCreateAssetAttribute ), true )]
    9. public class ContextCreateAssetAttributeDrawer : PropertyDrawer
    10. {
    11.     static Type        m_CreatePropertyType;
    12.     static string    m_LastDirectory = Application.dataPath;
    13.  
    14.     private static void HandleCreate ( object val )
    15.     {
    16.         // Asset file name dervived from type name - strips out class name hierarchy to just the actual scriptableObject name.
    17.         string assetTypeName        = m_CreatePropertyType.ToString();
    18.         string assetShortName        = assetTypeName.Substring( assetTypeName.LastIndexOf('.') + 1, assetTypeName.Length -  assetTypeName.LastIndexOf('.') - 1 );
    19.  
    20.         // Validate the last visit directory still exists.
    21.         if ( !Directory.Exists( m_LastDirectory ) ) m_LastDirectory = Application.dataPath;
    22.  
    23.         // Allow user to specify a directory and asset name.
    24.         string userFilePath            = EditorUtility.SaveFilePanel("Save " + assetShortName, m_LastDirectory, assetShortName, "asset");
    25.  
    26.         // Check for user cancel
    27.         if ( string.IsNullOrEmpty( userFilePath ) ) return;
    28.  
    29.         string userFileDirectory    = Path.GetDirectoryName( userFilePath );
    30.  
    31.         // Check for invalid path
    32.         if ( userFileDirectory.Length < Application.dataPath.Length )
    33.         {
    34.             EditorUtility.DisplayDialog( "Invalid Path", "The path must be within the Unity Assets folder", "OK" );
    35.             return;
    36.         }
    37.  
    38.         // Cache last directory so we can SaveFilePanel at same place next time.
    39.         m_LastDirectory                = userFileDirectory;
    40.  
    41.         // Make it relative to Application.dataPath. Assuming path seperators are consitent here, else try s.Replace(@"\", "/");
    42.         string filePathRelative        = userFilePath.Replace( Application.dataPath, "Assets" );
    43.  
    44.         CreateScriptableObjectAsset ( val, filePathRelative );
    45.     }
    46.  
    47.  
    48.     private static void HandleCreateInBrowser ( object val )
    49.     {
    50.         // Asset file name dervived from type name - strips out class name hierarchy to just the actual scriptableObject name.
    51.         string assetTypeName        = m_CreatePropertyType.ToString();
    52.         string assetShortName       = assetTypeName.Substring( assetTypeName.LastIndexOf('.') + 1, assetTypeName.Length -  assetTypeName.LastIndexOf('.') - 1 );
    53.  
    54.         // Get path from Unity Internal ProjectBrowser class
    55.         string projectPathRelative    = GetActiveFolderPath();
    56.  
    57.         // Check for invalid path
    58.         if ( string.IsNullOrEmpty( projectPathRelative) ) projectPathRelative = "Assets";
    59.  
    60.         // Cache last directory so we can SaveFilePanel at same place next time.
    61.         m_LastDirectory                = projectPathRelative.Replace( "Assets", Application.dataPath );
    62.  
    63.         string filePathRelative        = projectPathRelative + Path.AltDirectorySeparatorChar + assetShortName + ".asset";
    64.  
    65.         // Generate a unique path to avoid accidental overwrites.
    66.         string uniquePathRelative    = AssetDatabase.GenerateUniqueAssetPath( filePathRelative );
    67.  
    68.         // Debug.LogFormat( "{0}\n{1}\n{2}\n{3}", m_LastDirectory, projectPathRelative, filePathRelative, uniquePathRelative);
    69.  
    70.         CreateScriptableObjectAsset ( val, uniquePathRelative );
    71.     }
    72.  
    73.  
    74.     private static void CreateScriptableObjectAsset ( object val, string userFilePathRelative )
    75.     {
    76.         // Refresh in case user created directories
    77.         AssetDatabase.Refresh();
    78.  
    79.         // Create new Scriptable Object Instance
    80.         ScriptableObject asset = ScriptableObject.CreateInstance( m_CreatePropertyType );
    81.  
    82.         // Create the on disk asset of the Scriptable Object Instance
    83.         AssetDatabase.CreateAsset( asset, userFilePathRelative );
    84.  
    85.         // Update the original property to hold the reference to the new asset
    86.         var prop                    = (val as SerializedProperty);
    87.         prop.objectReferenceValue    = asset;
    88.         prop.serializedObject.ApplyModifiedProperties();
    89.  
    90.         // Save the Asset
    91.         AssetDatabase.SaveAssets();
    92.  
    93.         // Focus on new asset in ProjectBrowser
    94.         EditorUtility.FocusProjectWindow();
    95.         Selection.activeObject = asset;
    96.     }
    97.  
    98.     private static string GetActiveFolderPath ()
    99.     {
    100.         // Create default return path in the event something fails.
    101.         string defaultPath = "Assets";
    102.  
    103.         Type projectWindowUtilType = typeof( ProjectWindowUtil );
    104.         if ( projectWindowUtilType == null ) return defaultPath;
    105.  
    106.         // Reflection to get access to Internal private method - GetActiveFolderPath
    107.         MethodInfo getFolderMethodInfo = projectWindowUtilType.GetMethod("GetActiveFolderPath", BindingFlags.NonPublic | BindingFlags.Static);
    108.         if ( getFolderMethodInfo == null ) return defaultPath;
    109.  
    110.         // Get active path from ProjectBrowser
    111.         string projectPath = (string)getFolderMethodInfo.Invoke( null, null );
    112.  
    113.         return projectPath;
    114.     }
    115.  
    116.     public override void OnGUI ( Rect position, SerializedProperty property, GUIContent label )
    117.     {
    118.         if ( position.Contains( ( Event.current.mousePosition ) ) )
    119.         {
    120.             bool leftClickOnEmpty    = Event.current.type == EventType.MouseUp && null == property.objectReferenceValue;
    121.             bool contextClick        = Event.current.type == EventType.ContextClick;
    122.    
    123.             if ( contextClick || leftClickOnEmpty)
    124.             {
    125.                 m_CreatePropertyType = fieldInfo.FieldType;
    126.        
    127.                 if ( m_CreatePropertyType.BaseType == typeof( ScriptableObject ) )
    128.                 {
    129.                     GenericMenu menu = new GenericMenu();
    130.                     menu.AddItem( new GUIContent( "Create" ),            false, HandleCreate, property );
    131.                     menu.AddItem( new GUIContent( "CreateInBrowser" ),    false, HandleCreateInBrowser, property );
    132.                     menu.ShowAsContext();
    133.                 }
    134.             }
    135.         }
    136.  
    137.         EditorGUI.PropertyField( position, property, label, true );
    138.     }
    139. }
    Usage
    Just add the attribute prior to any scriptableobject reference.

    Code (CSharp):
    1. [ContextCreateAsset] public AwesomeScriptableObjectAsset    _myAwesomeAsset;
     
    Last edited: Aug 27, 2018
    TeagansDad, Xarbrough and BinaryCats like this.
  7. BinaryCats

    BinaryCats

    Joined:
    Feb 8, 2016
    Posts:
    317
    thanks for extending this functionality
     
  8. madGlory

    madGlory

    Joined:
    Jan 12, 2016
    Posts:
    44
    I made a small edit to Noisecrime's version:

    Instead of:
    Code (CSharp):
    1. if ( m_CreatePropertyType.BaseType == typeof( ScriptableObject ) )
    I used:
    Code (CSharp):
    1. if (m_CreatePropertyType.IsSubclassOf(typeof(ScriptableObject)))
    This should make it work for any inheritance structure.
     
    Noisecrime and BinaryCats like this.