快速連結

2020年2月5日

[工具]替換Component(Script)小工具


終於抽出時間來做些方便的小工具,其實也是為了要幫助自己快速優化專案的東西。
功能:可以對所有物件上所指定的script 同時替換成另一個script。但是限制是繼承同一個script~(例如NGUISPritePlus(繼承NGUISprite)可以取代NGUISprite)


功能簡介:
1.確認要置換的Script跟物件後,會讀出符合同一Type/Class的script物件,並列表。
2.可勾選需要轉換/替代的物件scrip。
3.列表內列舉的物件名稱都可以點選,點選後,會在Hierarchy內指向該物件。

希望改進的地方:
1.可以拉入unity原生的component。(因為我常常勾錯物件……想要復原覺得頭痛)
2.加入復原(Undo)的功能。(需要思考一下架構)

本文會一一解釋製作的思路,如果不想看這麼多廢言,可以直接拉到最後下載script,如果要轉載,請務必注明來自於熊阿寶的工作MEMO哦。
希望這個小工具可以幫助到大家~有覺得可以優化的部分也歡迎大家給予意見!

第一步:建立Window

[MenuItem("Tools/ConvertComponents")]
public static void ShowWindow()
{
  EditorWindow.GetWindow(typeof(ComponentConvertTool), false, "Component Convert Tool");
}
1.在選單中建立這個工具的選項。
2.創建這個工具的視窗。

第二步:宣告變數、建立函式

private MonoScript m_TargetScript;
private GameObject m_TargetGameObject;
private string[] m_GameObjectStrAry;
private bool[] m_GameObjectToogleAry;
private Object[] m_ObjectAry;
private int[] m_ObjectHashCodeAry;
private string m_LogStr;

private int m_GameObjectCount = 0;
private GUIStyle m_ObjectUnchoosedStyle, m_ObjectChoosedStyle;
private GUIStyle m_ObjectListStyle;
private void OnGUI()
{
  //TO DO...
}

private void DoSomething(string MethodName)
{
  //TO DO...
}

public void ShowTargetObject()
{
  //TO DO...
}

public void DoConvert()
{
  //TO DO...
}

變數:
  • m_TargetScript:取代的Script
  • m_TargetGameObject:要被取代Script的物件,可以是父層物件
  • m_GameObjectStrAry:準備拿來存物件名稱
  • m_GameObjectToogleAry:拿來存是否勾選物件
  • m_ObjectAry:準備拿來存物件
  • m_ObjectHashCodeAry:準備拿來存物件的Hash Code,方便在Hierarchy內跳到指定物件位置
  • m_LogStr:顯示LOG用
  • m_GameObjectCount:物件數量,我習慣存起來用
  • m_ObjectUnchoosedStyle, m_ObjectChoosedStyle, m_ObjectListStyle:元件樣式
函式:
  • OnGUI():繪製視窗內的元件
  • DoSomething(string MethodName):取得指定Script Base Type後,接下來要使用哪種功能,算是一種轉運站
  • ShowTargetObject():顯示有哪些物件掛著的Script符合指定的Type
  • DoConvert():改變Script


第二步:繪製視窗元件
OnGUI()內的script:

if (m_ObjectListStyle == null)
{
  m_ObjectListStyle = new GUIStyle(GUI.skin.box);
}
if (m_ObjectUnchoosedStyle == null)
{
  m_ObjectUnchoosedStyle = new GUIStyle(GUI.skin.label);
  m_ObjectUnchoosedStyle.alignment = TextAnchor.MiddleLeft;
  m_ObjectUnchoosedStyle.normal.textColor = Color.gray;
}
if (m_ObjectChoosedStyle == null)
{
  m_ObjectChoosedStyle = new GUIStyle(GUI.skin.label);
  m_ObjectChoosedStyle.alignment = TextAnchor.MiddleLeft;
  m_ObjectChoosedStyle.normal.textColor = Color.white;
}
已經建立過一次的樣式我習慣就不要重複建立了,因此會做條件式處理樣式。


EditorGUI.BeginChangeCheck();
EditorGUILayout.BeginHorizontal();
GUILayout.Label("Target script:");
m_TargetScript = EditorGUILayout.ObjectField(m_TargetScript, typeof(MonoScript), true) as MonoScript;
EditorGUILayout.EndHorizontal();

EditorGUILayout.BeginHorizontal();
GUILayout.Label("Be Convert GameObject/Group: ");
m_TargetGameObject = EditorGUILayout.ObjectField(m_TargetGameObject, typeof(GameObject), true) as GameObject;
EditorGUILayout.EndHorizontal();
顯示必要的兩個項目:替換的Script、要被替換的GameObject。


if (EditorGUI.EndChangeCheck())
{
  if (m_TargetScript != null)
  {
    if (m_TargetGameObject != null)
    {
      DoSomething("ShowTargetObject");
    }
    else
    {
      m_LogStr = "";
      m_GameObjectStrAry = null;
      m_GameObjectCount = 0;
    }
  }
  else
  {
    m_LogStr = "";
    m_GameObjectStrAry = null;
    m_GameObjectCount = 0;
  }
}
DoSomething("ShowTargetObject");
希望可以顯示,在指定的GameObject下含有可以被替換的Script的子物件列表。
並且進行數量的計算。


