# 问题

在制作场景的时候,经常出现一个问题:物件 A 是一个 Prefab,必须用于动态加载,在其下边同样需要放置其它 Prefab,在这儿称之为 B。这样,就出现了一个问题:因为 Unity 中,可是不允许 Prefab 之间进行嵌套的。如果你想那么做,可以,那么放置在物件 A 下边的 Prefab 就丢失之前的 Prefab 作用了.....

简单来说,就是变成了一个纯粹的子物体,B 的 Prefab 将不会再对它产生任何影响。本来,或许 Unity 的本意是为了保持每个 Prefab 的数据一致性和独立性,这样不至于因为制造大量 Prefab 之间的嵌套,造成数据的混乱。可是有的时候,我们却偏偏需要这个功能。

举个例子,我现在所在的项目,就需要这么一个功能:用于游戏场景的 “建筑” 就属于一个 Prefab,并且是动态加载与场景的,而建筑上的各项物件,自然就不可避免地使用到了其它的 Prefab。这样一来,如果任其自然,那么每个放进建筑的 Prefab,都将会丢失它们自己的引用,导致后期修改物件 Prefab 之后,不得不手动再次进行替换。另外,如果同时有两个人同时编辑了一个大 Prefab,即时改动很小,也是会产生冲突的,也就是说,如果所有东西都是一个 Prefab,那么同一时间,只能允许一个人改动。众所周知,这是一项非常麻烦的一件事,所以就有了 “请做一个连接各个 Prefab 的工具” 的要求。

# 效果

首先是效果,编辑中:
示例1
编辑后:
示例2

# 详情

作为一个编辑器工具,同时需要保存建筑上的 Prefab 信息,那么就必须有一个存放数据的 “容器”,在这儿就是一个脚本了。所以新建一个脚本 “BuildingPrefabManager”(之所以叫这个名字是因为),用于保存对其它 Prefab 的引用数据,这个脚本相对简单,因为大多数据处理都是在后边的编辑器工具中进行的。因为需要挂载在建筑物体上,所以这个类继承自 MonoBehaviour。首先在这个类中定义一个新类型 “PrefabLink”,用于方便保存 Prefab 的距离引用数据:

    public class BuildingPrefabManager : MonoBehaviour
    {
	    public class PrefabLink
        {
			//标示物体的位置及旋转
            public Transform m_marker;
			//物体的实际Prefab,在运行游戏后,动态加载
            public GameObject m_lampPrefab;

            public void MakeLamp()
            {
                GameObject o = GameObject.Instantiate<GameObject>(m_lampPrefab);
                o.transform.SetParent(m_marker, false);
                o.transform.localRotation = Quaternion.identity;
            }
        }
	}

看着很简单吧?如其所名,这个类主要就是保存 Prefab 的引用,并且以一个 “Marker”(即纯粹的空物体)保存下位置及旋转。
接着,在类中定义一个 PrefabLink 的 List 列表:

        public List<PrefabLink> m_prefabList = new List<PrefabLink>();
        public bool m_testMode = false;

这样,数据保存方面的定义基本就完成了。其中多出的 "m_testMode" 参数,之后的编辑器工具会用到,因为编辑器工具自身是无法保存数据的,所以这儿才定义了一个 Bool 变量作为标示。其作用后边会进行解释。

OK,现在保存的据的容器基本上定义完成了。那么就轮到最重要的东西:编辑器工具了。
这个类就叫 “BuildingPrefab”。

[CustomEditor(typeof(BuildingPrefabManager))]
public class BuildingPrefab : Editor
{

    private GUIStyle m_style = new GUIStyle();
    public override void OnInspectorGUI()
    {
        BuildingPrefabManager buildingPrefab = target as BuildingPrefabManager;

        GUILayout.Space(10);
        m_style.richText = true;
        m_style.normal.textColor = Color.white;
        GUILayout.Label("<color=yellow>提示:若需要自动检测,请在命名中带上以下相应内容:</color>", m_style);
        GUILayout.Label("  灯光:命名中带有<color=#FF44FF>“Lamp”</color>", m_style);

        GUILayout.Space(10);

        ToggleTestMode(buildingPrefab);

        if (buildingPrefab.m_testMode)
        {
            ShowBindingEditorGUI(buildingPrefab);

            ShowSavedData(buildingPrefab);
        }
    }

在这个编辑器工具中,首先是 “ToggleTestMode” 方法,这个方法主要用来切换 “测试模式”,可以在 “进入” 和 “退出” 测试模式时,用来处理一些带来方便的事情,比如开始测试模式时,将引用的 Prefab 创建出来,当关闭测试的时候,将其删除。这样可以实现更为直观的一个编辑功能。
噢.... 这儿倒不能说 “比如”,因为我就是这样干的:

