Search Unity

References to external assets (TextMesh Pro) in asset

Discussion in 'Assets and Asset Store' started by Arsonistic, Jan 7, 2019.

  1. Arsonistic

    Arsonistic

    Joined:
    Dec 11, 2016
    Posts:
    13
    I'm creating an asset involving UI text and my scripts contain references to classes in the TMPro namespace.
    An important distinction in my case is that my asset doesn't require TextMesh Pro, it's only being used for the prettier text, which I would like to be the default.
    Is this a bad idea? Should I reference the respective classes in UnityEngine.UI instead?
    Or is there a convenient way of auto-detecting the existence of TMPro and changing the scripts accordingly?
    Or some simple import-pop-up that shows up when importing the asset, giving you the choice between the two?

    It would be nice to avoid two versions of the scripts that diverge, cuz maintenance pains.
    The references are literally interchangeable, you could do a Find and Replace between the TMPro and UnityEngine.UI classes.

    Any bright ideas? Am I missing something obvious?
     
  2. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    9,006
    It's not obvious. Use compiler conditions to conditionally compile your TMP code only if TMP is present. TMP used to automatically define a scripting symbol TMP_PRESENT, but it no longer does that. You can define it on your own using PlayerSettings.SetScriptingDefineSymbolsForGroup.

    You will probably want to write an editor script with a method that uses UnityEditor.Callbacks.DidReloadScripts. In this method, call PackageManager.Client.List to list what packages are installed. If TMP is installed, set the scripting symbol.

    You could also write an import script that checks once if TMP is installed. If it isn't, ask the user if they want to install it. If so, use PackageManager.Client.Add to install it.
     
    Arsonistic and TeagansDad like this.
  3. Arsonistic

    Arsonistic

    Joined:
    Dec 11, 2016
    Posts:
    13
    Thanks for the response! I was finally able to sit down with it for a while and your response gave me a great place to start.
    I was able to get it working with this:
    Code (CSharp):
    1. using UnityEditor.PackageManager;
    2. sealed class CheckForTextMeshPro {
    3.     const string tmpDefine = "TMP_PRESENT";
    4.  
    5.     [UnityEditor.Callbacks.DidReloadScripts]
    6.     static void CheckForTMPPackage(){
    7.         var asyncRequest = Client.List(true);
    8.         while (!asyncRequest.IsCompleted){} //Idle loop
    9.      
    10.         foreach (var package in asyncRequest.Result){
    11.             if (package.displayName == "TextMesh Pro"){
    12.                 if (package.status == PackageStatus.Available)
    13.                     DefineUtilities.AddDefine(tmpDefine, DefineUtilities.GetValidBuildTargets());
    14.                 else
    15.                     DefineUtilities.RemoveDefine(tmpDefine, DefineUtilities.GetValidBuildTargets());
    16.                 return;
    17.             }
    18.         }
    19.     }
    20. }
    But after milling it over a bit I figured that this solution wasn't greatly elegant and that it wouldn't work with Unity versions older than 2018, so I figured: why not just check for the existence of a namespace?
    Code (CSharp):
    1. sealed class CheckForTextMeshPro {
    2.     const string tmpDefine = "TMP_PRESENT";
    3.  
    4.     [UnityEditor.Callbacks.DidReloadScripts]
    5.     static void CheckForTMPro(){
    6.         var namespaceFound = (from assembly in AppDomain.CurrentDomain.GetAssemblies()
    7.             from type in assembly.GetTypes()
    8.             where type.Namespace == "TMPro"
    9.             select type).Any();
    10.  
    11.         if (namespaceFound)
    12.             DefineUtilities.AddDefine(tmpDefine, DefineUtilities.GetValidBuildTargets());
    13.         else
    14.             DefineUtilities.RemoveDefine(tmpDefine, DefineUtilities.GetValidBuildTargets());
    15.     }
    16. }
    This works like a charm and is significantly faster (4ms vs 250-500ms), which might be relevant since it runs every script reload. Also might be worth doing an "if (EditorApplication.isPlayingOrWillChangePlaymode) return;" at the start of the method since reload also happens when entering play mode.

    And let's of course not forget to include my DefineUtilities class:
    Code (CSharp):
    1. public static class DefineUtilities {
    2.     /// <summary>
    3.     /// ScriptingDefineSymbols are separated into a collection by any of these,
    4.     /// but always written back using index 0.
    5.     /// </summary>
    6.     public static char[] separators = { ';', ' ' };
    7.  
    8.     public static void AddDefine(string _define, IEnumerable<BuildTargetGroup> _buildTargets){
    9.         foreach (var target in _buildTargets){
    10.             var defines = PlayerSettings.GetScriptingDefineSymbolsForGroup(target).Trim();
    11.  
    12.             var list = defines.Split(separators)
    13.                 .Where(x => !string.IsNullOrEmpty(x))
    14.                 .ToList();
    15.  
    16.             if (list.Contains(_define))
    17.                 continue;
    18.  
    19.             list.Add(_define);
    20.             defines = list.Aggregate((a, b) => a + separators[0] + b);
    21.             PlayerSettings.SetScriptingDefineSymbolsForGroup(target, defines);
    22.         }
    23.     }
    24.  
    25.     public static void RemoveDefine(string _define, IEnumerable<BuildTargetGroup> _buildTargets){
    26.         foreach (var target in _buildTargets){
    27.             var defines = PlayerSettings.GetScriptingDefineSymbolsForGroup(target).Trim();
    28.  
    29.             var list = defines.Split(separators)
    30.                 .Where(x => !string.IsNullOrEmpty(x))
    31.                 .ToList();
    32.  
    33.             if (!list.Remove(_define)) //If not in list then no changes needed
    34.                 continue;
    35.  
    36.             defines = list.Aggregate((a, b) => a + separators[0] + b);
    37.             PlayerSettings.SetScriptingDefineSymbolsForGroup(target, defines);
    38.         }
    39.     }
    40.  
    41.     public static IEnumerable<BuildTargetGroup> GetValidBuildTargets(){
    42.         return Enum.GetValues(typeof(BuildTargetGroup))
    43.             .Cast<BuildTargetGroup>()
    44.             .Where(x => x != BuildTargetGroup.Unknown)
    45.             .Where(x => !IsObsolete(x));
    46.     }
    47.  
    48.     public static bool IsObsolete(BuildTargetGroup group){
    49.         var obsoleteAttributes = typeof(BuildTargetGroup)
    50.             .GetField(group.ToString())
    51.             .GetCustomAttributes(typeof(ObsoleteAttribute), false);
    52.  
    53.         return obsoleteAttributes != null && obsoleteAttributes.Length > 0;
    54.     }
    55. }
    For includes, these cover all the classes:
    Code (CSharp):
    1. using System;
    2. using System.Linq;
    3. using System.Collections.Generic;
    4. using UnityEditor;
    On that topic, you wouldn't happen to know the difference between DidReloadScripts and InitializeOnLoadMethod? From my testing I have been unable to tell any difference between the two, and the cryptic info in the docs isn't helping much.
    Been googling for this, but can't seem to find any info on how to write 'import' scripts. Are you referring to some kind of AssetPostprocessor or ScriptedImporter? Because those don't seem quite like they would be useful for this sort of thing.
     
  4. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    9,006
    In almost all cases, they both get called. I think when you first start the Unity editor, if there are compiler errors then InitializeOnLoad gets called but DidReloadScripts doesn't. It's been a while since I played with that, but it would be an easy experiment to run.

    Use PackageManager.Client.Add.
     
  5. Arsonistic

    Arsonistic

    Joined:
    Dec 11, 2016
    Posts:
    13
    Tried it and neither was called.
    Oooooh, that's what you meant. I thought you meant using some kind of script that executes on package import, not a script that does the import :confused:

    If I were to construct such a check to only run once, where would be the best place to store the fact that it has been checked? Playerprefs is easy, but might also be wiped, re-triggering the popup. Data in an asset file would work, but I'd prefer to avoid making changes to assets.
     
    Last edited: Jan 12, 2019
  6. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    9,006
    I use EditorPrefs. And if it gets wiped, it does the check once, silently notes that no change is needed, and sets EditorPrefs again.
     
    Arsonistic likes this.
  7. Arsonistic

    Arsonistic

    Joined:
    Dec 11, 2016
    Posts:
    13
    Took another look at DidReloadScripts, InitializeOnLoad and InitializeOnLoadMethod and found one difference:
    InitializeOnLoad executes before InitializeOnLoadMethod, which in turn executes before DidReloadScripts, and DidReloadScripts can further specify order relative to others with the same attribute through an input.
    I.e: [UnityEditor.Callbacks.DidReloadScripts(-1)] executes before [UnityEditor.Callbacks.DidReloadScripts(0)], which in turn executes before [UnityEditor.Callbacks.DidReloadScripts(1)] ( [UnityEditor.Callbacks.DidReloadScripts] defaults to 0), but regardless of the number it always executes after InitializeOnLoadMethod and InitializeOnLoad.

    Summarized execution order:
    [InitializeOnLoad] -> [InitializeOnLoadMethod] -> [UnityEditor.Callbacks.DidReloadScripts(-1)] -> [UnityEditor.Callbacks.DidReloadScripts] -> [UnityEditor.Callbacks.DidReloadScripts(1)]
     
    TonyLi likes this.