Search Unity

  1. Unity 6 Preview is now available. To find out what's new, have a look at our Unity 6 Preview blog post.
    Dismiss Notice
  2. Unity is excited to announce that we will be collaborating with TheXPlace for a summer game jam from June 13 - June 19. Learn more.
    Dismiss Notice
  3. Dismiss Notice

How to implement "Create new asset"?

Discussion in 'Editor & General Support' started by Peter77, Oct 12, 2019.

  1. Peter77

    Peter77

    QA Jesus

    Joined:
    Jun 12, 2013
    Posts:
    6,640
    I've implemented a ScriptedImporter for my own file-type and I would like to add a menu item to the Unity "Create" menu, which can be used to create an asset of that file-type.

    However, I can't figure out how I can do this, so that it feels the same as for Unity assets.

    In Unity, if I choose "Assets > Create > Material" for example, Unity creates a "ghost asset" in the selected folder, which can be renamed before the actual asset is created. If I press ESC, this ghost asset disappears. If I press Enter, Unity actually creates the material.

    I would like to have this functionality for my own file-type too.

    My current workaround is to use [MenuItem] to add an item to the "Assets > Create" menu and then create an asset in the selected directory, where this was called from. However, this creates the asset immediately, rather than the "ghost asset" as Unity does and it does not feel like it's well integrated in Unity.
    Code (CSharp):
    1. [MenuItem("Assets/Create/Monkey", priority = 310)]
    2. static void DoMenuItem()
    3. {
    4.     // Get selected folder in Unity
    5.     Object target = Selection.activeObject;
    6.     string path = AssetDatabase.GetAssetPath(target);
    7.     string folder = File.Exists(path) ? Path.GetDirectoryName(path) : path;
    8.  
    9.     // Get all existing files of our own type in this folder
    10.     List<string> existingNames = new List<string>();
    11.     foreach (string p in Directory.GetFiles(folder, "*.monkey", SearchOption.TopDirectoryOnly))
    12.         existingNames.Add(Path.GetFileNameWithoutExtension(p));
    13.  
    14.     // Generate an unique "New Monkey" name
    15.     string name = ObjectNames.GetUniqueName(existingNames.ToArray(), "New Monkey");
    16.     string outputPath = Path.Combine(folder, name + ".monkey").Replace("\\", "/");
    17.  
    18.     // Write the actual asset
    19.     File.WriteAllText(outputPath, "");
    20.  
    21.     // Import and select just generated asset
    22.     AssetDatabase.ImportAsset(outputPath);
    23.     Selection.activeObject = AssetDatabase.LoadAssetAtPath(outputPath, typeof(Object));
    24. }

    I looked at [CreateAssetMenu] too, but it must be used on a class which is then created, such as a ScriptableObject. I don't have source code access this type, I write a ScriptedImporter for a type found in another library, so I don't have the source code to simply add this attribute to that type.
     
    Xarbrough likes this.
  2. Coincidentally I have spent some time on this recently. What I could figure out is this:
    Code (CSharp):
    1.        public static string GetSelectedPathOrFallback()
    2.         {
    3.             string path = "Assets";
    4.             foreach (Object obj in Selection.GetFiltered(typeof(Object), SelectionMode.Assets))
    5.             {
    6.                 path = AssetDatabase.GetAssetPath(obj);
    7.                 if ( !string.IsNullOrEmpty(path) && File.Exists(path) )
    8.                 {
    9.                     path = Path.GetDirectoryName(path);
    10.                     break;
    11.                 }
    12.             }
    13.             return path + "/";
    14.         }
    Code (CSharp):
    1. string filePath = AssetDatabase.GenerateUniqueAssetPath(GetSelectedPathOrFallback() + "NewFile.ext");
    2. ProjectWindowUtil.CreateAssetWithContent(filePath, contentAsString, texture2DOptional);
    This creates the usual asset in the project window with icon (optional) and with default filename. Then it starts to edit the file name. If you hit the ESC, the file will be disposed, if you hit enter (with or without a new file name), the file will be created with the contentAsString content.

    Oh, and this provides the "NewFile1.ext" automatically if you already have "NewFile.ext".
     
    Last edited by a moderator: Oct 13, 2019
    Xarbrough, Kirsche and Peter77 like this.
  3. Peter77

    Peter77

    QA Jesus

    Joined:
    Jun 12, 2013
    Posts:
    6,640
    That's just great, it's exactly doing what I was looking for. Thanks so much! I didn't even know
    ProjectWindowUtil
    existed :)
     
    Lurking-Ninja likes this.
  4. Peter77

    Peter77

    QA Jesus

    Joined:
    Jun 12, 2013
    Posts:
    6,640
    They also have the ProjectWindowUtil.GetActiveFolderPath method, but it's internal only. It's so unfortunate, their code base contains so many useful API's, but only a fraction seems to be exposed.
     
  5. If you really want it:
    Code (CSharp):
    1. Type pwuType = typeof(ProjectWindowUtil);
    2. string path = Convert.ToString(pwuType.GetMethod("GetActiveFolderPath", BindingFlags.NonPublic | BindingFlags.Static).Invoke(null, null));
    Although I haven't tested if you don't have the Project window open, I guess it would throw a nasty null exception.
     
  6. richard_harrington

    richard_harrington

    Unity Technologies

    Joined:
    Sep 29, 2020
    Posts:
    23
    For anybody who comes here looking, it would appear that ProjectWindowUtil.CreateAssetWithContent method fills this need - replicating the native Unity create-asset behavior:

    Code (CSharp):
    1. [MenuItem("Assets/Create/My Custom Asset Type", false, 1)]
    2. private static void CreateNewAsset()
    3. {
    4.     ProjectWindowUtil.CreateAssetWithContent(
    5.         "Default Name.extension",
    6.         string.Empty);
    7. }
     
    Nyarlathothep, ValakhP and Peter77 like this.
  7. Trisibo

    Trisibo

    Joined:
    Nov 1, 2010
    Posts:
    248
    Just today I needed something similar for ScriptableObjects, but I also needed to be notified when the asset was actually "created" (i.e., when the user accepted the name and the asset was stored in the asset database). As far as I know, "ProjectWindowUtil.CreateAsset" can't be used for that, it just returns immediately, but the asset isn't in the database at that point.

    So at the end I've got to make my own implementation, with "created" and "canceled" callbacks. It only works for ScriptableObjects, but with some minor modifications it can be used for custom assets as well:

    Code (CSharp):
    1. using System;
    2. using UnityEngine;
    3. using UnityEditor;
    4. using UnityEditor.ProjectWindowCallback;
    5.  
    6. public static class AssetCreator
    7. {
    8.     /// <summary>
    9.     /// Creates an asset in the current folder in the project window,
    10.     /// with its name being editable by the user.
    11.     /// Similar to <see cref="ProjectWindowUtil.CreateAsset"/>, but with added callbacks for creation and cancelation.
    12.     /// </summary>
    13.     /// <typeparam name="T">The type of asset.</typeparam>
    14.     /// <param name="initialAssetName">The initial asset name, which the user may change. If it doesn't have a ".asset" extension, it will be appended.</param>
    15.     /// <param name="onCreated">Optional callback to execute when the asset is created, after the player has accepted the name.</param>
    16.     /// <param name="onCanceled">Optional callback to execute when the action is canceled.</param>
    17.  
    18.     public static void CreateAssetInCurrentFolder<T>(string initialAssetName, Action<T> onCreated = null, Action onCanceled = null)
    19.         where T : ScriptableObject
    20.     {
    21.         // Process the asset name:
    22.         if (string.IsNullOrWhiteSpace(initialAssetName))
    23.             initialAssetName = "New " + ObjectNames.NicifyVariableName(typeof(T).Name);
    24.  
    25.         const string requiredExtension = ".asset";
    26.  
    27.         if (!initialAssetName.EndsWith(requiredExtension, StringComparison.InvariantCultureIgnoreCase))
    28.             initialAssetName += requiredExtension;
    29.  
    30.  
    31.         // Set up the end name edit action callback object:
    32.         var endNameEditAction = ScriptableObject.CreateInstance<AssetCreatorEndNameEditAction>();
    33.  
    34.         endNameEditAction.canceledCallback = onCanceled;
    35.  
    36.         if (onCreated != null)
    37.             endNameEditAction.createdCallback = (_instance) => onCreated((T)_instance);
    38.    
    39.  
    40.         // Create the asset:
    41.         T asset = ScriptableObject.CreateInstance<T>();
    42.         ProjectWindowUtil.StartNameEditingIfProjectWindowExists(asset.GetInstanceID(), endNameEditAction, initialAssetName, AssetPreview.GetMiniThumbnail(asset), null);
    43.     }
    44.  
    45.  
    46.  
    47.  
    48.     /// <summary>
    49.     /// Class to be used as the end name edit action callback on <see cref="ProjectWindowUtil.StartNameEditingIfProjectWindowExists"/>.
    50.     /// </summary>
    51.  
    52.     private class AssetCreatorEndNameEditAction : EndNameEditAction
    53.     {
    54.         public Action<UnityEngine.Object> createdCallback;
    55.         public Action canceledCallback;
    56.  
    57.  
    58.         ///          
    59.         /// <inheritdoc/>
    60.         ///          
    61.  
    62.         public override void Action(int instanceId, string pathName, string resourceFile)
    63.         {
    64.             var asset = EditorUtility.InstanceIDToObject(instanceId);
    65.             AssetDatabase.CreateAsset(asset, AssetDatabase.GenerateUniqueAssetPath(pathName));
    66.  
    67.             createdCallback?.Invoke(asset);
    68.         }
    69.  
    70.  
    71.         ///          
    72.         /// <inheritdoc/>
    73.         ///          
    74.  
    75.         public override void Cancelled(int instanceId, string pathName, string resourceFile)
    76.         {
    77.             Selection.activeObject = null;
    78.  
    79.             canceledCallback?.Invoke();
    80.         }
    81.     }
    82. }
     
    Last edited: Jul 7, 2022
    viridisamor likes this.
  8. AlexRoseGames

    AlexRoseGames

    Joined:
    Jul 13, 2015
    Posts:
    43
    Any idea if it's possible to get the new name of the object after it's been renamed? I want to make a new type of "create script" but the contents (which you put as string.Empty there) should contain the name you make. so e.g.

    MyNewName: MonoBehaviour

    should be a line in the script. but right now it does all that beneath the hood
     
  9. AlexRoseGames

    AlexRoseGames

    Joined:
    Jul 13, 2015
    Posts:
    43
    nvm thought of a solution

    Code (CSharp):
    1. const string CSharpScript = "[script template goes here with $1 instead of the script name]";
    2.  
    3. static void SelectionHook()
    4. {
    5.     if (Selection.activeObject != null)
    6.     {
    7.         Selection.selectionChanged -= SelectionHook;
    8.         string assetPath = AssetDatabase.GetAssetPath(Selection.activeObject);
    9.  
    10.         string scriptName = Path.GetFileNameWithoutExtension(assetPath);
    11.         File.WriteAllText(assetPath, CSharpScript.Replace("$1", scriptName));
    12.         //AssetDatabase.Refresh(); <-- not strictly necessary
    13.     }
    14. }
    15.  
    16. [MenuItem("Assets/Create/Custom C# Script", false, 1)]
    17. public static void CreateCustomScript()
    18. {
    19.     // Create a new script file
    20.     string scriptName = "NewPreloader.cs";
    21.  
    22.  
    23.  
    24.     ProjectWindowUtil.CreateAssetWithContent(
    25.         scriptName,
    26.         "");
    27.  
    28.     Selection.selectionChanged += SelectionHook;
    29. }