Unity提供了强大的Profiler工具,它专门查看每一帧内存的占用以及加载的耗时。此外,它还提供了自定义接口,开发者也可以自定义一个查看区间。

Unity还提供了Frame Debugger,它可以查看每一帧渲染的详细过程。如果DrawCall很多的话,可以参照它来想办法合并。

Unity有丰富的资源加载接口,我们可以在编辑模式下加载资源,在运行模式下加载本地资源和下载资源。

编辑模式加载资源

编辑器模式下Unity可以访问加载到硬盘上的任意资源,这对拓展引擎内置的编辑器是非常好的。引擎可以读取任意资源来丰富编辑器。

编辑模式下的资源可分为两类:一类是引擎可识别的资源,例如Prefab、声音、视频、动画和UI等;另一类是引擎无法识别的资源,例如外部导入资源,这类资源需要通过第三方工具将它的信息解析出来,最终组织成引擎内可识别资源才可以使用。

Unity标志性的类AssetDatabase,它专门负责读取工程内的资源。它读取的资源必须放在项目的Assets目录下,其他位置的资源可以尝试用File类来读取

Assets文件夹

我们在场景中随便放一个cube(没有父对象),坐标设置为(7,7,-6),并拉取放在Assets文件夹下成为一个prefab。然后删掉

cube prefab

我们使用以下代码,在选中场景的摄像机的情况下,执行Chapter11——LoadWrong

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using UnityEngine;
using UnityEditor;

public class GetPrefabWrong
{
[MenuItem("Chapter11/LoadWrong")]
static void MyLoad()
{
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Cube.prefab");
GameObject go = GameObject.Instantiate<GameObject>(prefab);//这里必须把prefab用Instantiate方法转换,因为prefab是从Project拿来的,在代码中不能直接放在scene里面
go.transform.SetParent(Selection.activeTransform);
}
[MenuItem("Chapter11/LoadWrong",true)]//设置没有选中情况下不可用
static bool ValidateMyLoad()
{
if (!Selection.activeObject)
return false;
return true;
}
}

我们会发现GameObject.Instantiate方法只是做了一个克隆,我们在Main Camera下挂载的并不是正常的Prefab(蓝色的)。而使用Transform.SetParent默认会继承世界坐标,这个Cube(Clone)的坐标变成了(7,6,4)

坐标已经改变

之前Cube的坐标为(7,7,-6)世界坐标,这些已经在prefab中记录下来了,如果我们现在直接把这个prefab拖拽进Hierarchy里面,那么它和Cube(Clone)的位置就会重合。因为Main Camera的位置坐标为(0,1,-10)世界坐标,Cube(Clone)变成了Main Camera的子级,Cube(Clone)的坐标就变成了相对坐标,为了保持Cube之前在世界坐标的位置,Cube(Clone)的坐标被换算成了(7,6,4),这是(7,7,-6)减去(0,1,-10)得来的

把上面代码中最后改为go.transform.SetParent(Selection.activeTransform,false);,就能让Cube(Clone)无视之前的世界坐标,不进行坐标转换的操作,依然是(7,7,-6);如果我们把Cube.prefab直接拖拽到Main Camera下作为它的子级,也是不进行坐标转换的操作

实例化Prefab

在编辑模式下,实例化Prefab需要使用PrefabUtility.InstantiatePrefab(),这样实例化后的Prefab就是“蓝色”的了,并且使用方法中提供的Transform参数,就能自动绑定父级,这个绑定父级也是不会做世界坐标的换算。除了Prefab以外,FBX也属于一种特殊的引用关系的资源,它也可以使用这种方法实例化,这样会保持它的一些引用关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using UnityEngine;
using UnityEditor;

public class GetPrefabRight
{
[MenuItem("Chapter11/LoadRight")]
static void LoadPrefabRight()
{
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Cube.prefab");
GameObject go = PrefabUtility.InstantiatePrefab(prefab,Selection.activeTransform) as GameObject;
}
[MenuItem("Chapter11/LoadRight", true)]
static bool ValidateLoadPrefabRight()
{
return Selection.activeGameObject != null && !EditorUtility.IsPersistent(Selection.activeGameObject);
}
}

原坐标不会转换

如果我们使用Transform.SetParent方法而不是在PrefabUtility.InstantiatePrefab中设定父对象,就能保持原位了。

创建Prefab

使用PrefabUtility.SaveAsPrefabAssetAndConnect方法可以创建Prefab,并且自动将Prefab替换创建prefab的对象。

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
using UnityEngine;
using UnityEditor;

