Unity WebGL 开发(一)

Unity3D 中的 WebGL 平台旨在替代以前的 UnityPlayer 平台,随着目前多数浏览器都已经能够很好的支持 Html5,因此 WebGL 也更加成熟,为了将项目发布为 WebGL 平台,在打包的时候,Unity 利用 Emscripten 工具链,将引擎的 C/C++ 代码转换为 WebAssembly(一种浏览器可以执行的格式,更加高效),而 C#代码则需要先通过 IL2CPP转换为 C/C++代码,在转换为 WebAssembly。

虽然目前 Unity3d WebGL 程序能够在大部分主流浏览器上运行,但是也存在一定差异,另外,在手机浏览器上是无法正常运行 WebGL 程序的

另外由于平台限制,有些功能在 WebGL 上是不支持的:

  • 不支持多线程,因为 JavaScript 不支持多线程,所以 System.Threading 命名空间下的类不要使用;
  • 不能在 Vs 中进行断点调试,后面会介绍如何进行调试;
  • 不能直接使用 Socket,包括 System.Net下的任何类型,以及 System.Net.Sockets 下的部分类型,以及 UnityEngine.Network,如果需要在 WebGL 平台使用网络功能,可以使用 WWW或者 UnityWebRequest这些都是基于 Http协议的实现,如要需要高实时性,可以选择 WebSockets或者 WebRTC;
  • WebGL 1.0是基于 OpenGL ES 2.0,WebGL 2.0基于 OpenGL ES 3.0,所以存在相应的限制;
  • WebGL 音频是基于自定义的后台,只具备基本的音频功能;
  • WebGL 是 AOT(ahead of time,即静态编译)平台,因此不能使用 System.Reflection.Emit 下的类型进行代码生成,IL2CPP和 IOS 也是如此。

浏览器支持

Unity WebGL 虽然在大部分浏览器上都支持,但是支持程度以及性能表现不一样,另外,在移动设备上是不支持的,虽然在一些高端设备上有可能运行,但是绝大部分设备是没有那么大内存来支持 Unity WebGL 的。

下表列出了FirefoxChromeSafriEdge各个浏览器的支持情况:

Firefox Chrome Safari Edge
WebAssembly 支持 (Firefox52及之后) 支持 (Chrome 57及之后) 支持 (Safari 11及之后) 支持 (Edge 16)
WebGL 1.0 支持1 支持 1 支持 支持
WebGL 2.0 Firefox 51 及之后 Chrome 56 及之后 不支持 不支持
Web Audio 支持 支持 支持 支持
全屏 支持 支持 Safari 10.1 支持
光标锁定 支持 支持 支持 Edge13 及之后
游戏手柄支持 支持 支持 支持 支持
IndexedDB(PlayerPrefs、WWW.LoadFromCacheOrDownload 涉及) Firefox43 及更高 支持 支持(但是 iFrame 中不支持) 支持
WebSockets (Networking 涉及) 支持 支持 支持 支持
WebRTC(WebCamTexture 涉及) 支持 支持 不支持 支持
静态编译 asm.js 支持 不支持 不支持 支持
Large-Allocation Http 响应头2 Firefox 53 或更高 不支持 不支持 不支持
Brotli 压缩算法(减少打包大小) 支持 支持 Safari 11 或更高 支持

说明:

  1. 浏览器厂商为了稳定性,通常会采用黑名单/白名单的方式,针对不同的显卡驱动进行功能限制,在 chrome 中可以通过输入 chrome://gpu 来查看当前 gpu 的状态,或者可以在这里查询;
  2. Large-Allocation 响应头是高速浏览器需要分配大量内存,目前只有 Firefox 支持。

编译运行 WebGl 项目

