Skip to content

Instantly share code, notes, and snippets.

@MattRix
Last active February 14, 2025 17:52
Show Gist options
  • Save MattRix/f174836be788124b80d99c447df46ec5 to your computer and use it in GitHub Desktop.
Save MattRix/f174836be788124b80d99c447df46ec5 to your computer and use it in GitHub Desktop.
Generate classes for prefabs so that you get named and typed references to their components and children.

Right-click on a prefab and choose "Generate Prefab Class" to get a base class with references to the prefab's components and children (it uses the first component of each child).

Use these prefab classes as base classes for your own code. For example, if you have a prefab called "Ball", this tool will generate "BallPrefab.cs" next to it. You can then make your own script "Ball.cs" which extends from BallPrefab, giving you properly typed references to all of its components and children.

You can select multiple prefabs (or even an entire folder) to generate a bunch of classes at once.

If the object has multiple children with the exact same name, it'll generate a list reference to them instead.

#if UNITY_EDITOR
using UnityEngine;
using UnityEditor;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
public class PrefabClassGenerator
{
// If OUTPUT_FOLDER is empty, generated scripts will be placed next to their prefab.
//static string OUTPUT_FOLDER = "Assets/PrefabClasses/";
static string OUTPUT_FOLDER = "";
[MenuItem("Assets/Generate Prefab Class", validate = true)]
private static bool ValidateGeneratePrefabClass()
{
var prefabs = FindAllPrefabsInSelection();
return prefabs.Count > 0;
}
[MenuItem("Assets/Generate Prefab Class", false, 1)]
private static void GeneratePrefabClass()
{
var prefabs = FindAllPrefabsInSelection();
foreach(var prefab in prefabs)
{
CreatePrefabClass(prefab);
}
AssetDatabase.Refresh();
}
private static List<GameObject> FindAllPrefabsInSelection()
{
List<GameObject> prefabs = new List<GameObject>();
if(Selection.activeObject != null)
{
var folderPath = AssetDatabase.GetAssetPath(Selection.activeObject);
//if a folder is selected, find all prefabs in the folder recursively
if(AssetDatabase.IsValidFolder(folderPath))
{
string[] assetGUIDs = AssetDatabase.FindAssets("", new[] { folderPath });
foreach (string guid in assetGUIDs)
{
string assetPath = AssetDatabase.GUIDToAssetPath(guid);
var go = AssetDatabase.LoadAssetAtPath<GameObject>(assetPath);
if(go != null)
{
prefabs.Add(go);
}
}
}
else //find all the prefabs in our current selected objects
{
foreach(var go in Selection.gameObjects)
{
if(PrefabUtility.IsPartOfPrefabAsset(go))
{
prefabs.Add(go);
}
}
}
}
return prefabs;
}
[MenuItem("Tools/Generate Classes For All Prefabs")]
public static void GenerateClassesForPrefabs()
{
if (!string.IsNullOrEmpty(OUTPUT_FOLDER) && !Directory.Exists(OUTPUT_FOLDER))
{
Directory.CreateDirectory(OUTPUT_FOLDER);
AssetDatabase.Refresh();
}
string[] guids = AssetDatabase.FindAssets("t:Prefab", new[] { "Assets" });
Debug.Log($"Found {guids.Length} prefabs.");
foreach (string guid in guids)
{
string prefabPath = AssetDatabase.GUIDToAssetPath(guid);
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
if (prefab == null)
{
Debug.LogWarning($"Could not load prefab at path: {prefabPath}");
continue;
}
CreatePrefabClass(prefab);
}
AssetDatabase.Refresh();
}
static void CreatePrefabClass(GameObject prefab)
{
string className = SanitizeClassName(prefab.name) + "Prefab";
string folderPath = OUTPUT_FOLDER;
if (string.IsNullOrEmpty(OUTPUT_FOLDER))
{
string prefabAssetPath = AssetDatabase.GetAssetPath(prefab);
folderPath = Path.GetDirectoryName(prefabAssetPath);
}
string filePath = Path.Combine(folderPath, className + ".cs");
string code = GenerateCodeForPrefab(prefab, className);
File.WriteAllText(filePath, code);
Debug.Log($"Generated {filePath}");
}
private static string GenerateCodeForPrefab(GameObject prefab, string className)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine("using UnityEngine;");
sb.AppendLine("using System.Collections.Generic;");
sb.AppendLine();
sb.AppendLine($"public class {className} : MonoBehaviour");
sb.AppendLine("{");
HashSet<string> usedNames = new HashSet<string>();
// Built-in component types that conflict with MonoBehaviour properties.
HashSet<string> builtIn = new HashSet<string>()
{
"Rigidbody", "Rigidbody2D", "Camera", "Animation", "ConstantForce", "Collider",
"Collider2D", "HingeJoint", "NetworkView", "GUIText", "GUITexture", "Audio"
};
List<string> propertyDeclarations = new List<string>();
List<string> setupLines = new List<string>();
// Process root components (excluding Transform)
Component[] comps = prefab.GetComponents<Component>();
foreach (var comp in comps)
{
if (comp == null || comp is Transform)
continue;
Type compType = comp.GetType();
if (compType.Name == prefab.name || compType.Name == prefab.name + "Prefab")
continue;
string typeString = GetTypeString(compType);
string shortTypeName = compType.Name;
string propName = char.ToLowerInvariant(shortTypeName[0]) + shortTypeName.Substring(1);
string baseName = propName;
int duplicateCount = 1;
while (usedNames.Contains(propName))
{
propName = baseName + duplicateCount;
duplicateCount++;
}
usedNames.Add(propName);
string modifier = builtIn.Contains(shortTypeName) ? "new " : "";
propertyDeclarations.Add($" {modifier}public {typeString} {propName} {{ get; protected set; }}");
setupLines.Add($" {propName} = GetComponent<{typeString}>();");
}
// Process immediate child transforms and group them by name.
Dictionary<string, List<ChildInfo>> childGroups = new Dictionary<string, List<ChildInfo>>();
CollectDirectChildGroups(prefab.transform, childGroups);
// Build group info: for a single child, generate a single property;
// for multiple children with the same name, generate a List<> property.
var childGroupList = new List<(string groupName, string propertyName, string typeString, bool isList)>();
foreach (var kvp in childGroups)
{
string groupName = kvp.Key;
List<ChildInfo> children = kvp.Value;
bool isList = children.Count > 1;
string typeString = children[0].TypeName;
string propName = SanitizePropertyName(groupName, usedNames);
if (isList)
propName += "List";
usedNames.Add(propName);
childGroupList.Add((groupName, propName, typeString, isList));
if (isList)
propertyDeclarations.Add($" public List<{typeString}> {propName} {{ get; protected set; }} = new List<{typeString}>();");
else
propertyDeclarations.Add($" public {typeString} {propName} {{ get; protected set; }}");
}
// Generate Setup() method with a switch statement for child transforms.
if(childGroupList.Count > 0)
{
if(setupLines.Count > 0) setupLines.Add(""); //add spacing if there are other component lines above this
setupLines.Add(" for (int i = 0; i < transform.childCount; i++)");
setupLines.Add(" {");
setupLines.Add(" var child = transform.GetChild(i);");
setupLines.Add(" switch(child.name)");
setupLines.Add(" {");
foreach (var group in childGroupList)
{
setupLines.Add($" case \"{group.groupName}\":");
if (group.isList)
{
if (group.typeString == GetTypeString(typeof(Transform)))
setupLines.Add($" {group.propertyName}.Add(child);");
else
setupLines.Add($" {group.propertyName}.Add(child.GetComponent<{group.typeString}>());");
}
else
{
if (group.typeString == GetTypeString(typeof(Transform)))
setupLines.Add($" {group.propertyName} = child;");
else
setupLines.Add($" {group.propertyName} = child.GetComponent<{group.typeString}>();");
}
setupLines.Add(" break;");
}
setupLines.Add(" }");
setupLines.Add(" }");
}
foreach (string line in propertyDeclarations)
sb.AppendLine(line);
sb.AppendLine();
sb.AppendLine(" virtual protected void Setup()");
sb.AppendLine(" {");
foreach (string line in setupLines)
sb.AppendLine(line);
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine(" virtual protected void Awake()");
sb.AppendLine(" {");
sb.AppendLine(" Setup();");
sb.AppendLine(" }");
sb.AppendLine("}");
return sb.ToString();
}
// Collects only the immediate children and groups them by name.
private static void CollectDirectChildGroups(Transform parent, Dictionary<string, List<ChildInfo>> childGroups)
{
foreach (Transform child in parent)
{
string baseName = child.name.Replace(" ", "");
if (string.IsNullOrEmpty(baseName))
continue;
string typeString = GetTypeString(typeof(Transform));
Component[] comps = child.GetComponents<Component>();
foreach (var comp in comps)
{
if (comp == null || comp is Transform)
continue;
typeString = GetTypeString(comp.GetType());
break;
}
ChildInfo info = new ChildInfo(child.name, typeString);
if (!childGroups.ContainsKey(child.name))
childGroups[child.name] = new List<ChildInfo>();
childGroups[child.name].Add(info);
}
}
// Helper to generate a sanitized property name and ensure uniqueness.
private static string SanitizePropertyName(string name, HashSet<string> usedNames)
{
string baseName = name.Replace(" ", "");
if (string.IsNullOrEmpty(baseName))
baseName = "child";
string propName = char.ToLowerInvariant(baseName[0]) + baseName.Substring(1);
string original = propName;
int duplicateCount = 1;
while (usedNames.Contains(propName))
{
propName = original + duplicateCount;
duplicateCount++;
}
return propName;
}
private class ChildInfo
{
public string ChildName;
public string TypeName;
public ChildInfo(string childName, string typeName)
{
ChildName = childName;
TypeName = typeName;
}
}
// Returns a proper type string; if the type belongs to UnityEngine, return only the short name.
private static string GetTypeString(Type type)
{
if (type.Namespace == "UnityEngine")
return type.Name;
return type.FullName;
}
// Sanitizes the class name by removing invalid characters and ensuring it starts with a letter.
private static string SanitizeClassName(string name)
{
StringBuilder sb = new StringBuilder();
foreach (char c in name)
if (char.IsLetterOrDigit(c) || c == '_')
sb.Append(c);
string valid = sb.ToString();
if (string.IsNullOrEmpty(valid) || !char.IsLetter(valid[0]))
valid = "_" + valid;
return valid;
}
}
#endif
@MattRix
Copy link
Author

MattRix commented Feb 14, 2025

Note: I haven't tested this much, so there are probably edge case issues, but it seems useful enough already.

@MattRix
Copy link
Author

MattRix commented Feb 14, 2025

Added some features. Now if you have a bunch of child objects with the same name, it'll automatically make a list property for them and populate it. You can also now select multiple prefabs (or even an entire folder) and right click and choose "Generate Prefab Class" and it'll generate it for all of them.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment