资源加载与优化

思考并回答以下问题:

  • 什么是资源?什么是对象?
  • 什么是编辑模式?什么是运行模式?
  • 编辑模式下可以访问加载到硬盘上的任意资源吗?如何访问?
  • 编辑模式下的资源可分为两类:一类是引擎可识别的资源,另一类是引擎无法识别的资源,各举例子。
  • 在编辑模式下AssetDatabase读取资源需要什么硬性条件?如果不满足,使用什么类读取?
  • GameObject.Instantiate()只能创建新对象,这样将丢失Prefab的引用是什么意思?
  • Resources目录是干什么用的?
  • 每个meta文件都会记录guid这个重要信息。guid是什么?
  • 自己总结:Resources.Load<>是加载资源,后面还需要Instantiate才能变成对象,这就是资源和对象的关系,怎么理解?

本章涵盖:

  • 编辑模式
  • 版本管理
  • 运行模式
  • AssetBundle
  • 游戏对象
  • 优化工具
  • 资源管理实例
  • 小结

在游戏开发中,资源(Assets文件夹下的所有东西)管理是非常重要的,管理不好,就容易导致内存溢出,引起闪退或者整体游戏卡顿的现象。Unity提供了强大的Profiler工具,它专门查看每一帧内存的占用以及加载的耗时。此外,它还提供了自定义接口,开发者也可以自行定义一个查看区间。另外,Unity还提供了Frame Debugger,它可以查看每一帧渲染的详细过程。如果DrawCall很多的话,可以参照它来想办法合并。另外,Unity也提供了丰富的资源加载接口,我们可以在编辑模式下加载资源,在运行模式下加载本地资源和下载资源。总之,管理好资源的加载以及优化,才能让游戏更加流畅。

编辑模式

编辑模式并非打包后的运行模式,而仅仅是在编辑器下运行。在编辑模式下Unity可以访问加载到硬盘上的任意资源,这对拓展引擎内置的编辑器是非常有用的。引擎可以读取任意资源来丰富编辑器。但是如果游戏发布后,就会有很多限制,比如编辑模式下可使用的大量代码在运行时无法使用。

加载资源

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

在编辑模式下,Unity提供了一个标志性的类AssetDatabase,它专门负责读取工程内的资源。需要保证所有资源必须放在项目的Assets目录下,不然AssetDatabase是无法读取的,只能使用File类或者其他第三辅助类来读取。

我们在编辑模式下实例化一个Prefab到场景中。

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

public class InstantiatePrefabEditorMode
{
[MenuItem("Assets/My Tools/Load", false, 1)]
static void MyLoad()
{
// 先选取MainCamera再点击Load按钮
if (Selection.activeTransform)
{
// 读取Prefab
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Cube.prefab");

// 实例化到Scene中
GameObject go = GameObject.Instantiate<GameObject>(prefab);
// 设置它的父节点
go.transform.SetParent(Selection.activeTransform);
}
}
}

只要提供一个Assets内的相对路径,使用AssetDatabase.LoadAssetAtPath()方法即可读取Assets文件夹下的任意资源。

如下图所示,Prefab被实例化到了MainCamera节点下,但是为什么它不是“蓝色”的呢?并且坐标为什么没有回归原点呢?因为GameObject.Instantiate()只能创建新对象,这样将丢失Prefab的引用。坐标没有回归原点是因为transform.SetParent会继承世界坐标。

实例化Prefab

在编辑模式下,实例化Prefab需要使用PrefabUtility.InstantiatePrefab()方法。

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

public class Script_11_02
{
[MenuItem("Assets/My Tools/LoadPrefab", false, 2)]
static void LoadPrefab()
{
if (Selection.activeTransform)
{
// 读取Prefab
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Cube.prefab");

// 实例化到Scene中
// GameObject go = GameObject.Instantiate<GameObject>(prefab);
GameObject go = PrefabUtility.InstantiatePrefab(prefab) as GameObject;

// 设置它的父节点
// go.transform.SetParent(Selection.activeTransform);
go.transform.SetParent(Selection.activeTransform, false);
}
}
}

将Prefab实例化到Hierarchy视图中。其中,PrefabUtility.InstantiatePrefab ()方法可以保持原有的引用关系,并非克隆一份新的。另外,给transform.SetParent()的第二个参数传入false,表示它不被父坐标影响,所以就会回归原点。

如下图所示,实例化后的Prefab已经变成了“蓝色”,并且Transform的坐标已经回归原点。此外,除了Prefab以外,FBX也属于一种特殊的引用关系的资源,它也可以使用这种方法,这样会保持它的一些引用关系。

创建Prefab