GUILayout.Label("Include GameObject List:");
if (m_GameObjectCount > 0)
{
  if (m_GameObjectToogleAry == null || m_GameObjectToogleAry.Length != m_GameObjectCount)
  {
    m_GameObjectToogleAry = new bool[m_GameObjectCount];
    for (int i = 0; i < m_GameObjectCount; i++)
   m_GameObjectToogleAry[i] = true;
  }
  EditorGUILayout.BeginVertical(m_ObjectListStyle);
  for (int i = 0; i < m_GameObjectCount; i++)
  {
    EditorGUILayout.BeginHorizontal();
    m_GameObjectToogleAry[i] = EditorGUILayout.Toggle(m_GameObjectToogleAry[i], GUILayout.Width(20));
    if (GUILayout.Button(m_GameObjectStrAry[i], m_GameObjectToogleAry[i] ? m_ObjectChoosedStyle : m_ObjectUnchoosedStyle))
    {
   Selection.activeInstanceID = m_ObjectHashCodeAry[i];
    }
    EditorGUILayout.EndHorizontal();
  }
  EditorGUILayout.EndVertical();
  if (GUILayout.Button("Convert"))
  {
    m_LogStr = "";
    if (m_TargetScript == null || m_TargetGameObject == null)
    {
   GUILayout.Label("ERROR! Targets are NULL", EditorStyles.boldLabel);
    }
    else
    {
     DoSomething("DoConvert");
    }
  }
}
當有同樣/繼承指定script的物件時,就會顯示列表。
m_GameObjectToogleAry[i] = EditorGUILayout.Toggle(m_GameObjectToogleAry[i], GUILayout.Width(20));
項目可以被選擇要置換或是不置換。
GUILayout.Button(m_GameObjectStrAry[i], m_GameObjectToogleAry[i] ? m_ObjectChoosedStyle : m_ObjectUnchoosedStyle)
加上「點擊項目即可在Hierarchy內跳到該物件」、「被選擇時項目顏色不同」的要素,將項目顯示設置成button。
Selection.activeInstanceID = m_ObjectHashCodeAry[i];
利用物件的HashCode來找到Hierarchy內的物件。
GUILayout.Button("Convert")
增加一個叫做Convert的Button,並且在按下時執行行為。

第三步:開始DoSomething

private void DoSomething(string MethodName)
{
  System.Type t = m_TargetScript.GetClass().BaseType;
  var method = typeof(ComponentConvertTool).GetMethod(MethodName);
  if (method != null)
  {
    var fooRef = method.MakeGenericMethod(t);
    fooRef.Invoke(this, null);
  }
  else
  {
    Debug.Log("ERROR!Can't Find this Method:" + MethodName);
  }
}
這邊比較麻煩的是呼叫泛型函式,由於我們取得的Type變數沒有辦法直接放進泛型內,因此要使用彈性的方是來進行呼叫,使用的是Reflection技術,生成動態物件後再利用此物件呼叫。
  1. 需要先得到指定Script的BaseType,才能去取得替換物件內所有子物件有沒有相關的script/Component。
  2. 為了可以使用泛型函式,需要使用GetMethod(string)來生成此函式的物件。
  3. 確認該物件存在,利用此物件來呼叫泛型函式。

顯示物件列表

public void ShowTargetObject()
{
  T[] oAry = m_TargetGameObject.GetComponentsInChildren(true);
  m_GameObjectCount = oAry.Length;
  m_GameObjectStrAry = new string[m_GameObjectCount];
  m_ObjectAry = new Object[m_GameObjectCount];
  m_ObjectHashCodeAry = new int[m_GameObjectCount];
  for (int i = 0; i < m_GameObjectCount; i++)
  {
    m_ObjectAry[i] = oAry[i] as Object;
    m_ObjectHashCodeAry[i] = oAry[i].GetHashCode();
    m_GameObjectStrAry[i] += oAry[i].ToString();
  }
  if (oAry.Length == 0)
  {
    m_LogStr = "NO Script Match BaseType("+typeof(T).ToString()+")";
  }
}
這邊的code相對簡單,不多贅述。

進行置換

public void DoConvert()
{
  for (int i = 0;i < m_GameObjectCount; i++)
  {
    if (m_GameObjectToogleAry[i])
    {
      Object o = m_ObjectAry[i];
      var so = new SerializedObject(o);
      so.Update();
      so.FindProperty("m_Script").objectReferenceValue = m_TargetScript;
      so.ApplyModifiedProperties();
    }
  }
  DoSomething("ShowTargetObject");
}
  1. 取得物件,並且將其宣告成一個SerializedObject,方便作FindProperty。
  2. Update()可作可不作。
  3. 尋找該物件名為m_Script的變數,這個可以在Inspector上看到,指定Component是哪個script。接著就是替換這個變數的Reference。
  4. 最後,一定要使用ApplyModifiedProperties()進行資料更動進行寫入。

以上,其實並不是很複雜的code,不過在呼叫泛型函式時確實遇到了瓶頸,這還是第一次用Reflection來呼叫泛型函式XD

Script 位置:ComponentConvertTool

沒有留言:

張貼留言

歡迎大家留言提問,我會答的都會盡力回答!
如果太久沒出現回應就是我又忘記回來看留言了TAT