18 November 2015

The quest for WebGL - part 2

Find part 1 here.

So now that we have a +/- working version running with Unity5 it was time for the next step: making a webgl build. That wasn't hard, getting the build to run on the other hand was.

1. Communicating with the api

You can't start Kweetet unless you're logged in, so one thing that we needed to get working first was the communication with the api. We used sockets in the webplayer with the UniWeb plugin, but as you might know sockets aren't supported in webgl, so we had to look for other solutions. Our website had a javascript class that already communicated with the api via CORS for other purposes than the game, so we wrote a javascript plugin that uses this class. The documentation on how to do that can be found here, but what's missing is how to use callbacks. Our function needs to call the api and trigger a succes or error callback. It ended up looking like this:

var WebGLAPI = {
  $callbackObject: {},

  InitRestClient: function(successCallback, errorCallback)
  {
    callbackObject.successCallback = successCallback;
    callbackObject.errorCallback = errorCallback;
  },
  
  DoJSCall: function(path, method, data)
  {
    DK.restClient.doCall(Pointer_stringify(path), Pointer_stringify(method), Pointer_stringify(data), function() {
      var returnStr = arguments[0];
      var buffer = _malloc(lengthBytesUTF8(returnStr) + 1);
      writeStringToMemory(returnStr, buffer);
      Runtime.dynCall('vi', callbackObject.successCallback, [buffer]);
      _free(buffer);
    },
    function() {
      var returnStr = JSON.stringify(arguments[0].data);
      var buffer = _malloc(lengthBytesUTF8(returnStr) + 1);
      writeStringToMemory(returnStr, buffer);
      Runtime.dynCall('vi', callbackObject.errorCallback, [buffer]);
      _free(buffer);
    });
  }  
};

autoAddDeps(WebGLAPI, '$callbackObject');
mergeInto(LibraryManager.library, WebGLAPI);

As you can see we need to define a callbackobject where we register our callbacks in an init function. In the C# part that looks like this:

public class JsApiComm {
  [DllImport("__Internal")]
  private static extern void DoJSCall(string path, string method, string data);

  [DllImport("__Internal")]
  public static extern void InitRestClient(Action<string> success, Action<string> failure);

  [MonoPInvokeCallback(typeof(Action<string>))]
  public static void SuccessCallback(string arg)
  {
    ...
  }

  [MonoPInvokeCallback(typeof(Action<string>))]
  public static void ErrorCallback(string arg)
  {
    ...
  }

  public JsApiComm()
  {
    InitRestClient(SuccessCallback, ErrorCallback);
  }
  
  ...
}

Just today I stumbled upon a subtle problem with the code from the manual: to create the buffer to write a javascript string into, the manual uses the length of the string to create a buffer of that many bytes + 1. But if the string contains multibyte characters as in for example "Dieriƫ" then you need to allocate more bytes than characters, or you have a buffer overflow!

The javascript function writeStringToMemory does not look at the size of the buffer so if the string is larger than the buffer it simply writes outside of it. This of course caused many seemingly random crashes in our game, which made it difficult to find the cause at first. The solution is using the function that counts the bytes in the string, and thus enables us to create a buffer of the correct size. I mentioned this on the forum and they're updating this.

2. Memory leak

So with the api in place we can start the game, but even before the first loadscreen was done the browser ran out of memory. I had this in a latest version of Firefox 32bit and it was even worse in Chrome (Chrome has proven to be very bad at most of the things I tried). I then installed a 64bit developer edition, since that one would be able to go beyond 3GB memory and it did: the memory consumption went up to 10GB (!) and kept rising.

This turned out to be a Unity bug in WWW.LoadFromCacheOrDownload that we used all the time. It was this explanation of Jonas Echterhoff that tipped me in the right direction. A must read!

So we temporarily use the regular WWW download and now the memory leak issue is gone. It's supposed to be fixed in 5.3 so I'll try again then.

3. "Could not produce class..."

Another hard to solve problem was that our game crashed when loading a new scene with LoadLevelAsync. Apparently (thanks Marco Trivellato) this is caused by the stripping of classes you never reference in your code but do use in your scenes that you then load via assetbundles. You know you have this issue when you start seeing log entries that look like "Could not produce class with ID xxx". This gives you an ID which you can use to lookup here. For example, we had a few WindZones with ID 182 in some scenes that caused this issue.

4. Development vs Release build

With a development build it's easier to read stacktraces which you need to fix bugs, but most browsers aren't capable to handle the large output of that build (a .js file of 192MB!). Firefox 32 bit tries and succeeds most of the time, Chrome gives up before it executed anything. So I really recommend using the 64bit developer edition of Firefox, that one can handle any build, no matter what I enable or disable.

Luckily, chrome can run a release build, so we're in the clear. But it's still not as fast as Firefox.

Once these issues were solved we finally have a running build! Hurray! I commented out some parts of the game however to get this far, so I have to re-enable those en see what errors I need to fix there. But I'm cautiously confident that we'll succeed to port our game to webgl, how cool is that!

No comments: