UFL3-1——Hierarchy窗口布局优化

窗口布局优化指的是什么

现在直接失活对象,当之后项目做大了,抽屉多了,对象多了
游戏中成百上千个对象,在开发测试时不方便从Hierarchy窗口中查看对象获取信息

image

可见,目前创建出来的对象都是直接排布在场景上而没有做优化,因此我们希望能优化一下Hierarchy窗口中的布局,将对象和抽屉的关系可视化

制作思路和具体实现

  • 制作思路:

    1. 柜子管理自己的柜子根物体 Pool
    2. 抽屉管理自己的抽屉根物体
    3. 失活时建立父子关系,激活活时断开父子关系

    要实现的效果如下

    image

  • 具体实现:

    1. 先实现将所有对象放入柜子根物体中

      先声明在缓存池类中声明柜子根物体对象的变量 poolObj​,与场景上的Pool​对象关联起来
      当调用 PushObj​ 而柜子根物体Pool​对象不存在于场景的时候,创建这个Pool​对象
      然后,所有要压入池子内的对象都作为Pool​对象的子物体

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      using System.Collections;
      using System.Collections.Generic;
      using UnityEngine;

      /// <summary>
      /// 缓存池模块 管理器
      /// </summary>
      public class PoolManager : BaseManager<PoolManager>
      {
      //柜子容器当中有抽屉的体现
      private Dictionary<string, Stack<GameObject>> poolDic = new Dictionary<string, Stack<GameObject>>();

      //池子根对象
      private GameObject poolObj;

      private PoolManager() { }

      /// <summary>
      /// 拿东西的方法
      /// </summary>
      /// <param name="name">抽屉容器的名字</param>
      /// <returns>从缓存池中取出的对象</returns>
      public GameObject GetObj(string name)
      {
      GameObject obj;
      //有抽屉,且抽屉内有对象
      if (poolDic.ContainsKey(name) && poolDic[name].Count > 0)
      {
      obj = poolDic[name].Pop();
      obj.SetActive(true);
      //取出时,断开父子关系
      obj.transform.SetParent(null);
      }
      //否则,就应该去创建对象
      else
      {
      obj = GameObject.Instantiate(Resources.Load<GameObject>(name));
      //为了避免实例化出来的对象默认名字带一个"(clone)",我们需要重命名这个对象
      obj.name = name;
      }
      return obj;
      }

      /// <summary>
      /// 往缓存池中放入对象
      /// </summary>
      /// <param name="name">抽屉(对象池)的名字</param>
      /// <param name="obj">要放入的对象</param>
      public void PushObj(GameObject obj)
      {
      //如果根物体为空,就创建
      if (poolObj == null)
      poolObj = new GameObject("Pool");

      //并不是直接移除对象,而是将对象失活一会再用,用的时候再激活它
      //除了失活对象,还可以通过把对象放到很远的地方等方式来隐藏对象(失活的性能可能偏低一些)
      obj.SetActive(false);
      //把失活的对象(要放入抽屉中的对象),如对象
      obj.transform.SetParent(poolObj.transform);
      //如果不存在对应的抽屉容器,先创建抽屉,再往抽屉内放
      if (!poolDic.ContainsKey(obj.name))
      poolDic.Add(obj.name, new Stack<GameObject>());
      poolDic[obj.name].Push(obj);
      }

      /// <summary>
      /// 清除整个柜子当中的数据
      /// </summary>
      public void ClearPool()
      {
      poolDic.Clear();
      poolObj = null;
      }
      }

      按照之前的测试方法再次测试,得到的Hierarchy窗口布局如下

      image

    2. 再实现将对象放入对应的抽屉根物体中,用面向对象的思想将抽屉相关数据行为封装起来

      新声明一个PoolData​类,代表具体存储某一种对象的抽屉,该类包括具体的抽屉父对象,存储对象的池子(栈),以及获取池子内对象数量的属性
      实例化该类的时候需要传入池子根对象,存储的对象的名字,以方便设置抽屉对象的名字,并将池子根对象对象作为抽屉对象的父对象

      该类为存储对象的池子(栈)的对象,封装存入Push​和取出Pop​的方法,存入或取出时要执行一些必备的处理,例如激活,或者父子对象设置等等

      其中,要存入到池子内的对象,存入前就可以将PoolData类对应的抽屉父对象作为自己的父对象

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      public class PoolData
      {
      //抽屉根对象,用来就行布局管理的对象
      private GameObject poolObj;
      //用来存储抽屉中的对象
      private Stack<GameObject> dataStack = new Stack<GameObject>();
      //获取容器中是否有对象
      public int Count => dataStack.Count;

      public PoolData(GameObject poolRootObj, string poolName)
      {
      //创建抽屉父对象,和柜子父对象建立父子关系
      poolObj = new GameObject(poolName);
      poolObj.transform.SetParent(poolRootObj.transform);
      }

      /// <summary>
      /// 从抽屉中弹出数据对象
      /// </summary>
      /// <returns>弹出的数据对象</returns>
      public GameObject Pop()
      {
      //从抽屉内取出并激活对象,再断开父子关系
      GameObject obj = dataStack.Pop();
      obj.SetActive(true);
      obj.transform.SetParent(null);
      return obj;
      }

      /// <summary>
      /// 将数据压入到抽屉内
      /// </summary>
      /// <param name="obj">压入到抽屉内</param>
      public void Push(GameObject obj)
      {
      //失活后放入将抽屉根对象作为父对象,然后压入到抽屉内
      obj.SetActive(false);
      obj.transform.SetParent(poolObj.transform);
      dataStack.Push(obj);
      }
      }

      由于PoolData​类内封装的存入Push​和取出Pop​的方法已经做好了例如激活,或者父子对象设置等等处理
      因此除了PoolManager​对象池字典内的值改为PoolData​类,诸如存入Push​和取出Pop​的方法不再需要实现激活,或者父子对象设置等等逻辑

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      /// <summary>
      /// 缓存池模块 管理器
      /// </summary>
      public class PoolManager : BaseManager<PoolManager>
      {
      //柜子容器当中有抽屉的体现
      private Dictionary<string, PoolData> poolDic = new Dictionary<string, PoolData>();

      //池子根对象
      private GameObject poolObj;

      private PoolManager() { }

      /// <summary>
      /// 拿东西的方法
      /// </summary>
      /// <param name="name">抽屉容器的名字</param>
      /// <returns>从缓存池中取出的对象</returns>
      public GameObject GetObj(string name)
      {
      GameObject obj;
      //有抽屉,且抽屉内有对象
      if (poolDic.ContainsKey(name) && poolDic[name].Count > 0)
      {
      //弹出栈中的对象,直接返回给外部使用
      obj = poolDic[name].Pop();
      }
      //否则,就应该去创建对象
      else
      {
      obj = GameObject.Instantiate(Resources.Load<GameObject>(name));
      //为了避免实例化出来的对象默认名字带一个"(clone)",我们需要重命名这个对象
      obj.name = name;
      }
      return obj;
      }

      /// <summary>
      /// 往缓存池中放入对象
      /// </summary>
      /// <param name="name">抽屉(对象池)的名字</param>
      /// <param name="obj">要放入的对象</param>
      public void PushObj(GameObject obj)
      {
      //如果根物体为空,就创建
      if (poolObj == null)
      poolObj = new GameObject("Pool");

      //如果不存在对应的抽屉容器,先创建抽屉,再往抽屉内放
      if (!poolDic.ContainsKey(obj.name))
      poolDic.Add(obj.name, new PoolData(poolObj, obj.name));
      poolDic[obj.name].Push(obj);
      }

      /// <summary>
      /// 清除整个柜子当中的数据
      /// </summary>
      public void ClearPool()
      {
      poolDic.Clear();
      poolObj = null;
      }
      }

      按照之前的测试方法再次测试,得到的Hierarchy窗口布局如下:

      image

将窗口布局优化变为可控制开启功能

频繁变化父子对象带来的性能开销是不可忽视的,而窗口布局优化往往只是为了方便我们在Hierarchy窗口上观察和调试对象
因此,这个窗口布局优化应当是可关闭的,当我们发布游戏或者拥有更佳方案时,该功能就应该关闭掉,让对象池只控制对象的是否激活,提高性能

PoolManager​内声明一个公开的isOpenLayout​,代表是否开启Hierarchy窗口自动布局功能

如果不开启,则PoolData​内的构造函数,Pop​和Push​,以及PoolManager​的Push​与自动布局相关的逻辑都需要跳过不执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PoolData
{
//抽屉根对象,用来就行布局管理的对象
private GameObject poolObj;
//用来存储抽屉中的对象
private Stack<GameObject> dataStack = new Stack<GameObject>();
//获取容器中是否有对象
public int Count => dataStack.Count;

public PoolData(GameObject poolRootObj, string poolName)
{
//开启功能时,才会动态创建父子关系
if (PoolManager.isOpenLayout)
{
//创建抽屉父对象,和柜子父对象建立父子关系
poolObj = new GameObject(poolName);
poolObj.transform.SetParent(poolRootObj.transform);
}
}

/// <summary>
/// 从抽屉中弹出数据对象
/// </summary>
/// <returns>弹出的数据对象</returns>
public GameObject Pop()
{
//从抽屉内取出并激活对象
GameObject obj = dataStack.Pop();
obj.SetActive(true);
//开启布局功能时,才需要断开父子关系
if (PoolManager.isOpenLayout)
obj.transform.SetParent(null);
return obj;
}

/// <summary>
/// 将数据压入到抽屉内
/// </summary>
/// <param name="obj">压入到抽屉内</param>
public void Push(GameObject obj)
{
//失活后压入到抽屉内
obj.SetActive(false);
//开启了布局功能,才需要将要压入的对象设置父子关系
if (PoolManager.isOpenLayout)
obj.transform.SetParent(poolObj.transform);
dataStack.Push(obj);
}
}