	private void ToggleTestMode(BuildingPrefabManager buildingLamp)
    {
        GUILayout.BeginHorizontal();
        string testMode = buildingLamp.m_testMode ? "<color=#28FF28>开启</color>" : "<color=red>关闭</color>";
        GUILayout.Label("测试状态:" + testMode, m_style);
        if (buildingLamp.m_testMode)
        {
            if (GUILayout.Button("关闭测试模式"))
            {
                buildingLamp.m_testMode = false;
				//销毁所有实例化出来的Prefab
                DeleteTempLamp(buildingLamp);
            }
        }
        else
        if (GUILayout.Button("开启测试模式"))
        {
            buildingLamp.m_testMode = true;
        }
        GUILayout.EndHorizontal();
    }
	
	//销毁所有用于观看的Prefab模型
	private static void DeleteTempLamp(BuildingPrefabManager buildingPrefab)
    {
        foreach (var lamp in buildingPrefab.m_prefabList)
        {
            for (int i = 0; i < buildingPrefab.transform.childCount; i++)
            {
                Transform child = buildingPrefab.transform.GetChild(i);
                if (lamp.m_marker == child)
                {
                    for (int j = 0; j < child.childCount; j++)
                    {
                        DestroyImmediate(child.GetChild(j).gameObject);
                    }
                }
            }
        }
    }
	```

  之所以在开启测试的时候,没有看见实例化的代码,是因为我在这个Editor脚本中,本身就是不断进行监听的,如果有相应的Prefab,并且处于测试模式,直接就实例化出来了。这主要就是“ShowBindingEditorGUI”方法完成的:
``` CS
    private void ShowBindingEditorGUI(BuildingPrefabManager buildingPrefab)
    {
        //-------------------------->>>>>绑定
        GUILayout.Space(10);
        GUILayout.Label("<color=#28FF28>①当前检测到的相应物体:</color>", m_style);

        int index = 0;
        //循环建筑的子物体
        for (int i = 0; i < buildingPrefab.transform.childCount; i++)
        {
            Transform child = buildingPrefab.transform.GetChild(i);
            //判断子物体的名字是否带有相应的单词,并忽略大小写
            if (child.name.ToLower().Contains("lamp") || child.name.ToLower().Contains("bed") || child.name.ToLower().Contains("sofa") || child.name.ToLower().Contains("chair"))
            {
                index++;
                GUILayout.BeginHorizontal();
                EditorGUILayout.LabelField(index + "." + child.name);
                //根据Transform获取PrefabLink实例
                BuildingPrefabManager.PrefabLink prefab = buildingPrefab.GetPrefab(child);
                //为PrefabLink实例赋值
                prefab.m_marker = child;
                prefab.m_lampPrefab = EditorGUILayout.ObjectField(prefab.m_lampPrefab, typeof(GameObject), false) as GameObject;

                //判断是否可以将PrefabLink中的Prefab实例化,显示出来,以提高编辑的便利性
                Transform lampObj;
                if (prefab.m_lampPrefab)
                    if (child.childCount == 0)
                    {
                        lampObj = GameObject.Instantiate<GameObject>(prefab.m_lampPrefab).transform;
                        lampObj.name = lampObj.name.Replace("(Clone)", "");
                        lampObj.SetParent(child);
                        lampObj.transform.localPosition = Vector3.zero;
                        lampObj.transform.localRotation = Quaternion.identity;
                    }
                    else if (child.childCount > 0)
                    {
                        Transform t = child.GetChild(0);
                        if (t.name != prefab.m_lampPrefab.name)
                            DestroyImmediate(t.gameObject);
                    }

                GUILayout.EndHorizontal();
            }
        }
    }

其中的 GetPrefab () 方法是 BuildingPrefabManager 中的一个方法,当存在传入的 Transform 组成的 PrefabLink 时,将会返回这个 PrefabLink;若不存在,则会返回一个新的实例。可以大大提搞便利性:

