细说精灵尺寸与 Unity3d GUI

Sprite & Texture

一般而言,精灵(Sprite)是由贴图(Texture)和矩形(Rect)构成的。矩形描述了精灵使用的是贴图中的哪个部分。

Sprite = Texture + Rect

通常一张贴图只对应一个精灵,于是矩形就和整个贴图大小相同。例如一张 512 * 512 的贴图,矩形则是 Rect(x=0, y=0, width=512, height=512);但从优化的角度来说,一张贴图可以由多个 Sprite 所共享,在加载的时候只需要一次 IO,且在渲染的时候,可以共用一个 Draw Call 。在这种情况下,在同一张贴图上使用不同的矩形框就能呈现出不同的 Sprite,例如:

  • Rect(x=0, y=256, width=256, height=256)
  • Rect(x=256, y=256, width=256, height=256)
  • Rect(x=0, y=0, width=256, height=256)
  • Rect(x=256, y=0, width=256, height=256)

以上 4 个矩形把一张 512 * 512 的贴图分割成 4 个精灵。

还有一种情况是,游戏引擎在优化资源的时候,可以把原来单张的贴图自动或手动合并成一张独立的大贴图来达到同样的优化目的。

Sprite in Scene

但是在 Unity3d 中贴图的大小是可变的,它可以针对对于不同的平台,在打包的时候导出不同尺寸和质量的贴图——那这是否会影响精灵在场景上显示的尺寸呢?

答案是否定的。Unity3d 为每个贴图提供了 PixelsPerUnit 属性,这个属性表示场景中的一个单位长度对应该贴图多少像素。例如,一个宽度为 512 的贴图,它的 PixelsPerUnit 为 100,显示到场景中即 5.12 单位长度。

sprite-inspector.png

Width_Scene = Width_Texture / PixelsPerUnit_Texture

要注意的是,检查器中 PixelsPerUnit 并非最终的 PixelsPerUnit,它会受到导出时的 Max Size 影响。一个使用 512 * 512 的贴图的精灵,但贴图的 Max Size 被设置到 256 时,它被缩小了 50%,为了保证精灵在场景里还是原来的大小,PixelsPerUnit 同样需要缩小 50% 。如果在游戏中读取上面那个在场景中为 5.12 单位长度的精灵的 PixelsPerUnit ,可以发现已经变成了 50,而不是在检查器中的 100 。

PixelsPerUnit_Texture = PixelsPerUnit_Inspector * Compressed_Factor

Sprite in Canvas

在 Unity3d 中场景的坐标系与 UI 的坐标系是完全不同的。UI 通常以像素为单位,方便设计和布局。那么是否直接在 UI 中使用贴图本身的大小来呈现图像就可以了呢?

答案还是否定的。但是从上文的介绍来看,由于贴图受优化的影响,大小是会变化的。我们最终只能知道贴图被优化后的尺寸和 PixelsPerUnit,这能保证精灵在场景中保持原来的显示尺寸,但却丢失了精灵本该有的像素尺寸信息。

有一种方案是给精灵附上原有的像素尺寸信息,这样的话就需要改精灵的组成,使得 精灵 = 贴图 + 矩形 + 尺寸信息 ,这样的话需要为大量的精灵储存额外的信息。但是从开发的习惯上来看,制作 UI 用的精灵通常都使用相同的 PixelsPerUnit ,所以其实只需要记录这个信息,应用到所有精灵上即可。于是 Unity3d 为 Canvas Scaler 提供了一个叫 ReferencePixelsPerUnit 的属性,表示 1 个场景单位长度在 Canvas 中显示为多少像素。这个属性用来将场景坐标中的精灵缩放到 UI 坐标系。这样即使精灵不知道原来的像素尺寸信息,也能按设计时的尺寸显示到 UI 上了。

Width_Canvas = Width_Texture / PixelsPerUnit_Texture * ReferencesPixelPerUnit

所以在开发 Unity3d gui 时最佳实践是为所有 UI 贴图使用相同的 PixelPerUnit,并为 Canvas Scaler 指定相同的 ReferencePixelsPerUnit 。