使用PrefabUtility.SaveAsPrefabAssetAndConnect()方法可以创建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
using UnityEngine;  
using UnityEditor;

public class Script_11_03
{
[MenuItem("Assets/My Tools/Create Prefab", false, 3)]
static void CreatePrefab()
{
if (Selection.activeTransform)
{
string path = string.Format("Assets/{0}.prefab", Selection.activeGameObject.name);

// 如果文件已经存在,删除它
if (AssetDatabase.LoadAssetAtPath<GameObject>(path))
{
AssetDatabase.DeleteAsset(path);
}
// 创建新的Prefab
PrefabUtility.SaveAsPrefabAssetAndConnect(Selection.activeGameObject,
path,
InteractionMode.AutomatedAction);

// 刷新Project视图目录
AssetDatabase.Refresh();
}
}
}

SaveAsPrefabAssetAndConnect表示创建Prefab的同时自动关联到创建它的这个游戏对象。在Hierarchy视图中选择任意游戏对象,并且选择Create Prefab菜单项,会在资源目录下创建Prefab,并将它关联到创建的这个游戏对象本身。

更新Prefab

使用PrefabUtility.SaveAsPrefabAssetAndConnect()方法也可以更新Prefab,不过在更新前,需要判断Hierarchy选择的游戏对象是否为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
using UnityEngine;  
using UnityEditor;

public class Script_11_04
{
[MenuItem("Assets/My Tools/Update Prefab", false, 4)]
static void UpdatePrefab()
{
if (Selection.activeTransform)
{
string path = string.Format("Assets/{0}.prefab", Selection.activeGameObject.name);

// 确保Hierarchy视图中当前选择的是Prefab
GameObject prefab = PrefabUtility.GetCorrespondingObjectFromSource<GameObject>(Selection.activeGameObject);

if (prefab)
{
// 替换它
PrefabUtility.SaveAsPrefabAssetAndConnect(Selection.activeGameObject.gameObject,
path,
InteractionMode.AutomatedAction);
}
// 刷新Project视图目录
AssetDatabase.Refresh();
}
}
}

在上述代码中,我们通过PrefabUtility.GetCorrespondingObjectFromSource()判断当前选择的是否为Prefab(即Assets文件夹中有选中对象的预制体,且Hierarchy视图中是蓝色的):如果是Prefab,则更新它。

卸载资源

在编辑模式下,只能使用GameObject.DestroyImmediate()方法来卸载游戏对象。如果需要卸载游戏对象引用的资源,则第二个参数填true,其默认值是false。

如以下代码所示,选择Hierarchy视图中的任意游戏对象,点击Delete菜单项即可删除它。

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

public class Script_11_05
{
[MenuItem("Assets/My Tools/Delete", false, 5)]
static void Delete()
{
if (Selection.activeTransform)
{
// 第二个参数表示是否卸载游戏对象引用的资源
GameObject.DestroyImmediate(Selection.activeGameObject, true);
}
}
}

游戏对象与资源的关系

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

Unity这么做是有原因的:很多游戏对象的加载与卸载是很频繁的,如果每次卸载都将引用的资源清理掉,无疑会造成IO的阻塞。但是如果长时间不卸载这些资源,那么内存必然涨上去,所以Unity又提供了一个方法来自动卸载无用资源。其中,无用资源表示没有被别的对象或者代码引用的资源。如以下代码所示,调用EditorUtility.UnloadUnusedAssetsImmediate方法即可卸载编辑器下无用的资源了。在运行时也存在类似的资源,后面会讲到。

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

public class Script_11_06
{
[MenuItem("Assets/My Tools/UnloadUnusedAssetsImmediate", false, 6)]
static void UnloadUnusedAssetsImmediate()
{
EditorUtility.UnloadUnusedAssetsImmediate();
}
}

版本管理

多人同时开发游戏时,需要对项目的版本进行管理,通常会将整个工程上传SVN或者Git,然而资源在导入Unity的时候,会自动生成很多中间资源,这些资源是不需要上传的。只需要将Assets、ProjectSettings文件夹下的所有文件以及.meta文件上传即可。

.meta文件

.meta文件是Unity自动生成的。每个游戏资源都会有一个对应的.meta文件,它会标记资源在引擎中的一些设置信息,我们可以在资源视图面板中重新设置这些资源的参数。此时.meta文件中将会保留这些参数。将资源拖入工程时,就会利用这些参数重新压缩资源。换句话说,在用户无感知的情况下被Unity优化了。如下图所示,首先需要在Editor Settings中设置显示.meta文件。

接着,随便打开一张贴图对应的.meta文件,如下图所示。

每个meta文件都会记录guid这个重要信息。guid就是用来关联资源与游戏对象的引用的。比如场景中有个模型,引用了这张贴图,那么模型对应Prefab的.meta文件中就会引用这个guid;如果模型不是Prefab,只是保持在场景中,那么场景的.meta文件就会引用这个guid。所以说,Unity中所有资源的引用关系都是这样来计算的。

再回到这个贴图资源。引擎内可以设置每张贴图的压缩格式、大小和mipmap等,这些信息都会保存在.meta文件中,Unity会根据这些参数重新压缩这个贴图。最终呈现给玩家的贴图,已经不是当初我们放入Unity中的了。所以说,开发者只需要设置一些参数,Unity就实现了无感知的优化。

所以,.meta文件也一定要上传到SVN或Git中,不然别人在更新到这个资源的时候,无法用它本地的Unity对资源做正确的设置了。

多工程

游戏项目会多角色进行参与,如程序员、策划人员和美术人员等,如果不希望一部分代码被美术人员和策划人员看到,那么项目就需要分成两个工程了。分工程很容易,但是合并两个工程就比较麻烦了。在Project视图中选择需要导出的资源,单击鼠标右键,从弹出的快捷菜单中选择Export Package命令,可导出一个包。打开新的工程,把刚刚导出的包导入即可实现两个工程的同步。

同步文件

首先,需要保证美术资源都放在一个最顶层的文件夹下。资源可以按子文件夹归类,但引用关系只能在最顶层的文件夹中。同步文件其实就是同步目录,此时使用Expot Package命令就不太方便了,可以使用代码直接复制文件夹。这里可能有人有疑问:直接复制会不会引起资源依赖的改变?其实是不会的。因为美术资源都放在同一个顶层文件,这样依赖只维持在这个内部文件夹下,即使复制出来以后,还会保留原有的依赖。如以下代码所示,我们来实现文件夹的复制同步功能。

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

public class Script_11_07
{
///<summary>
///复制目录
///</summary>
///<param name="raw">Raw.</param>
///<param name="copy">Copy.</param>
static public void CopyFolder(string strSource, string strDestination)
{
clearFolder(strDestination);

foreach (string from in Directory.GetFiles(strSource, "*.*", SearchOption.AllDirectories))
{
if (!from.Contains(".svn"))
{
CopyFile(from, from.Replace(strSource, strDestination));
}
}
}

///<summary>
///复制文件
///</summary>
///<param name="raw">Raw.</param>
///<param name="copy">Coy.</param>
static private void CopyFile(string raw, string copy)
{
string extenision = Path.GetExtension(raw);

if (extenision != ".Ds_store")
{
if (File.Exists(copy))
{
File.Delete(copy);
}

if (File.Exists(raw))
{
string path = Path.GetDirectoryName(copy);

if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
File.Copy(raw, copy);
}
}
}
}

///<summary>
///清空文件夹下的所有资源
///</summary>
///<param name="path">Path.</param>
static private void clearFolder(string path)
{
if (Directory.Exists(path))
{
Directory.Delete(path, true);
AssetDatabase.Refresh();
}
}
}

运行模式

运行模式和编辑模式是完全不同的,编辑模式下可以放成千上万的资源,那么这些资源是否需要都打包在发布的游戏包中呢?显然是不可能的!打包时,Unity会自动删除掉没有引用的资源,只会保留Resources目录以及StreamingAssets目录下的资源。

引用资源

只有被引用的资源Unity才会打包,那么如何分辨资源是否被引用呢?在下图中:

  • B贴图被New Material材质引用;
  • New Material材质被Cube引用;
  • Cube被Scene引用。

如果这个Scene被添加到了Scene In Build中,那么以上这几种资源都会被打入游戏包中。但是这个Cube在代码中是无法直接操作的,因为没有办法去加载它,只能在打开场景时自动实例化它。

Resources

Resources文件夹是Unity中标志性的目录,这个目录下的资源无论是否有引用关系,都会被强制打在游戏包中。如下图所示,Resources文件夹可以是顶层目录,也可以是某个文件夹的子目录。打包后,Unity会自动将它们合并在一起,接着在代码中动态读取这些资源,并且加载它。

我们可以通过Resources.Load来加载各类游戏资源。

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

public class Script : MonoBehaviour
{
void Start()
{
// 读取材质
Material material = Resources.Load<Material>("New Material");
// 读取贴图
Texture texture = Resources.Load<Texture>("B");
// 读取Prefab
GameObject prefab = Resources.Load<GameObject>("Cube");
// 实例化游戏对象
GameObject go = GameObject.Instantiate<GameObject>(prefab);
// 挂在主摄像机节点下
go.transform.SetParent(Camera.main.transform, false);
}
}

