Not so long ago, the possibility of creating projects for WebGL was released from beta in Unity. I share my build experience for this platform of a big game project.
Disclaimer: The article is only for those who are going to do something similar - it is very technical and uses Unity-specific terminology.
1. Type loading via reflection
First problem: such a simple code will not work correctly:
var type = Type.GetType("TypeName");
It works, but the type is returned incorrect and empty. Problems begin when working with this type - almost all of its methods return empty values. To make everything right, you need to write like this:
')
var assembly = Assembly.Load("Assembly-CSharp");
Here it is stated that this is the expected behavior.
2. Dynamic resource loading
The following problem occurs when using the Resources.Load or Resources.LoadAll methods. These methods work incredibly long. For a single-threaded browser, this easily becomes critical. Dynamic resource loading by these and similar methods is better not to use at all. Where possible, it is necessary to change it to static (in advance to make links to the necessary prefabs). The difference in load time on my project reached up to ten seconds.
3. File System Synchronization
If you use the file system, you need to know that it is implemented as a wrapper over the database built into the browser. But the most important thing is that the browser is synchronized with this database only via a direct javascript command:
FS.syncfs(false,function (err) {
If you do not call it, then after turning off the browser data may not be saved. You can call it like this:
1) Add this to the
WebGL template :
<script type = "text/javascript" language = "javascript" > function SyncFiles() { FS.syncfs(false,function (err) {
2) Call it from c #:
private void Sync() { #if UNITY_WEBGL Application.ExternalCall("SyncFiles"); #endif }
4. Initializing WebGL and Shader Compilation
When you start the application, initialization of WebGL may take a significant amount of time. This is very important, because this time is taken into account by the browser during the total time the script runs continuously, which is limited (in Firefox, for example, it is 10 seconds by default).
The main time during WebGL initialization is compiling shaders. Moreover, only those shaders are compiled that are on the stage or in prefabs that are referenced from the stage. If you (like me) just took into your game a lot of different assets from different sources, then you will have an indecent lot of shaders and their compilation may take more than 10 seconds.
What do we have to do?
1) Minimize the number of different shaders used in the project. Often the project uses almost identical shaders, which came to it from different purchased assets.
2) If this is not enough, you will have to transfer part of assets to dynamically loaded resources or
bundles . Yes, it's longer than static loading. But dynamic loading can be done pending and load your assets in parts so that the loading of each of the parts exactly takes no more than 10s. As a result, the total load time will increase. But at least the browser will stop pushing the player to stop the hung script.
In order to understand which shaders are used by which assets, it is convenient to write a script.
5. Firefox Cache
If you are going to debug your project using a local server and Firefox, then you will encounter the fact that the browser incorrectly caches part of the WebGL project.
The script is:
I am doing and running version 1. It works. Then I do version 2. I launch - it falls with an incomprehensible error.
This is treated by manually cleaning the FF caches at:
1) C: \ Users \ {NAME UZER} \ Application Data \ Mozilla \ Firefox \ Profiles \ {NAME PROFILE} \ storage \ temporary \
Here you can delete everything at all.
2) C: \ Users \ {NAME UZER} \ Application Data \ Mozilla \ Firefox \ Profiles \ {NAME PROFILE} \ storage \ default \
Here you need to delete only your site. for example
"Http +++ 127.0.0.1 + 7888"
Note: This happens only when working with an HTTP server. If you run the project from the files, the error does not occur, since the caches for each file path are different. But for the server one cache, regardless of the path.
6. Script doesn’t respond for a long time. Stop it?
Firefox will almost certainly ask you about this. If the project is large, it may ask several times.
Partly on this issue was discussed above - in the paragraph on the initialization of WebGL. But it is important to understand how the WebGL project is loaded and what time the browser takes into account when it offers to shoot a hung script.
Launch steps:
1) Loading data from the server by the browser. Here we can not accelerate anything, but this time the browser and does not limit.
2) Data is being decompressed.
For example:
Decompressed Release / w69.memgz in 100ms. You can remove this delay. UnityLoader.js: 1: 775
Decompressed Release / w69.jsgz in 391ms. You can remove this delay. UnityLoader.js: 1: 775
Decompressed Release / w69.datagz in 2764ms. You can remove this delay.
As follows from the explanation, if the server is properly configured, decompression will occur on the fly and this time will become zero. In any case, it is also not limited.
3) Asm.js is being compiled.
Successfully compiled asm.js code (total compilation time 9088ms; stored in cache) 1 56937f89-a8fd-4b65-94aa-453e33be78d8
It may take 5-10 seconds, but is also not limited to the browser, since the code of the game has not yet started.
4) But then the game code starts
And, from the browser’s point of view, the Unity code (such as WebGL initialization) is no different from the code of the game itself. And they are performed in one piece, in a row. Therefore, in 10 seconds, both WebGL initialization and user code should pass. Considering that the initialization time of WebGL usually cannot be shortened more than up to 4-5 seconds on a good computer, it’s better not to risk and reduce the initialization time of the user code to a minimum. Ideally, he should do nothing at all. How, then, to initialize the game? It can be postponed. For example:
void Awake() { DontDestroyOnLoad(gameObject); StartCoroutine(Init()); } IEnumerator Init() { yield return new WaitForSeconds(0.1f);
The point is to immediately return control to the browser and after 0.1 second run your code. After this trick, the browser starts counting 10 seconds again. Accordingly, if your initialization is long, you can further break it into parts in the same way (although it is better to try to shorten it - the user does not like to wait).
One last thing: the limit on 10 seconds of continuous execution of the script is applied not only during initialization.
7. Using bundles
When using asset bundles, it is important to remember that they must be downloaded from the same server as the game itself. Otherwise, it will not work due to a violation of single origin policy.
The second point is to avoid a delay for decompression of the bundle after loading, it is better to use not gz (by default), but lz4 when creating a bundle:
8. Limit to 512 mb
On my machine, no browser was able to allocate the game more than 512mb. Although a lot of memory on the machine. I believe that it is not necessary to allocate to games collected for WebGL more memory than 512mb. And the game itself must be done in such a way that it suffices. Ideally, generally leave 256mb which are the default.
9. Strip engine code
The strip engine code is a tick in the build settings that causes Unity to throw unused system scripts out of the build.
This allows you to significantly reduce the final assembly volume. The problem here is that if some code is used only by assets that are in the bundle, it will also be thrown. And the final assembly will not work. And exception will be absolutely incomprehensible.
Findings:
1) If you get an incomprehensible exception, try to collect removing this tick;
2) You can directly use the scripts needed by the assets from the bundles in the code or use the special feature of Unity - the file link.xml.
10. Developer builds - no fast builds!
When you collect developer build there is a choice between fast build and fast execution. In fact, a quick build takes almost the same (enormous) amount of time and at the same time, on a large project often does not work at all due to lack of memory. Better not to use it.
11. Crunched textures
For WebGL projects, always use crunched texture. Otherwise, the volume of the game will be indecently large for a web application.
In order not to set the type of each texture manually, you can use just such a
technique (you need to overload the OnPostprocessTexture method).
And there is no twelfth problem, just “twelve problems” sounds better than eleven.
UPDATE of June 1st, 2017:
The twelfth problem is still there. It is connected with the transfer of string parameters between Unity and WebGL plugins. Passing a string from Unity to the webgl works fine. But on the contrary - not always.
Unity suggests using a snippet like this:
var buffer = _malloc (val.length + 1);
writeStringToMemory (val, buffer);
Here, val is a string in javascript.
This works as long as the string length coincides with the number of bytes in it. As soon as there are non-single-byte (for example, Russian) characters in the string, this code results in writing beyond the allocated memory, which is expressed in random crashes of your application in arbitrary locations.
So either use encode so that your strings use only characters from latin1, or rewrite this snippet so that it uses the number of bytes, not characters in the string.