Canvas 与 Camera 层次关系探究

简单测试了一下 Unity3d 里多个 Canvas 在不同的渲染模式中的绘制的层次关系,记录于此。

Screen Space - Overlay

  • Canvas 尺寸随屏幕大小变化;

  • 所有相机处理完成后,再绘制在屏幕上;

  • 即使场景里没有相机也可以绘制;

  • 当多个 Canvas 的 Render Mode 都为 Screen Space - Overlay 时,按照 Sort Order 参数从小到大的顺序绘制;

Screen Space - Camera

  • Canvas 尺寸随 Camera 变化;

  • 将 Canvas 绘制到指定的 Camera;

  • 指定 Camera 后,该 Canvas 不会再出现在其它 Camera 中;

  • 若多个 Canvas 指定到同一个相机,则按照 Sorting Layer 的顺序绘制;若 Sorting Layer 相同,则按照 Order in Layer 从小到大的顺序绘制;若 Order in Layer 也相同,则按照 Plane Distance 由远到近的顺序绘制;

  • 部分等价于 Sprite ,只是位置和大小由 Camera 控制;

World Space

  • Canvas 尺寸固定不变;

  • 当 Canvas 出现在 Camera 的视野中,就被绘制;

  • 可以被绘制到多个 Camera 中;

  • 若多个 Canvas 重叠,则按照 Sorting Layer 的顺序绘制;若 Sorting Layer 相同,则按照 Order in Layer 从小到大的顺序绘制;若 Order in Layer 也相同,则按照距离 Camera 由远到近的顺序绘制;

  • 等价于 Sprite;