WebGL 项目打包之后,会生成如下文件结构:

  • index.html
  • TemplateData - 包括logo、加载进度条,只有模板选择 Default 时才有)
  • StreamingAssets - 项目中的 StreamingAssets 文件夹(WebGL平台下 Application.streamingAssetsPath 的值为 http://youer_host_url:port/StreamingAssets)
  • Build
    • UnityLoader.js - 加载 unity 内容的脚本
    • myproject.json - PlayerSetting 中的部分设置以及其他资源的 Url,给 UnityLoader 用的
    • myproject.wasm.framework.unityweb - JavaScript 运行时和插件
    • myproject.wasm.code.unityweb - 编译完成的 WebAssembly
    • myproject.wasm.memory.unityweb -
    • myproject.data.unityweb - 项目中的资源和场景

Build 文件夹下载 *.unityweb 文件,可能是压缩文件,由 PlayerSetting/publishing setting/Compression Format 设置。

生成完毕之后,可以用浏览器打开 index.html 文件,由于 Chrome 不允许运行本地文件,因此需要将生成文件放到 Web 服务器,或者通过 unity 的 Build And Run,unity 会在编译完成之后启动一个本机 web 服务器。

调试模式

可以获得什么:

  • 内置 Console,方便查看错误;
  • 可读性良好的 JavaScript,Release 版的会压缩代码,不具备可读性;
  • 查看调用堆栈;
  • 可以连接 Unity Profiler 进行性能调试;

缺点:

  • 增大发布内容

WebGL 平台不要在 Vs 中进行调试,而要使用 Chrome 的开发者工具进行调试,常见错误:

Problem:The build runs out of memory
这通常在 32 位浏览器上出现,原因是内容占用内存过大,浏览器分配内存失败。

Problem:Files saved to Application.persistentDataPath do not persist
持久化文件没有保存完成,Unity WebGL 将所有持久化数据(PlayerPrefs、缓存)都保存到浏览器的 IndexedDB 中,在开发者工具中,可以通过 Application 标签页查看,这一API 是异步的,所以不知道什么时候能够完成。

Error message: Incorrect header check
通常是服务器配置不正确。

Error message: Decompressing this format (1) is not supported on this platform
通常是加载 LZMA 压缩格式的 AssetBundle 时发生,WebGL 平台不支持 LZMA 格式,改用 LZ4 格式压缩。

重要发布设置

Disable HW Statistics

默认不勾选,表示将会在加载内容时,向 Unity 发送你的硬件信息、用户信息等,建议勾选上,因为国内网络访问 Unity 服务器比较慢,会降低内容的加载速度。

如果勾选的话,在 Chrome 中可以看到它会向 https://config.uca.cloud.unity3d.com/ 发送请求,可以看到他的发送内容。

代码裁剪

Strip Engine Code

默认勾选,表示对于项目中没有用到类型,将会把那一部分代码剔除掉,从而减少编译大小,提高运行时的性能。Unity 会扫描项目中所有从 UnityObject 继承的类型,包括检查它的内部引用以及序列化字段,将会移除没有任何引用的类型,从而减少发布大小,生成的代码也更少、更快、内存占用更少。

代码裁剪可能引起的问题:

可能会裁剪掉实际用到的类型,比如预制体包含一个类型 A,而将该预制体打包到 AssetBundle 之中,Unity 就可能将 A 类型裁剪掉,运行时将在控制到看到错误 Could not produce class with ID XXX,可以按照下面两步来解决该问题:

  1. 根据提示 ID,查找裁剪的类型,查找地址:这里
  2. 将该类型在脚本中引用一下,或者在创建 Assets 目录下 link.xml,格式如下。
1
2
3
4
5
<linker>
<assembly fullname="UnityEngine">
<type fullname="UnityEngine.Collider" preserve="all"/>
</assembly>
</liner>

如果想要查看裁剪之后 Unity 包含了内些类型,可以在打包之后,找到文件 Temp/StagingArea/Data/il2cppOutput/UnityClassRegistration.cpp 进行查看,除此之外没有其他更加方便的方法。

另一一点需要知道的是 Strip Engine Code选项只是针对 Unity 的本机代码,对于托管代码来说,总是会进行代码裁剪,IL2CPP 通过托管 DLL,以及代码脚本中的静态引用,来进行代码裁剪,如果代码中使用反射来获取类型,那么同样的,该类型有可能被裁剪掉,因此也需要添加到 linker.xml中。

Enable Exceptions

该设置主要用来设置如何处理程序中的异常:

  • None 不需要异常支持,性能最好,发布包最小,但是任何错误都会引起系统停止;
  • Explicitly Thrown Exceptions Only : 默认设置,能够捕获由代码中 throw 抛出的异常,并且能够正确执行 finally 语句块,这将会引起生成大 JavaScript 代码更长、更慢,但这通常只在代码引起瓶颈时才要考虑;
  • Full Without Stacktrace:能够捕捉如下异常:
    • throw 抛出的异常
    • 空引用异常,Null Reference
    • 数组越界
  • Full With Stacktrace:和上面多了一个调用栈信息,通常应该在调试阶段使用,并且在 64 位浏览器中测试。

Data Caching

默认启用,会使用资源缓存到浏览器的 IndexedDB 数据库,在后面运行的时候不用再次从服务器下载资源,不同浏览器的缓存策略也不尽相同。

如果只是想进行空引用检查和数组越界检查,而不想完全支持异常,可以通过如下代码来支持:

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

public class WebGLEditorScript
{
[MenuItem("WebGL/EnableNullChecks")]
public static void EnableNullChecks()
{
PlayerSettings.SetPropertyString("additionalIl2CppArgs", "--emit-null-checks --enable-array-bounds-check",UnityEditor.BuildTargetGroup.WebGL);
}
}

降低发布尺寸

WebGl 平台的内容是需要用户通过浏览器,从 Web 服务器上进行下载,所以控制好发布大小,减少用户的加载时间是非常有必要的,可以通过以下几种方式帮助实现:

  • 贴图采用有损压缩格式,即 Crunch Compression;
  • 不要使用开发版本发布;
  • 可以酌情考虑将 Enable Exception 设置为 None;
  • 启用 Strip Engine Code
  • 特别关注第三方托管库,因为它可能依赖很多系统库,造成 Strip Engine Code 不能有效减少尺寸。
  • 启用 Compression Format

AssetBundles

因为 WebGL 在启动之前,需要将所有的资源预加载完成,因此减少启动时间的一个有效方式就是减少系统资源,或者说减少发布资源,可以利用 AssetBundle 将资源从主包中分离出来,这样,只需要加载一个非常小的加载场景即可,另外 AssetBundle 还能帮助资源管理。

在 WebGl 平台使用 AssetBundle 有以下几点需要注意:

  • AssetBundle 中包括主包中没有的类型时,会引起资源加载失败,最好是在 AssetBundle 中不要打包新类型;
  • WebGL 不支持多线程,而AssetBundle 数据在 Http 下载完成后才可用,因此,AssetBundle 就需要在主线程进行解压缩,这会引起主线阻塞,LZMA 在 WebGL 平台是不可用的,因为它是整包压缩,可以采用 LZ4 压缩,它是单个资源独立压缩,即加载单个资源时,不需要解压整个资源包,如果需要更小的资源包格式,可以采用 gzip 或者 brotli 进行二次压缩,不过这需要 web 服务器进行相应的配置;
  • 支持 WWW.LoadFromCacheOrDownload 方法,它采用浏览器的 IndexedDB 来实现缓存;

改变 Build 文件夹位置

当把发布完成的内容放到服务器时,可能需要将 Build 文件夹放到其他的位置,这里需要修改两个地方:

  1. index.html 中修改 json 文件的 url;
  2. 修改 json 文件中各个 url,其中的相对位置,会认为是相对 json 文件的位置。

增量编译

IL2CPP 采用增量式编译,代码的编译结果会缓存到 Library/il2cpp_cache,如果代码没有变化,将不会进行再次编译,如果需要进行重新编译,只需要删除该文件夹即可。

压缩打包

前面提过在 PlayerSetting 中可以设置发布内容的压缩格式,分别是:

  • gzip : 默认格式,压缩率不如 brotli,但是打包速度要快一些,并且 httphttps都支持;
  • brotli : 压缩率最好,但是打包速度要慢,但是只有 https 协议中支持;
  • disabled : 不压缩。

目前主流的浏览器都已经支持了压缩格式,能够在 http(s)传输过程中,将压缩的数据进行解压,这在效率上要比 js 进行解压要快很多。

另外,unity 在发布时也提供了 js 实现的解压器,如果浏览器解压失败的话,将会采用 js 解压的方式,当然这会带来一些性能损失,增长内容加载时间。

为了让浏览器在 http(s)传输过程中进行解压,需要Web 服务器通过 http(s)头来告诉浏览器,传输内容的压缩格式,就需要对 Web 服务器进行相应的设置。

Apache 服务器设置

设置 Build 文件夹中的 .htaccess 文件,如果没有进行创建:

gzip 压缩格式设置

1
2
3
<IfModule mod_mime.c>
AddEncoding gzip .unityweb
</IfModule>

grotli 压缩设置

1
2
3
<IfModule mod_mime.c>
AddEncoding gzip .unityweb
</IfModule>

IIS 服务器设置

IIS 默认是无法访问未知的 MIME 类型,所以需要先给 .unityweb 指定 MIME 类型,这里有两种方式:

  • 通过 IIS Manager 界面,选择网站的 MIME Types 设置,选择添加,然后关联 .unitywebapplication/octet-stream;
  • 通过网站配置文件.config文件,该文件是影响网站所有子目录的,所以只需要修改根目录即可,修改内容如下:
1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.webServer>
<staticContent>
<remove fileExtension=".unityweb" />
<mimeMap fileExtension=".unityweb" mimeType="application/octet-stream" />
</staticContent>
</system.webServer>
</configuration>

上面设置完成之后,需要设置压缩格式支持,同样在 Build目录创建 web.config文件,内容如下:

gzip 压缩

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.webServer>
<staticContent>
<remove fileExtension=".unityweb" />
<mimeMap fileExtension=".unityweb" mimeType="application/octet-stream" />
</staticContent>
<rewrite>
<outboundRules>
<rule name="Append gzip Content-Encoding header">
<match serverVariable="RESPONSE_Content-Encoding" pattern=".*" />
<conditions>
<add input="{REQUEST_FILENAME}" pattern="\.unityweb$" />
</conditions>
<action type="Rewrite" value="gzip" />
</rule>
</outboundRules>
</rewrite>
</system.webServer>
</configuration>

brotli 压缩

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.webServer>
<staticContent>
<remove fileExtension=".unityweb" />
<mimeMap fileExtension=".unityweb" mimeType="application/octet-stream" />
</staticContent>
<rewrite>
<outboundRules>
<rule name="Append br Content-Encoding header">
<match serverVariable="RESPONSE_Content-Encoding" pattern=".*" />
<conditions>
<add input="{REQUEST_FILENAME}" pattern="\.unityweb$" />
</conditions>
<action type="Rewrite" value="br" />
</rule>
</outboundRules>
</rewrite>
</system.webServer>
</configuration>

流式 WebAssembly

2019.2开始,增加了 WebAssembly streaming 选项,它能够提高内容的启动速度,它是通过浏览器在下载 WebAssembly 的同时,进行 WebAssembly 的转换和编译。

Web 服务器需要进行相关配置,和配置压缩格式类似。

Apache 服务器配置

修改 .htaccess 文件

如果未压缩:

1
2
3
<IfModule mod_mime.c>
AddType application/wasm .wasm
</IfModule>

gzip 压缩

1
2
3
4
5
<IfModule mod_mime.c>
AddEncoding gzip .unityweb
AddEncoding gzip .wasm
AddType application/wasm .wasm
</IfModule>

brotli 压缩

1
2
3
4
5
<IfModule mod_mime.c>
AddEncoding br .unityweb
AddEncoding br .wasm
AddType application/wasm .wasm
</IfModule>

IIS 设置

修改 Build 文件夹的 web.config 文件。

未压缩设置

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.webServer>
<staticContent>
<remove fileExtension=".unityweb" />
<mimeMap fileExtension=".unityweb" mimeType="application/octet-stream" />
<remove fileExtension=".wasm" />
<mimeMap fileExtension=".wasm" mimeType="application/wasm" />
</staticContent>
</system.webServer>
</configuration>

gzip 设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.webServer>
<staticContent>
<remove fileExtension=".unityweb" />
<mimeMap fileExtension=".unityweb" mimeType="application/octet-stream" />
<remove fileExtension=".wasm" />
<mimeMap fileExtension=".wasm" mimeType="application/wasm" />
</staticContent>
<rewrite>
<outboundRules>
<rule name="Append gzip Content-Encoding header">
<match serverVariable="RESPONSE_Content-Encoding" pattern=".*" />
<conditions>
<add input="{REQUEST_FILENAME}" pattern="\.(unityweb|wasm)$" />
</conditions>
<action type="Rewrite" value="gzip" />
</rule>
</outboundRules>
</rewrite>
</system.webServer>
</configuration>

brotli 设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.webServer>
<staticContent>
<remove fileExtension=".unityweb" />
<mimeMap fileExtension=".unityweb" mimeType="application/octet-stream" />
<remove fileExtension=".wasm" />
<mimeMap fileExtension=".wasm" mimeType="application/wasm" />
</staticContent>
<rewrite>
<outboundRules>
<rule name="Append br Content-Encoding header">
<match serverVariable="RESPONSE_Content-Encoding" pattern=".*" />
<conditions>
<add input="{REQUEST_FILENAME}" pattern="\.(unityweb|wasm)$" />
</conditions>
<action type="Rewrite" value="br" />
</rule>
</outboundRules>
</rewrite>
</system.webServer>
</configuration>