由于Resources目录下的资源都会被打包,所以尽可能不要把不需要运行时加载的资源,或者已经废弃掉的资源放进去,因为这无疑会增大包体。此外,Resources目录下的资源尽量不要直接引用在场景中,不然这个资源会被场景和Resources达成两份。

删除对象

运行时,需要使用GameObject.Destroy()GameObject.DestroyImmediate()方法删除游戏对象。其中,GameObject.Destroy()会等一帧再彻底删除。因为有可能在这一帧的后面还有地方在操作这个对象,所以一般建议使用它来删除对象。GameObject.DestroyImmediate()表示立即删除。如果这局代码后面有地方操作删除的对象,立刻就会报错,具体代码如下所示。

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

public class Script : MonoBehaviour
{
public GameObject g1;
public GameObject g2;
public GameObject g3;

void Start()
{
// 删除游戏对象
GameObject.Destroy(g1);
// 一秒后删除
GameObject.Destroy(g2, 1f);
// 立即删除
GameObject.DestroyImmediate(g3);
}
}

删除资源

游戏对象和游戏资源的关系是什么?游戏对象删除了,它引用的资源其实并没有删除。如下图所示,在Profiler中依然能看到这个贴图在内存中。不过我们可以使用Resources.UnloadAsset()以及Resources.UnloadUnusedAssets()方法强制卸载资源。由于卸载资源是异步操作,所以可以使用isDone来判断是否完成。

如下代码所示,释放游戏对象后,Resources.UnloadUnusedAssets()会释放无用资源,然后在Update()方法中判断isDone是否释放结束。

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 System.IO;

public class Script : MonoBehaviour
{
public GameObject g1;
private AsyncOperation m_Operation;

void Start()
{
GameObject.Destroy(g1);
m_Operation = Resources.UnloadUnusedAssets();

// 也可以强制卸载对象引用的资源
// Resources.UnloadAsset(g1);
}

void Update()
{
if(m_Operation != null)
{
if(m_Operation.isDone)
{
m_Operation = null;
Debug.Log("资源卸载完成");
}
}
}
}

GC

在C#中,可能还会有很多临时对象引用这个游戏资源,这很可能会导致Resources.UnloadUnusedAssets()无法释放掉。因此,在卸载无用资源前,需要保证C#完成垃圾收集工作,而且有时候进行一遍垃圾回收工作是没用的,最好调用两遍GC()和Resources.UnloadUnusedAssets()。

如下代码所示,这里封装了一个内部的GC类,调用完UnloadUnusedAssets()后,再调用一次UnloadUnusedAssets()进行充分的垃圾回收。

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
using UnityEngine;
using System.IO;
using UnityEngine.Events;

public class Script : MonoBehaviour
{
public GameObject g1;
private AsyncOperation m_Operation;

void Start()
{
GameObject.Destroy(g1);
GC gc = GetComponent<GC>() ?? gameObject.AddComponent<GC>();
gc.UnloadUnusedAssets(delegate(){
gc.UnloadUnusedAssets(delegate(){
Debug.Log("彻底卸载掉资源!!");
});
});
}

public class GC : MonoBehaviour
{
public AsyncOperation m_Operation;
public UnityAction m_Callback;

public void UnloadUnusedAssets(UnityAction callback)
{
m_Callback = callback;
System.GC.Collect();
m_Operation = Resources.UnloadUnusedAssets();
}

void Update()
{
if(m_Operation != null)
{
if(m_Operation.isDone)
{
m_Operation = null;
m_Callback();
// 删除自身
DestroyImmediate(this);
}
}
}
}
}

游戏对象

游戏对象就是个空壳子,它关联着所有资源以及组件的引用。游戏对象有一套自己的管理方式,Unity提供了丰富的接口来操作它,比如创建、删除、修改和查询等。

创建游戏对象

创建游戏对象的方式有两种。第一种是从资源中创建对象,Resources读取一个Prefab,接着通过GameObject.Instantiate()实例化创建。另一种就是通过代码创建,即使用new GameObject(),接着在后面挂脚本或者设置参数等。如下代码所示,我们可以在运行时创建一个空的游戏对象,并将其绑定在一个摄像机脚本中。

1
2
3
4
5
6
7
8
9
10
11
using System.Collections;
using UnityEngine;

public class Script : MonoBehaviour
{
void Start()
{
Camera camera = new GameObject("MyCamera").AddComponent<Camera>();
camera.transform.position = Vector3.one * 10f;
}
}

Transform设置排序

0%