其它事项

  • 不同的 Camera 会按照 Depth 参数从小到大绘制(后绘制的 Camera 需要将 Clear Flags 设置为 Don't Clear,不然会把之前绘制的东西都清空);

  • 当 Render Mode 为 World Space 的 Canvas 与 Render Mode 为 Screen Space - Camera 的 Canvas 出现在同一个 Camera 中时,Sorting Layer 和 Order in Layer 按原有作用生效,与相机的距离等效于 Plane Distance;(类 Sprite)

理解位图字体

位图字体即 Bitmap Font ,将预制的字符以图片的形式渲染在画面上的字体方案。由于是图像,所以支持各种静态的字体特效,例如描边、阴影、渐变等。可以使用许多第三方工具制作而成,最终生成两个文件:一个是包含所有字体图像的图集,另一个是描述字符在图集中的位置、大小以及字体信息的描述文件。

经过搜索,得知使用最广的方案是 Andreas Jönsson 提出的 bmfont 也就是常见的 fnt 格式的位图字体。在他的网站 angelcode.com 对这种格式提供了详细的文档介绍:位图字体规格说明。此外,71 Squared 还对些扩展并提供了多种格式的方案:Bitmap Font File Format

文档中对字符信息的描述是最重要的,每一个 char 行表示一个字符,后面带各种参数,解释如下:

id           字符的编码
x            字符在图集中的左边距
y            字符在图集中的上边距
width        字符在图集中的宽度
height       字符在图集中的高度
xoffset      当字符显示在屏幕上时的x偏移(见下图红点位置)
yoffset      当字符显示在屏幕上时与上行边界的距离(见下图)
xadvance     指定下一个字符绘制的位置(见下图空心红点位置)
page         当一个图集放不下所有字符时,指定字符所在的图集页码
chnl         图集使用的颜色通道(1 = 蓝, 2 = 绿, 4 = 红, 8 = 透明, 15 = 全部).

光有文档还不够,如何解析和使用这些参数才是我探索的目的。好在在 BMFont 的文档中有这么一章:How to render text 介绍了字符是如何显示的:

How to render text

以下是一些能够生成 fnt 位图字体的工具:

[免费] Bitmap Font Generator
[免费] Shoebox
[收费] Glyph Designer
[免费] Littera (Online)
[免费] glyphite (Online)

Unity3d 同步/异步加载资源

在游戏中,加载资源可以分为同步加载(sync)与异步加载(async)两种方式。同步加载易于使用,无需特别组织代码结构,加载过程会阻塞线程,好处是之后的代码可以立即使用加载后的资源:

Texture tex = LoadSync("background.png") as Texture;
image.texture = tex;

既然同步加载会阻塞线程,也就表明,如果在游戏过程使用同步加载的方式去加载较大的资源,会导致游戏掉帧,甚至卡顿,影响体验。

这时候就可以使用异步加载的方式来加载资源。异步加载有代码组织上有多种选择,一种是回调的方式:

LoadAsync("super-big-backgroud.png", (Object data) => {
    image.texture = data as Texture;
});

另一种是使用协程:

IEnumerator loadBackground () {
    AsyncOperation loadRequest = LoadAsync("super-big-background.png");
    yield return loadRequest;
    image.texture = loadRequest.bytes as Texture;
}

StartCoroutine (loadBackground());

协程是 Unity 官方的实现方式,好处是比回调方式看起来更“同步”一些。资源在 yield 返回后即可使用,代码比回调的方式容易组织。但是如果你更喜欢回调风格,可以稍微包装一下:

private IEnumerator _load (string path, Action<Object> onLoaded) {
    AsyncOperation loadRequest = LoadAsync(path);
    yield return loadRequest;
    onLoaded(loadRequest.bytes);
}

public void load(string path, Action<Object> onLoaded) {
    StartCoroutine (_load(path, onLoaded));
}

load(path, (Object data) => {
    image.texture = data as Texture
});

Unity3d

本文基于 unity3d 4.6.x 部分接口在 unity3d 5.x 有变动。

Unity3d 提供了两组接口用于资源加载:Resources 和 AssetBundle 。其中 Resources 用于加载打包后 resources.assets 内的资源,而 AssetBundle 用于加载来自本地文件、网络文件或内存中的 assetBundle 文件。

这意味着 Resources 使用的资源在游戏发布后是不可变动的。而 AssetBundle 可以使用 WWW 类从外部获得新的资源。这也使得对游戏资源进行热更成为可能。所以在项目中使用 AssetBundle 则成为手机网游的标配。

AssetBundle

要从 AssetBundle 中加载资源,首先要先读取 AssetBundle 到内存中,然后再使用 AssetBundle.Load() 或者 AssetBundle.LoadAsync() 来同步或异步加载资源。

但是读取 AssetBundle 到内存中这个过程的同步和异步又是怎么实现的呢?

Async

官方推荐使用 WWW + 协程的方式来异步加载 AssetBundle:

IEnumerator Start () {
    WWW www = new WWW("http://myserver/myBundle.unity3d");
    yield return www;

    // Get the designated main asset and instantiate it.
    Instantiate(www.assetBundle.mainAsset);
}

yield return www; 的时候,会将 www 对象抛给 unity3d 内核进行处理,虽然文档里没有太多内容,但是从接口提供的 WWW.isDone 和 WWW.progress 可以猜测 www 本质上是一个异步操作对象(AsyncOperation)。

官方的这个例子并没有给出如何获取加载进度,这里有个简单的方法,通过另一个协程来管理加载进度即可:

IEnumerator Start () {
    WWW www = new WWW("http://myserver/myBundle.unity3d");
    yield return StartCoroutine(loading(www));

    // Get the designated main asset and instantiate it.
    Instantiate(www.assetBundle.mainAsset);
}

IEnumerator loading(WWW www) {
    while (!www.isDone) {
        Debug.Log (www.progress); // do something with progress
        yield return null;
    }
}

可以把上面的方法封装成一个 Loader,就可以作为通用的 AssetBundler 加载器,并且可以通过事件来获得加载进度和完成情况。

即使是本地文件,即以 file:// 协议开头的资源,也是可以使用上述的异步方式进行加载。

Sync

那么要如何同步加载 AssetBundle 呢?对于远程文件,即 http(s):// 协议访问的文件,要想同步加载,前提是提前下载到本地,如热更新资源,然后通过同步接口读到内存中,最后使用 AssetBundle.CreateFromMemoryImmediate() 接口进行创建:

bytes[] data = File.ReadAllBytes(path)
AssetBundle ab = AssetBundle.CreateFromMemoryImmediate(data);

另外在测试的过程中,我发现如果用 WWW 访问本地资源,而不进行 yield return www; 它也能够同步加载 assetbundle:

WWW www = new WWW("file://" + "/path/to/assetbundle/file");
AssetBundle ab = www.assetBundle; // it works!

经测试,上面这种方法在 iPhone 真机上无法同步加载 assetbundle 。

AssetBundle 生命周期

最后附一张很棒的 AssetBundle 生命周期图:

assetbundle.jpg

参考资料:

全面理解Unity加载和内存管理

Unity3d 项目结构分析

Unity3d 在项目结构上,对特定的目录名赋予了不同的作用。本文作一些简单的记录:

Special Folders

Assets/

对应 Application.dataPath

放置在这个目录下的文件将显示在 Unity Editor 的 Project View 下面(隐藏文件及 meta 文件除外)。

Assets/**/Editor/

放在这(些)个目录下的脚本,只有在 Unity Editor 下会被启用,而不会被编译到最终的游戏中。但有些例外(见 Assets/Plugins)。

Assets/Plugins/

该目录放置一些跨平台的第三方 SDK 的项目资源,如 ulua,微信 SDK、友盟 SDK 等,通常是一些静态库。在打包项目的时候,会根据不同平台将该目录下的资源一起复制到应用的 Libraries 目录下。

Assets/Plugins/Editor/

* 特别需要注意的是:在 Assets/Plugins 中的 Editor 目录,除了 Assets/Plugins/Editor 不会被编译到游戏中以外,其它的 Assets/Plugins/**/Editor 不会被过滤。

Assets/Plugins 并不适合放 Unity 脚本,不要把它当作第三方脚本的目录,因为这些第三方脚本常常会带有 Editor 子目录,这会导致编译的时候由于引用了 UnityEditor 命名空间而报错。

如果一些第三方 SDK 带有 Editor 脚本,正确的位置应该是放在 Assets/Plugins/Editor 下。

Unity3d 5.X 之后,动态链接库形式的插件可以放在任意文件夹,并通过 Inspector 来指定平台。

Assets/StreamingAssets/

对应 Application.streamingAssetsPath

放在该目录下的所有文件,在打包的时候会被复制到游戏应用的特定目录下。如果要处理跨平台游戏,这里应该保持干净,只放一些共有文件。其它的文件可以通过 PostProcessBuild 编写代码根据不同平台复制文件到上述目录。

*/Resources/

放在这(些)个目录下的文件即使没有在场景中引用,也会被归档到 resources.assets 以及 sharedassets*.assets 里面。

这也是最常被误用的文件夹名称,然而值得庆幸的是并不是所有文件都会被归档,只有 Unity3d 常用的格式——像是声音、贴图、材质等,如果是其它文件需要满足这些扩展名才能被归档,如二进制文件可以命名为 *.bytes,以上这些格式将以 TextAsset 类型加载。

多个 Resources 文件夹可以并存于项目中,其中的资源通过 Resources.Load(path) 加载,(path 是相对于 Resources 的路径,不带扩展名),在 Unity Editor 中测试时,Unity 将在所有 Resources 文件夹检索这个资源,打包后则是在 *.assets 文件里检索。

Temp/

对应 Application.temporaryCachePath

比较少被注意到的文件夹,用于存放一些临时文件。在 Editor 脚本需要产生临时文件或中间文件的时候,可以放在这里。

另外可以使用 FileUtil.GetUniqueTempPathInProject() 获得一个可写的临时文件路径,也是放在这个目录下。

Application.persistentDataPath

这个目录指向游戏应用的可读写目录,通常游戏的存档或者热更包都在这里。并且里面的文件不会随着 APP 升级而丢失,除非是手动卸载游戏。

参考资料

Unity Manual: Special Folder Names
Unity Wiki: Special Folder Names in your Assets Folder
Unity Manual: Loading Resources at Runtime