	        public PrefabLink GetPrefab(Transform marker)
        {
            PrefabLink prefab = m_prefabList.Find((m) => { return m.m_marker == marker; });
            if (prefab == null)
            {
                prefab = new PrefabLink();
                m_prefabList.Add(prefab);
            }
            return prefab;
        }

最后就是 ShowSavedData 方法了。这个方法功能很简单,主要就是删除不合法的 PrefabLink,在这儿主要表现为没有为其提供实际的 Prefab 物体;并且显示已保存的 PrefabLink 数据:

    private void ShowSavedData(BuildingPrefabManager buildingPrefab)
    {
        int index;
        //-------------------------->>>>>已确定者
        List<BuildingPrefabManager.PrefabLink> tempLsit = new List<BuildingPrefabManager.PrefabLink>();
        //检测创建的PrefabLink列表中,是否存在不合格的数据
        //若存在不合格的数据,则将其从列表删除
        foreach (var prefab in buildingPrefab.m_prefabList)
        {
            if (prefab.m_marker == null || prefab.m_lampPrefab == null)
            {
                tempLsit.Add(prefab);
            }
        }
        foreach (var prefab in tempLsit)
        {
            buildingPrefab.m_prefabList.Remove(prefab);
        }

        //显示合法数据
        GUILayout.Space(10);
        GUILayout.Label("<color=#28FF28>②已保存,且有效数据:</color>", m_style);
        index = 0;
        for (int i = 0; i < buildingPrefab.m_prefabList.Count; i++)
        {
            index++;

            BuildingPrefabManager.PrefabLink prefab = buildingPrefab.m_prefabList[i];

            GUILayout.BeginHorizontal();
            GUILayout.Label(index + "." + prefab.m_marker.name);
            prefab.m_lampPrefab = EditorGUILayout.ObjectField(prefab.m_lampPrefab, typeof(GameObject), false) as GameObject;
            GUILayout.EndHorizontal();
        }
    }

# 完整代码

# BuildingPrefabManager

/**********************************************************
*Author: CWHISME
*Date: 2015.12.28
*Func:
**********************************************************/
using UnityEngine;
using System.Collections.Generic;

namespace Pathea.BuildingNs
{
    public class BuildingPrefabManager : MonoBehaviour
    {

        public List<PrefabLink> m_prefabList = new List<PrefabLink>();
        public bool m_testMode = false;

        void Start()
        {
            foreach (var lamp in m_prefabList)
            {
                lamp.MakeLamp();
            }
        }

        public bool CheckHaveTarget(Transform marker)
        {
            return m_prefabList.Find((m) => { return m.m_marker == marker; }) != null;
        }

        public PrefabLink GetPrefab(Transform marker)
        {
            PrefabLink prefab = m_prefabList.Find((m) => { return m.m_marker == marker; });
            if (prefab == null)
            {
                prefab = new PrefabLink();
                m_prefabList.Add(prefab);
            }
            return prefab;
        }

        [System.Serializable]
        public class PrefabLink
        {
            public Transform m_marker;
            public GameObject m_lampPrefab;

            public PrefabLink()
            { }

            public PrefabLink(Transform marker, GameObject lamp)
            {
                m_marker = marker;
                m_lampPrefab = lamp;
            }

            public void MakeLamp()
            {
                GameObject o = GameObject.Instantiate<GameObject>(m_lampPrefab);
                o.transform.SetParent(m_marker, false);
                o.transform.localRotation = Quaternion.identity;
            }
        }
    }
}

# BuildingPrefab

/**********************************************************
*Author: CWHISME
*Date: 2015.12.28
*Func:
**********************************************************/
using UnityEngine;
using UnityEditor;
using Pathea.BuildingNs;
using System.Collections.Generic;

[CustomEditor(typeof(BuildingPrefabManager))]
public class BuildingPrefab : Editor
{

    private GUIStyle m_style = new GUIStyle();
    public override void OnInspectorGUI()
    {
        BuildingPrefabManager buildingPrefab = target as BuildingPrefabManager;

        GUILayout.Space(10);
        m_style.richText = true;
        m_style.normal.textColor = Color.white;
        GUILayout.Label("<color=yellow>提示:若需要自动检测,请在命名中带上以下相应内容:</color>", m_style);
        GUILayout.Label("  灯光:命名中带有<color=#FF44FF>“Lamp”</color>", m_style);

        GUILayout.Space(10);

        ToggleTestMode(buildingPrefab);

        if (buildingPrefab.m_testMode)
        {
            ShowBindingEditorGUI(buildingPrefab);

            ShowSavedData(buildingPrefab);
        }
    }