/// <summary>
/// 缓存池模块 管理器
/// </summary>
public class PoolManager : BaseManager<PoolManager>
{
//柜子容器当中有抽屉的体现
private Dictionary<string, PoolData> poolDic = new Dictionary<string, PoolData>();

//池子根对象
private GameObject poolObj;

//是否开启Hierarchy窗口自动布局功能
public static bool isOpenLayout = false;

private PoolManager() { }

/// <summary>
/// 拿东西的方法
/// </summary>
/// <param name="name">抽屉容器的名字</param>
/// <returns>从缓存池中取出的对象</returns>
public GameObject GetObj(string name)
{
GameObject obj;
//有抽屉,且抽屉内有对象
if (poolDic.ContainsKey(name) && poolDic[name].Count > 0)
{
//弹出栈中的对象,直接返回给外部使用
obj = poolDic[name].Pop();
}
//否则,就应该去创建对象
else
{
obj = GameObject.Instantiate(Resources.Load<GameObject>(name));
//为了避免实例化出来的对象默认名字带一个"(clone)",我们需要重命名这个对象
obj.name = name;
}
return obj;
}

/// <summary>
/// 往缓存池中放入对象
/// </summary>
/// <param name="name">抽屉(对象池)的名字</param>
/// <param name="obj">要放入的对象</param>
public void PushObj(GameObject obj)
{
//如果根物体为空且自动排布开启,就创建
if (poolObj == null && isOpenLayout)
poolObj = new GameObject("Pool");

//如果不存在对应的抽屉容器,先创建抽屉,再往抽屉内放
if (!poolDic.ContainsKey(obj.name))
poolDic.Add(obj.name, new PoolData(poolObj, obj.name));
poolDic[obj.name].Push(obj);
}

/// <summary>
/// 清除整个柜子当中的数据
/// </summary>
public void ClearPool()
{
poolDic.Clear();
poolObj = null;
}
}