public class CreateToPrefab
{
[MenuItem("Chapter11/CreatePrefabs")]
static void CreatePrefabs()
{
// 追踪所有选择的对象
GameObject[] objectArray = Selection.gameObjects;

foreach (GameObject gameObject in objectArray)
{
string localPath = "Assets/" + gameObject.name + ".prefab";

// 使用此方法创建新的同名prefab时会自动创建新的,并在命名后面加数字1
localPath = AssetDatabase.GenerateUniqueAssetPath(localPath);

// InteractionMode.UserAction表示Unity会保存新建动作并且可撤回
PrefabUtility.SaveAsPrefabAssetAndConnect(gameObject, localPath, InteractionMode.UserAction);
}
}

[MenuItem("Chapter11/CreatePrefabs", true)]
static bool ValidateCreatePrefabs()
{
return Selection.activeGameObject != null && !EditorUtility.IsPersistent(Selection.activeGameObject);
}
}

更新Prefab

还是使用上面的代码,进行相应的修改即可

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
using UnityEngine;
using UnityEditor;

public class RefreshThePrefab : MonoBehaviour
{
[MenuItem("Chapter11/RefreshPrefabs")]
static void CreatePrefabs()
{
// 追踪所有选择的对象
GameObject[] objectArray = Selection.gameObjects;

foreach (GameObject gameObject in objectArray)
{
string localPath = "Assets/" + gameObject.name + ".prefab";
// PrefabUtility会自动更新同名Prefab
// InteractionMode.UserAction表示Unity会保存新建动作并且可撤回
PrefabUtility.SaveAsPrefabAssetAndConnect(gameObject, localPath, InteractionMode.UserAction);
}
}

[MenuItem("Chapter11/RefreshPrefabs", true)]
static bool ValidateCreatePrefabs()
{
return Selection.activeGameObject != null && !EditorUtility.IsPersistent(Selection.activeGameObject);
}
}

卸载资源

在编辑模式下,使用GameObject.DestroyImmediate()方法来卸载游戏对象,这个方法第二个参数是allowDestroyingAssets,设置为true的话意思是允许销毁资产引用占用的内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using UnityEngine;
using UnityEditor;

public class UninstallResource
{
[MenuItem("Chapter11/UninstallResourse")]
static void UninstallObject()
{
if (Selection.activeTransform)
{
Object.DestroyImmediate(Selection.activeTransform.gameObject,true);
}
}
[MenuItem("Chapter11/UninstallResourse",true)]
static bool ValidateUninstallObject()
{
return Selection.activeGameObject != null && !EditorUtility.IsPersistent(Selection.activeGameObject);
}
}

游戏对象与资源的关系

游戏对象与资源是一种引用关系。例如一个模型是由贴图和Mesh组成,将它拖入场景中时,生成的游戏对象就会引用这两种资源。当程序调用GameObject.Destroy()或者GameObject.DestroyImmediate()方法时,只会卸载掉它的对象,它身上引用的贴图和Mesh还在内存中。

我们来实验这个场景

新建PrefabDeleteTest脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using UnityEngine;

public class PrefabDeleteTest : MonoBehaviour
{
public GameObject prefab;
GameObject temp = null;
private void Start()
{
temp = Instantiate<GameObject>(prefab);
}
private void OnGUI()
{
if(GUILayout.Button("Delete Prefab"))
{
DestroyImmediate(temp, true);
}
}
}

挂载这个脚本,我们在Assets里面找一个Prefab放在脚本的引用上,然后运行,按Delete Prefab就会删除它

可以看到生成的是Clone

如果我们的代码这么写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using UnityEngine;

public class PrefabDeleteTest : MonoBehaviour
{
public GameObject prefab;
private void Start()
{
Instantiate<GameObject>(prefab);
}
private void OnGUI()
{
if(GUILayout.Button("Delete Prefab"))
{
DestroyImmediate(prefab, true);
}
}
}

我们点击Delete Prefab就会报错

报错

也就是说,我们实例化一个Prefab之后,在Scene中出现的是一个克隆,我们要删也是删掉这个克隆(上上个代码中的temp对象),而不能删除Prefab最原始的“root game GameObject”

Unity这么做是有原因的:很多游戏对象的加载与卸载是很频繁的,如果每次都将引用的资源清理掉,无疑会造成IO的阻塞。

但是如果长期不卸载这些资源,那么内存必然涨上去,所以Unity又提供了一个方法来自动卸载无用资源。其中,无用资源表示没有被别的对象或者代码引用的资源。在编辑器模式下,调用EditorUtility.UnloadUnusedAssetsImmediate()即可。

1
2
3
4
5
6
7
8
9
10
using UnityEditor;

public class ReleaseUnusedAssets
{
[MenuItem("Chapter11/UnloadUnusedAssetsIm")]
static void ReleaseUnusedAssetsImme()
{
EditorUtility.UnloadUnusedAssetsImmediate();
}
}