    private void ShowBindingEditorGUI(BuildingPrefabManager buildingPrefab)
    {
        //-------------------------->>>>>绑定
        GUILayout.Space(10);
        GUILayout.Label("<color=#28FF28>①当前检测到的相应物体:</color>", m_style);

        int index = 0;
        //循环建筑的子物体
        for (int i = 0; i < buildingPrefab.transform.childCount; i++)
        {
            Transform child = buildingPrefab.transform.GetChild(i);
            //判断子物体的名字是否带有相应的单词,并忽略大小写
            if (child.name.ToLower().Contains("lamp"))
            {
                index++;
                GUILayout.BeginHorizontal();
                EditorGUILayout.LabelField(index + "." + child.name);
                //根据Transform获取PrefabLink实例
                BuildingPrefabManager.PrefabLink prefab = buildingPrefab.GetPrefab(child);
                //为PrefabLink实例赋值
                prefab.m_marker = child;
                prefab.m_lampPrefab = EditorGUILayout.ObjectField(prefab.m_lampPrefab, typeof(GameObject), false) as GameObject;

                //判断是否可以将PrefabLink中的Prefab实例化,显示出来,以提高编辑的便利性
                Transform lampObj;
                if (prefab.m_lampPrefab)
                    if (child.childCount == 0)
                    {
                        lampObj = GameObject.Instantiate<GameObject>(prefab.m_lampPrefab).transform;
                        lampObj.name = lampObj.name.Replace("(Clone)", "");
                        lampObj.SetParent(child);
                        lampObj.transform.localPosition = Vector3.zero;
                        lampObj.transform.localRotation = Quaternion.identity;
                    }
                    else if (child.childCount > 0)
                    {
                        Transform t = child.GetChild(0);
                        if (t.name != prefab.m_lampPrefab.name)
                            DestroyImmediate(t.gameObject);
                    }

                GUILayout.EndHorizontal();
            }
        }
    }

    private void ShowSavedData(BuildingPrefabManager buildingPrefab)
    {
        int index;
        //-------------------------->>>>>已确定者
        List<BuildingPrefabManager.PrefabLink> tempLsit = new List<BuildingPrefabManager.PrefabLink>();
        //检测创建的PrefabLink列表中,是否存在不合格的数据
        //若存在不合格的数据,则将其从列表删除
        foreach (var prefab in buildingPrefab.m_prefabList)
        {
            if (prefab.m_marker == null || prefab.m_lampPrefab == null)
            {
                tempLsit.Add(prefab);
            }
        }
        foreach (var prefab in tempLsit)
        {
            buildingPrefab.m_prefabList.Remove(prefab);
        }

        //显示合法数据
        GUILayout.Space(10);
        GUILayout.Label("<color=#28FF28>②已保存,且有效数据:</color>", m_style);
        index = 0;
        for (int i = 0; i < buildingPrefab.m_prefabList.Count; i++)
        {
            index++;

            BuildingPrefabManager.PrefabLink prefab = buildingPrefab.m_prefabList[i];

            GUILayout.BeginHorizontal();
            GUILayout.Label(index + "." + prefab.m_marker.name);
            prefab.m_lampPrefab = EditorGUILayout.ObjectField(prefab.m_lampPrefab, typeof(GameObject), false) as GameObject;
            GUILayout.EndHorizontal();
        }
    }

    private void ToggleTestMode(BuildingPrefabManager buildingLamp)
    {
        GUILayout.BeginHorizontal();
        string testMode = buildingLamp.m_testMode ? "<color=#28FF28>开启</color>" : "<color=red>关闭</color>";
        GUILayout.Label("测试状态:" + testMode, m_style);
        if (buildingLamp.m_testMode)
        {
            if (GUILayout.Button("关闭测试模式"))
            {
                buildingLamp.m_testMode = false;
                DeleteTempLamp(buildingLamp);
            }
        }
        else
        if (GUILayout.Button("开启测试模式"))
        {
            buildingLamp.m_testMode = true;
        }
        GUILayout.EndHorizontal();
    }

    private static void DeleteTempLamp(BuildingPrefabManager buildingPrefab)
    {
        foreach (var lamp in buildingPrefab.m_prefabList)
        {
            for (int i = 0; i < buildingPrefab.transform.childCount; i++)
            {
                Transform child = buildingPrefab.transform.GetChild(i);
                if (lamp.m_marker == child)
                {
                    for (int j = 0; j < child.childCount; j++)
                    {
                        DestroyImmediate(child.GetChild(j).gameObject);
                    }
                }
            }
        }
    }
}