# 问题
在制作场景的时候,经常出现一个问题:物件 A 是一个 Prefab,必须用于动态加载,在其下边同样需要放置其它 Prefab,在这儿称之为 B。这样,就出现了一个问题:因为 Unity 中,可是不允许 Prefab 之间进行嵌套的。如果你想那么做,可以,那么放置在物件 A 下边的 Prefab 就丢失之前的 Prefab 作用了.....
简单来说,就是变成了一个纯粹的子物体,B 的 Prefab 将不会再对它产生任何影响。本来,或许 Unity 的本意是为了保持每个 Prefab 的数据一致性和独立性,这样不至于因为制造大量 Prefab 之间的嵌套,造成数据的混乱。可是有的时候,我们却偏偏需要这个功能。
举个例子,我现在所在的项目,就需要这么一个功能:用于游戏场景的 “建筑” 就属于一个 Prefab,并且是动态加载与场景的,而建筑上的各项物件,自然就不可避免地使用到了其它的 Prefab。这样一来,如果任其自然,那么每个放进建筑的 Prefab,都将会丢失它们自己的引用,导致后期修改物件 Prefab 之后,不得不手动再次进行替换。另外,如果同时有两个人同时编辑了一个大 Prefab,即时改动很小,也是会产生冲突的,也就是说,如果所有东西都是一个 Prefab,那么同一时间,只能允许一个人改动。众所周知,这是一项非常麻烦的一件事,所以就有了 “请做一个连接各个 Prefab 的工具” 的要求。
# 效果
首先是效果,编辑中:
编辑后:
# 详情
作为一个编辑器工具,同时需要保存建筑上的 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);
}
}
}
}
}
}