Skip to content

Instantly share code, notes, and snippets.

@JLChnToZ
Last active November 8, 2018 12:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JLChnToZ/204908fca7a163c15aa1a2f3baa28d16 to your computer and use it in GitHub Desktop.
Save JLChnToZ/204908fca7a163c15aa1a2f3baa28d16 to your computer and use it in GitHub Desktop.
A simple editor tool that writes all persistent calls into a stub file

Persistent Call Stub Generator

As we all know, it is hard to trace a single C# method in a behaviour if that has been referenced in any "persistent callbacks" (Callbacks that assigned in inspector) in Unity projects, especially for large game projects, even we try to refactor those methods. That's why this tool is born.

This tool will generate a stub file, which contains all persistent callbacks within all selected objects/assets and those children. Simply find the GUI from whe window menu and click on the buttons on top, and that's it. Once the stub file is saved, we can be easier to know the method has been referenced by any and which persistent callback.

But for long term development, it is not recommend to use persistent callbacks.

using System;
using System.IO;
using System.Text;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;
using UnityEditor;
using UnityObject = UnityEngine.Object;
public class PersistentCallFinder: EditorWindow
{
private string rawContent;
private string[] lines;
private Vector2 scroll;
private UnityObject[] references;
[MenuItem("Window/Persistent Call Stub Generator")]
public static PersistentCallFinder Open()
{
return GetWindow<PersistentCallFinder>();
}
protected void OnEnable()
{
titleContent = new GUIContent("Persistent Call Stub Generator");
}
protected void OnGUI()
{
EditorGUILayout.BeginVertical();
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
if (GUILayout.Button("(Re)load From Selected Scene Object(s)", EditorStyles.toolbarButton))
InitStubFile(false);
if (GUILayout.Button("(Re)load From Selected Asset(s)", EditorStyles.toolbarButton))
InitStubFile(true);
EditorGUI.BeginDisabledGroup(references == null || references.Length == 0);
if (GUILayout.Button("Save Stub File", EditorStyles.toolbarButton))
{
var file = EditorUtility.SaveFilePanel("Save Stub File", "", "PersistentCallStub.cs", "cs");
if (!string.IsNullOrEmpty(file))
{
File.WriteAllText(file, rawContent);
AssetDatabase.Refresh();
}
}
if (GUILayout.Button("Remove All References", EditorStyles.toolbarButton))
{
RemoveAllPersistentCalls(references);
references = null;
}
EditorGUI.EndDisabledGroup();
EditorGUILayout.EndHorizontal();
scroll = EditorGUILayout.BeginScrollView(scroll);
if (lines != null)
foreach (string c in lines)
GUILayout.Label(c);
EditorGUILayout.EndScrollView();
EditorGUILayout.EndVertical();
}
public static string GetPersistantCallStubFile(UnityObject[] targets)
{
const string STUB_HEADER = "using System;{0}" +
"using UnityEngine;{0}{0}" +
"#if UNITY_EDITOR{0}" +
"public static class PersistentCallStub{1}{0}" +
"{{{0}" +
"\tpublic static void GetPersistantDelegate(UnityEngine.Object obj){0}" +
"\t{{{0}";
const string STUB_FOOTER = "\t}\n" +
"}\n#endif";
var nl = Environment.NewLine;
var calls = FindPersistentCall(targets);
var sb = new StringBuilder();
sb.AppendFormat(STUB_HEADER, nl, DateTime.Now.ToFileTime());
foreach (var call in calls)
{
sb.Append($"\t\t// {GetHirechyPath(call.source)}{nl}\t\t(obj as {call.type}).{call.method.Name}(");
bool firstArg = true;
foreach (var arg in call.method.GetParameters())
{
if (!firstArg) sb.Append(", ");
sb.Append($"default({arg.ParameterType})");
firstArg = false;
}
sb.AppendLine($");{nl}");
}
sb.AppendLine(STUB_FOOTER.Replace("\n", nl));
return sb.ToString();
}
private void InitStubFile(bool assets)
{
references = assets ?
Selection.GetFiltered<UnityObject>(SelectionMode.DeepAssets) :
Selection.GetFiltered<UnityObject>(SelectionMode.Deep);
rawContent = GetPersistantCallStubFile(references);
lines = rawContent.Split(new[] { Environment.NewLine }, StringSplitOptions.None);
}
public static PersistentCallInfo[] FindPersistentCall(params UnityObject[] targets)
{
var result = new List<PersistentCallInfo>();
foreach (var obj in GetObjectsInTree(targets))
{
if (obj == null) continue;
var so = new SerializedObject(obj);
if (so == null) continue;
var prop = so.GetIterator();
bool goDeeper = false;
do
{
if (!(goDeeper = prop.propertyType == SerializedPropertyType.Generic))
continue;
var persistentCalls = prop.FindPropertyRelative("m_PersistentCalls.m_Calls");
if (persistentCalls == null || persistentCalls.type != "PersistentCall")
continue;
for (int i = 0; i < persistentCalls.arraySize; i++)
{
var call = persistentCalls.GetArrayElementAtIndex(i);
var target = call.FindPropertyRelative("m_Target").objectReferenceValue;
if (target == null)
continue;
var type = target.GetType();
var method = type.GetMethod(
call.FindPropertyRelative("m_MethodName").stringValue,
GetTypes(call)
);
if (method == null)
continue;
result.Add(new PersistentCallInfo
{
type = type,
method = method,
source = obj,
target = target,
});
}
goDeeper = false;
} while (prop.Next(goDeeper));
}
return result.ToArray();
}
private static void RemoveAllPersistentCalls(params UnityObject[] targets)
{
foreach (var obj in GetObjectsInTree(targets))
{
if (obj == null) continue;
var so = new SerializedObject(obj);
if (so == null) continue;
var prop = so.GetIterator();
bool goDeeper = false;
do
{
if (!(goDeeper = prop.propertyType == SerializedPropertyType.Generic))
continue;
var persistentCalls = prop.FindPropertyRelative("m_PersistentCalls.m_Calls");
if (persistentCalls != null && persistentCalls.type == "PersistentCall")
{
persistentCalls.ClearArray();
goDeeper = false;
}
} while (prop.Next(goDeeper));
}
}
private static ICollection<UnityObject> GetObjectsInTree(UnityObject[] targets)
{
var scan = new Stack<UnityObject>(targets);
var objs = new HashSet<UnityObject>();
while (scan.Count > 0)
{
var target = scan.Pop();
if (target is Transform transform)
target = transform.gameObject;
if (target is GameObject gameObject)
{
foreach (Transform child in gameObject.transform)
scan.Push(child);
foreach (var mb in gameObject.GetComponents<MonoBehaviour>())
objs.Add(mb);
}
else if (target is ScriptableObject || target is MonoBehaviour)
objs.Add(target);
}
return objs;
}
private static Type[] GetTypes(SerializedProperty serializedProperty)
{
var argsProp = serializedProperty.FindPropertyRelative("m_Arguments");
var argTypes = new Type[argsProp.arraySize];
for (int i = 0; i < argTypes.Length; i++)
argTypes[i] = Type.GetType(argsProp
.GetArrayElementAtIndex(i)
.FindPropertyRelative("m_ObjectArgumentAssemblyTypeName")
.stringValue
);
return argTypes;
}
private static string GetHirechyPath(UnityObject obj)
{
var path = AssetDatabase.GetAssetOrScenePath(obj);
if (obj is Component component)
obj = component.transform;
else if (obj is GameObject gameObject)
obj = gameObject.transform;
if (obj is Transform transform)
{
var sb = new StringBuilder(path);
var stack = new Stack<string>();
do
stack.Push(transform.name);
while ((transform = transform.parent) != null);
while (stack.Count > 0)
{
sb.Append('/');
sb.Append(stack.Pop());
}
path = sb.ToString();
}
return path;
}
public struct PersistentCallInfo
{
public UnityObject source, target;
public Type type;
public MethodInfo method;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment