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

版本管理文件夹

.meta文件

.meta文件是Unity自动生成的。每个游戏资源都会有一个对应的.meta文件,它会标记资源在引擎中的一些设置信息。也就是我们在Unity中选中一个资源时,在Inspector中设置的导入信息,资源在用户无感知的情况下被Unity优化了。

当我们在Edit——Project Settings——Version Control中,选择Mode为Visible Version Files,就能在文件资源管理器中看到meta文件用来版本管理。

Version Control

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

也就是说各种贴图、材质、模型、脚本、文件夹等都有对应的.meta文件,每个.meta文件都记录着对应资源的guid以及一系列导入信息,每个.prefab、.mat、.unity、.asset文件都记录着需要引用资源的guid

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

.meta文件

所以.meta文件一定要上传到SVN中,否则团队无法统一对资源的设置

多工程

游戏项目会有多个角色进行参与,如程序员、策划员和美术人员等。分工程很容易,但是合并两个工程就比较麻烦了。在Project视图中选择需要导出的资源,单击鼠标右键——Export Package,可导出一个包。打开新的工程,把刚刚导出的包导入,即可实现两个工程的同步。

Export Package

导出选项

同步文件

首先,需要保证美术资源都放在一个最顶层的文件夹下。资源可以按子文件夹归类,但是引用关系只能在最顶层的文件夹中。

同步文件其实就是同步目录,此时使用Export Package命令就不太方便了,可以使用代码直接复制文件夹。因为美术资源都放在同一个顶层文件夹下,资源的依赖只维持在此文件夹内,所以不会引起资源依赖的改变。

新建SynchronizeArtFiles脚本

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

public class SynchronizeArtFiles
{
/// <summary>
/// 复制目录
/// </summary>
/// <param name="strSource">Raw.</param>
/// <param name="strDestination">Copy.</param>
static public void CopyFolder(string strSource,string strDestination)
{
ClearFolder(strDestination);
//*.*是通配符,表示任意名称加任意拓展名。SearchOption.AllDirectories指包括所有子文件夹
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">Copy.</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);//true表示同时删除子目录
AssetDatabase.Refresh();
}
}
}

SVN外链

合并资源还有一种更方便的方式,那就是添加SVN外链。客户端的SVN仓库在Asset文件夹下外链一个美术人员的SVN仓库文件夹,那么美术人员在自己的SVN仓库提交资源时,客户端只要更新就行了,不需要考虑资源的合并。

但是如果SVN打版本分支的时候,外链也必须打分支,不然未来美术人员更新了资源,老的客户端SVN仓库就会把新的资源外链下来。

游戏就是使用这种方法来管理客户端和美术资源的,这样确实方便。

运行模式引用资源

运行模式和编辑模式是完全不同的,编辑模式下可以放成千上万的资源,但是运行环境下要的是优化,在此时Unity主要的资源目录只有ResourcesStreamingAssets

我们在Project窗口中放入一个贴图,新建一个Material,然后再放入一个Cube Prefab,

  • 贴图被Material引用
  • Material被Cube Prefab引用
  • Cube Prefab被Scene引用

最后我们在Build Settings里面把当前的Scene添加进去,那么根据这条引用链,响应的资源都会被打入游戏包中。

Resources

Resources文件夹下的资源无论是否有引用关系,都会被强制打在游戏包中。

在编辑模式下,Resources文件夹可以有多重子目录,打包后,Unity会自动将它们合并在一起,接着在代码中动态读取这些资源,并且加载它。

Resources文件夹

我们可以通过Resources.Load<T>(path)来加载各类游戏资源

如果可以找到,则此方法返回path中的资产,否则返回 null。

请注意,路径不区分大小写,并且不得包含文件扩展名。 Unity 中的所有资产名称和路径都使用正斜杠,因此在路径中使用反斜杠将不起作用。

该path应用于项目的Assets文件夹中名为Resources的任何文件夹。可以使用多个Resources文件夹。如果您有多个Resources文件夹,则不能重复使用相同的资产名称

