Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Code generation with Unity

Discussion in 'Scripting' started by liortal, Mar 11, 2014.

  1. liortal

    liortal

    Joined:
    Oct 17, 2012
    Posts:
    3,562
    Last edited: Apr 13, 2018
  2. Deleted User

    Deleted User

    Guest

    Nice one!
    but I'm not a Fan of using 3rd Party programms all over the place. And because I was bored anyways I wrote this little script that does the same without the use of T4. This is also fully automatic. Just drop it in \Assets\Editor\ , and you are good to go.

    Code (csharp):
    1.  
    2. using System.IO;
    3. using System.Text;
    4. using UnityEditor;
    5. using UnityEngine;
    6. using UnityEditorInternal;
    7.  
    8. [InitializeOnLoad]
    9. public static class TagCodeGenerator
    10. {
    11.     // an array that hold all tags
    12.     private static string[] _tags;
    13.     // a flag if the dataset has changed
    14.     private static bool _hasChanged = false;
    15.     // time when we start to count
    16.     private static double _startTime = 0.0;
    17.     // the time that should elapse between the change of tags and the File write
    18.     // this is importend because changed are triggered as soon as you start typing and this can cause lag
    19.     private static double _timeToWait = 1.0;
    20.  
    21.     static TagCodeGenerator()
    22.     {
    23.         //subscripe to event
    24.         EditorApplication.update += Update;
    25.         // get tags
    26.         _tags = InternalEditorUtility.tags;
    27.         // write file
    28.         WriteCodeFile();
    29.        
    30.     }
    31.    
    32.     private static void Update()
    33.     {
    34.         // returns if we are in play mode
    35.         if( Application.isPlaying == true )
    36.             return;
    37.  
    38.         Wait();
    39.  
    40.         // temp array that hold new tags
    41.         string[] newTags = InternalEditorUtility.tags;
    42.         // check if the lenght is not the same
    43.         if ( newTags.Length != _tags.Length )
    44.         {
    45.             _tags = newTags;
    46.             _hasChanged = true;
    47.             _startTime = EditorApplication.timeSinceStartup;
    48.             return;
    49.         }
    50.         else
    51.         {
    52.             // loop thru all new tags and compare them to the old ones
    53.             for( int i = 0; i < newTags.Length; i++ )
    54.             {
    55.                 if( string.Equals( newTags[i], _tags[i] ) == false)
    56.                 {
    57.                     _tags = newTags;
    58.                     _hasChanged = true;
    59.                     _startTime = EditorApplication.timeSinceStartup;
    60.                     return;
    61.                 }
    62.             }
    63.         }
    64.  
    65.     }
    66.  
    67.     private static void Wait()
    68.     {
    69.         // if nothing has changed return
    70.         if(_hasChanged == false)
    71.             return;
    72.  
    73.         // if the time delta between now and the last change, is greater than the time we schould wait Than write the file
    74.         if (EditorApplication.timeSinceStartup - _startTime > _timeToWait)
    75.         {
    76.             WriteCodeFile();
    77.             _hasChanged = false;
    78.         }
    79.     }
    80.  
    81.  
    82.     // writes a file to the project folder
    83.     private static void WriteCodeFile()
    84.     {
    85.  
    86.         // the path we want to write to
    87.         string path = string.Concat( Application.dataPath, Path.DirectorySeparatorChar, "Tags.cs" );
    88.  
    89.         try
    90.         {
    91.             // opens the file if it allready exists, creates it otherwise
    92.             using ( FileStream stream = File.Open( path, FileMode.OpenOrCreate, FileAccess.Write ) )
    93.             {
    94.                 using( StreamWriter writer = new StreamWriter(stream) )
    95.                 {
    96.                     StringBuilder builder = new StringBuilder();
    97.                     builder.AppendLine("// ----- AUTO GENERATED CODE ----- //");
    98.                     builder.AppendLine("public static class Tags");
    99.                     builder.AppendLine("{");
    100.                     foreach(string tag in _tags)
    101.                     {
    102.                         builder.AppendLine( string.Format("\tpublic static readonly string {0} = \"{0}\";", tag));
    103.                     }
    104.                    
    105.                     builder.AppendLine("}");
    106.                     writer.Write( builder.ToString() );
    107.                 }
    108.             }
    109.         }
    110.         catch(System.Exception e)
    111.         {
    112.             Debug.LogException( e );
    113.  
    114.             // if we have an error, it is certainly that the file is screwed up. Delete to be save
    115.             if(File.Exists( path ) == true)
    116.                 File.Delete( path );
    117.         }
    118.  
    119.         AssetDatabase.Refresh();
    120.     }
    121. }
    122.  
     
    destructor465 likes this.
  3. liortal

    liortal

    Joined:
    Oct 17, 2012
    Posts:
    3,562
    Your code goes the extra mile to build the tags class automatically whenever there's an update, nice.

    Just to be clear, my solution uses T4 preprocessed templates, meaning that my template is a self-contained class that can be used at runtime, without any dependency on T4 whatsoever.

    The class itself already contains the text output of how to format a class, which "placeholders" that are replaced with the real tags that will be generated.
     
  4. blueknee

    blueknee

    Joined:
    Apr 5, 2014
    Posts:
    8
    I know this is old topic, but since google brings me here I found it worthy that share my solution, which expanded element_wsc's one.
    Here is the repository.

    https://github.com/srndpty/CodeGen

    Notice or PR me if you find something to be improved.
     
    sztobar likes this.
  5. Tkrain42

    Tkrain42

    Joined:
    Jul 25, 2015
    Posts:
    43
    I used this as a basis to create an auto generated enum from scriptable objects with stat names in them. This gives the designer a simple way to add enum values to the stats without needing to go into code (and the custom editor behaves in much the same way that Unity's tag and layer editor works, but that's another story.
    Thanks for this!
     
  6. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,520
    Oooooh, be careful! Are you emitting hard numbers for those as well as ensuring that those numbers never change into the future, surviving intact through removals and new additions? You should be!

    If your enum-emitter blasts them all out unnumbered and their order changes, you will be mysteriously very sad.

    In general, enums are bad bad bad in Unity3D if you intend them to be serialized. Here's why:

    https://forum.unity.com/threads/bes...if-not-do-something-else.972093/#post-6323361

    https://forum.unity.com/threads/unity-card-game-structure.1006826/#post-6529526

    It is much better to use ScriptableObjects for many enumerative uses. You can even define additional associated data with each one of them, and drag them into other parts of your game (scenes, prefabs, other ScriptableObjects) however you like. References remain rock solid even if you rename them, reorder them, reorganize them, etc. They are always connected via the meta file GUID.

    Collections / groups of ScriptableObjects can also be loaded en-masse with calls such as
    Resources.LoadAll<T>().


    Best of all, Unity already gives you a built-in filterable picker when you click on the little target dot to the right side of a field of any given type... bonus!
     
  7. Tkrain42

    Tkrain42

    Joined:
    Jul 25, 2015
    Posts:
    43
    Ordinarily, I would agree with that, and have actually used systems in the past for exactly that purpose (actually for my personal implementation of the RPG project from the course I'm a TA for). This particular setup is for integrating into a course that is specifically using Enums for stats, and some of those need to be fixed in stone. I've rigged the Editor to always include about a dozen "standard" stats, and once you've added a stat, you cannot delete it. (You can rename it, but the renamed stat would have the same ordinal number as the previous stat). Since the students will be starting with the entire project centered around an enum based stat, it would require more time explaining how to make ScriptableStats that are retrievable at runtime, and dealing with students who forgot that yes, they do need to attach the Health ScriptableStat to their Health component or it won't work, etc) than it will be to provide this StatEditor with a brief note on how to use it.
     
    Kurt-Dekker likes this.
  8. Tkrain42

    Tkrain42

    Joined:
    Jul 25, 2015
    Posts:
    43
    Actually, thinking about it, it wouldn't be that hard to add an auto-numbering scheme to the SO and editor, and then have the enum include the hard serialized number. And I do agree, enums in general have some issues when people go in and muck about changing/adding/subtracting (ugh) members over time. That's part of why I'm putting this together, because students have run head into the downsides of it.
     
  9. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,520
    Oh no, no, no, no, no! That completely defeats the purpose!!!

    Not only that but you have just opened your codebase up to a MASSIVE collection of hard-to-find pernicious bugs if you place any kind of duplicate primary indentifier inside a ScriptableObject!!!

    - the identifier changes
    - identifier collides with another
    - identifier incorrect
    - someone copies that identifier out, then it changes
    - someone copies that identifier out, but you realize you need to change it
    - etc...

    These are really really HARD AWFUL bugs to be avoided at all costs! These are the bugs that sink entire projects.

    The entire point of the ScriptableObject instance itself is that it IS the identifier.

    It is NOT that it CONTAINS an identifer, that would be pointless.

    The single point of truth (SPOT) is the unique instance of that ScriptableObject.

    The identifier IS the ScriptableObject instance. Full stop. Any other way lies madness and bugs.

    As a long-time engineer, I have committed and observed and repaired and retrofitted programs with ALL of these bugs. This is a class of bug along the lines of running with scissors or playing tiddlywinks in the middle of the freeway. AVOID!
     
  10. Tkrain42

    Tkrain42

    Joined:
    Jul 25, 2015
    Posts:
    43
    There is ONE scriptableObject with the names of the enum and an index, basically a flat table with an autonumber like you would find in just about any database. The enum is generated reading that scriptableObject as soon as any entry changes. Entries cannot be deleted, and assigned numbers are read only. The editor only works on one instance of the ScriptableObject. The only way for the user to edit the index would be to go into the Yaml and change the number (and let's face it, if they're that dumb...).

    This is for a specific project, to make the easiest integration possible. To change the codebase from the existing Stat to a ScriptableStat for each stat is a massive undertaking. I know, because I have done it (and it works just fine), but I'll leave 90% of my students behind explaining the changes. If and when we rewrite the courses, I'll have a hand in it, and will be pushing to have all of the enums in the course replaced with ScriptableObjects.
     
    Kurt-Dekker likes this.