Last active
September 7, 2023 17:10
-
-
Save dogfuntom/00faf9f3598d845b1a5c to your computer and use it in GitHub Desktop.
[Unity CodeDom] Editor-time code generator. Generates constant-declaration-only classes for Tags, Layers, Sorting layers and Input axes. Good for type-safety. #Unity #Unity3d #CodeDom
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Microsoft.CSharp; | |
using System.CodeDom; | |
using System.CodeDom.Compiler; | |
using System.Collections.Generic; | |
using System.Collections.ObjectModel; | |
using System.Collections.Specialized; | |
using System.IO; | |
using System.Linq; | |
using System.Reflection; | |
using System.Text; | |
using UnityEditor; | |
using UnityEditorInternal; | |
using UnityEngine; | |
public class CodeGenerator_Window : EditorWindow | |
{ | |
private string axesPath = @"Scripts/Auto/Axes.cs"; | |
private string tagsPath = @"Scripts/Auto/Tags.cs"; | |
private string sortingLayersPath = @"Scripts/Auto/SortingLayers.cs"; | |
private string layersPath = @"Scripts/Auto/Layers.cs"; | |
[MenuItem("Window/Code Generator")] | |
private static void CallCreateWindow() | |
{ | |
// Get existing open window or if none, make a new one: | |
CodeGenerator_Window window = (CodeGenerator_Window)EditorWindow.GetWindow(typeof(CodeGenerator_Window)); | |
window.autoRepaintOnSceneChange = true; | |
window.title = "Code Generator"; | |
window.Show(); | |
} | |
private void OnInspectorUpdate() | |
{ | |
this.Repaint(); | |
} | |
private void OnGUI() | |
{ | |
DrawGenerationGui("Input", ref this.axesPath, GetAllAxisNames); | |
EditorGUILayout.Separator(); | |
DrawGenerationGui("Tags", ref this.tagsPath, GetAllTags); | |
EditorGUILayout.Separator(); | |
DrawGenerationGui("Sorting layers", ref this.sortingLayersPath, GetAllSortingLayers); | |
EditorGUILayout.Separator(); | |
DrawGenerationGui("Layers", ref this.layersPath, GetAllLayers); | |
} | |
private static void DrawGenerationGui(string title, ref string path, System.Func<IEnumerable<string>> namesProvider) | |
{ | |
EditorGUILayout.BeginVertical(EditorStyles.inspectorFullWidthMargins); | |
EditorGUILayout.LabelField(title, EditorStyles.boldLabel); | |
EditorGUILayout.PrefixLabel(@"Path: /Assets/... + "); | |
path = EditorGUILayout.TextField(path, EditorStyles.textField); | |
EditorGUILayout.BeginHorizontal(); | |
GUILayout.FlexibleSpace(); | |
if (GUILayout.Button("Generate", EditorStyles.miniButton)) | |
{ | |
try | |
{ | |
GenerateAndForceImport(path, namesProvider); | |
System.GC.Collect(); | |
} | |
catch (System.Exception ex) | |
{ | |
Debug.LogException(ex); | |
} | |
} | |
GUILayout.FlexibleSpace(); | |
EditorGUILayout.EndHorizontal(); | |
EditorGUILayout.EndVertical(); | |
} | |
#region code generation | |
private static void GenerateAndForceImport(string path, System.Func<IEnumerable<string>> namesProvider) | |
{ | |
var fullPath = Path.Combine(Application.dataPath, path); | |
var names = namesProvider(); | |
if (names.Any()) | |
{ | |
GenerateNamesCodeFile(fullPath, names); | |
AssetDatabase.ImportAsset(fullPath, ImportAssetOptions.ForceUpdate); | |
AssetDatabase.Refresh(); | |
} | |
else | |
EditorUtility.DisplayDialog("No data", "No names found.", "Close"); | |
} | |
private static void GenerateNamesCodeFile(string fullPath, IEnumerable<string> names) | |
{ | |
var name = Path.GetFileNameWithoutExtension(fullPath); | |
var constants = names.ToDictionary(s => ConvertToValidIdentifier(s), s => s); | |
var code = CreateStringConstantsClass(name, constants); | |
Directory.CreateDirectory(Path.GetDirectoryName(fullPath)); | |
using (var stream = new StreamWriter(fullPath, append: false)) | |
{ | |
var tw = new IndentedTextWriter(stream); | |
var codeProvider = new CSharpCodeProvider(); | |
codeProvider.GenerateCodeFromCompileUnit(code, tw, new CodeGeneratorOptions()); | |
} | |
} | |
private static CodeCompileUnit CreateStringConstantsClass( | |
string name, | |
IDictionary<string, string> constants) | |
{ | |
var compileUnit = new CodeCompileUnit(); | |
var @namespace = new CodeNamespace(); | |
var @class = new CodeTypeDeclaration(name); | |
ImitateStaticClass(@class); | |
foreach (var pair in constants) | |
{ | |
var @const = new CodeMemberField( | |
typeof(string), | |
pair.Key); | |
@const.Attributes &= ~MemberAttributes.AccessMask; | |
@const.Attributes &= ~MemberAttributes.ScopeMask; | |
@const.Attributes |= MemberAttributes.Public; | |
@const.Attributes |= MemberAttributes.Const; | |
@const.InitExpression = new CodePrimitiveExpression(pair.Value); | |
@class.Members.Add(@const); | |
} | |
@namespace.Types.Add(@class); | |
compileUnit.Namespaces.Add(@namespace); | |
return compileUnit; | |
} | |
/// <summary> | |
/// Marks class as sealed and adds private constructor to it. | |
/// </summary> | |
/// <remarks> | |
/// It's not possible to create static class using CodeDom. | |
/// Creating abstract sealed class instead leads to compilation error. | |
/// This method can be used instead to make pseudo-static class. | |
/// </remarks> | |
private static void ImitateStaticClass(CodeTypeDeclaration type) | |
{ | |
@type.TypeAttributes |= TypeAttributes.Sealed; | |
@type.Members.Add(new CodeConstructor | |
{ | |
Attributes = MemberAttributes.Private | MemberAttributes.Final | |
}); | |
} | |
private static string ConvertToValidIdentifier(string name) | |
{ | |
var sb = new StringBuilder(name.Length + 1); | |
if (!char.IsLetter(name[0])) | |
sb.Append('_'); | |
var makeUpper = false; | |
foreach (var ch in name) | |
{ | |
if (char.IsLetterOrDigit(ch)) | |
{ | |
sb.Append(makeUpper | |
? char.ToUpperInvariant(ch) | |
: ch); | |
makeUpper = false; | |
} | |
else if (char.IsWhiteSpace(ch)) | |
{ | |
makeUpper = true; | |
} | |
else | |
{ | |
sb.Append('_'); | |
} | |
} | |
return sb.ToString(); | |
} | |
#endregion | |
#region names providers | |
private static IEnumerable<string> GetAllAxisNames() | |
{ | |
var result = new StringCollection(); | |
var serializedObject = new SerializedObject(AssetDatabase.LoadAllAssetsAtPath("ProjectSettings/InputManager.asset")[0]); | |
var axesProperty = serializedObject.FindProperty("m_Axes"); | |
axesProperty.Next(true); | |
axesProperty.Next(true); | |
while (axesProperty.Next(false)) | |
{ | |
SerializedProperty axis = axesProperty.Copy(); | |
axis.Next(true); | |
result.Add(axis.stringValue); | |
} | |
return result.Cast<string>().Distinct(); | |
} | |
private static IEnumerable<string> GetAllTags() | |
{ | |
return new ReadOnlyCollection<string>(InternalEditorUtility.tags); | |
} | |
private static IEnumerable<string> GetAllSortingLayers() | |
{ | |
var internalEditorUtilityType = typeof(InternalEditorUtility); | |
var sortingLayersProperty = internalEditorUtilityType.GetProperty("sortingLayerNames", BindingFlags.Static | BindingFlags.NonPublic); | |
var sortingLayers = (string[])sortingLayersProperty.GetValue(null, new object[0]); | |
return new ReadOnlyCollection<string>(sortingLayers); | |
} | |
private static IEnumerable<string> GetAllLayers() | |
{ | |
return new ReadOnlyCollection<string>(InternalEditorUtility.layers); | |
} | |
#endregion | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ------------------------------------------------------------------------------ | |
// <autogenerated> | |
// This code was generated by a tool. | |
// Mono Runtime Version: 2.0.50727.1433 | |
// | |
// Changes to this file may cause incorrect behavior and will be lost if | |
// the code is regenerated. | |
// </autogenerated> | |
// ------------------------------------------------------------------------------ | |
public sealed class Axes { | |
public const string Horizontal = "Horizontal"; | |
public const string Vertical = "Vertical"; | |
public const string Fire1 = "Fire1"; | |
public const string Fire2 = "Fire2"; | |
public const string Fire3 = "Fire3"; | |
public const string Jump = "Jump"; | |
public const string MouseX = "Mouse X"; | |
public const string MouseY = "Mouse Y"; | |
public const string MouseScrollWheel = "Mouse ScrollWheel"; | |
public const string Submit = "Submit"; | |
public const string Cancel = "Cancel"; | |
private Axes() { | |
} | |
} |
I added your comment as a quote to the Readme.
I also added UPM support to the GitHub repo, so it can be used directly with https://github.com/mob-sakai/UpmGitExtension.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
There's also my post:
Russian or
Auto-translated into English
The post itself is not too helpful (it's faster to understand the code directly than to read a ton of words about it). But the criticism of my approach in comments is interesting and has valid points.