例如,一个项目可能有名为Assets / Resources/Assets / Guns / Resources/的 Resources 文件夹。path不需要在字符串中包含 Assets 和 Resources,例如在Assets/Guns/Resources/Shotgun.prefab加载 GameObject 只需要 Shotgun 作为path。此外,如果Assets / Resources / Guns / Missiles / PlasmaGun.prefab存在,则可以使用Guns / Missiles / PlasmaGun作为path字符串来加载它。

不要把不需要运行时加载的资源或者已经废弃的资源放入Resources文件夹

Resources文件夹下的资源尽量不要直接引用在场景中,否则这个资源会被场景和Resources打成两份,也就是打包出来后StreamingAssets文件夹内还有一份重复的资源

删除对象

Object.Destroy(Object obj,float t=0.0f)Object.DestroyImmediate(Object obj, bool allowDestroyingAssets = false)

Object.Destroy会等一帧再彻底移除,因为有可能在这一帧后面还有地方在操作这个对象。

Object.DestroyImmediate会立即移除,如果此对象在被移除后在别的地方被引用,就会报错。

第一个参数obj可以是Component,使用此方法后GameObject上挂载的对应Component就会移除;也可以是GameObject,使用此方法后GameObject、GameObject挂载的Component、GameObject的子对象都会被移除。第二个参数t表示从调用此方法开始过多久才会执行,单位是秒。

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

public class ScriptExample : MonoBehaviour
{
void DestroyGameObject()
{
Destroy(gameObject);
}

void DestroyScriptInstance()
{
// 从GameObject上删除此脚本的实例组件
Destroy(this);
}

void DestroyComponent()
{
// 从GameObject上删除Rigidbody组件
Destroy(GetComponent<Rigidbody>());
}

void DestroyObjectDelayed()
{
// 5秒后消除GameObject
Destroy(gameObject, 5);
}

// 按Ctrl键时,GameObject上的BoxCollider组件就会被移除
void Update()
{
if (Input.GetButton("Fire1") && GetComponent<BoxCollider>())
{
Destroy(GetComponent<BoxCollider>());
}
}
}

删除资源

前面我们讲过游戏对象和游戏资源的关系,游戏对象删除了,它引用的资源并没有删除。我们在Profiler工具中,我们选择Memory——把Sample改为Detailed——打开Gather object references。在Asset里面,能查看当前内存中的需要被引用的内容(下图是编辑器模式下的Profiler)。我们可以使用Resources.UnloadAsset()以及Resources.UnloadUnusedAssets()方法强制卸载资源。其中Resources.UnloadUnusedAssets()是异步方法,可以返回AsyncOperation来检测方法是否完成

Profiler工具

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

public class ReleaseUnusedAssetsRuntime : 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("资源卸载完成");
}
}
}

UnityEngine.AsyncOperation - Unity 脚本 API (unity3d.com)

GC

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

新建GCImplement脚本,封装一个内部的GC类,调用两次Resources.UnloadUnusedAssets()GC.Collect()以进行充分的垃圾回收。

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

public class GCImplement : MonoBehaviour
{
public GameObject g1;
private AsyncOperation m_Operation;
void Start()
{
Destroy(g1);
GC gc = GetComponent<GC>() ?? gameObject.AddComponent<GC>();
gc.UnloadUnusedAssets(() =>
{
gc.UnloadUnusedAssets(() =>
{
Debug.Log("彻底卸载掉资源!!");
});
});
}

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

public void UnloadUnusedAssets(UnityAction callback)
{
m_Callback = callback;
System.GC.Collect();
m_Operaton = Resources.UnloadUnusedAssets();
}
private void Update()
{
if(m_Operaton != null)
{
if (m_Operaton.isDone)
{
m_Operaton = null;
m_Callback.Invoke();
//删除自身
DestroyImmediate(this);
}
}
}
}
}