Sunday, January 1, 2023

Unity WebGL tips / advice in 2023

I recently released a Unity WebGL game and the process was a bit painful. Here's what I learned...

In summary: 

  • I was using built-in pipeline and didn't try URP. (HDRP is definitely out of the question btw)
  • Unity WebGL support isn't bad, and WebGL performance is even OK, as long as you treat it like a ~2015 mobile device in terms of capability and performance. Don't throw a lot at it, especially because iOS browsers can't do a lot...
  • ... because it's 2023 and iOS WebGL performance is still pretty shitty even with Apple's promised ANGLE WebGL 2.0 support. You should expect to do a lot of mitigations and workarounds just so iPhones and iPads don't explode. Meanwhile, Windows and Android browsers are generally solid and reasonable. (In case you can't tell, I'm pretty annoyed with Apple.)
  • Here's what'll happen to you: your WebGL build tests on your desktop browser will work fine and you'll be pleasantly surprised... and then you'll try it on an iPhone and it'll be a mild disaster where you spend a week or two fixing all the various ways it explodes.

(Note: this is current as of Unity 2021.3.11 LTS + iOS 15.)

  • Platform specific code: Application.isMobilePlatform is false for iPads; there's no built-in Unity way to detect iPads on WebGL, you have to do weird stuff with an external .jslib that sniffs the user agent string and passes it back to Unity.
  • Touch: Don't rely on Input.touchSupported which will return true on WebGL on every browser and OS. Instead, it's better to check whether Input.touchCount > 0.
  • Audio: keep it simple, iOS web audio handling is bad.
    • Behind the scenes, Unity strips its internal FMOD and manually reimplements AudioSource etc. with WebAudio API. Only basic core features are supported.
    • In the end I just set all AudioClips to Decompress On Load. Even music files. When I left them on Compressed in Memory, each iOS sound playback would gradually build up lots of static crackling distortion, until the audio playback just completely dies and even crashes the tab.
    • I had the most luck with minimizing use of PlayOneShot(), and just generally playing AudioSources less frequently.
    • Your game audio will continue playing even outside of the browser, because iOS fuckin' sucks. You can implement a OnApplicationFocus(false) { // pauses the game and mute audio } type of feature, which has a 50% chance of actually triggering when the user switches tabs or minimizes the browser app.
  • Mobile keyboard input: as far as I know there's still bugs with user typing in text fields / buggy events and detection. The old advice here was to use an external .jslib that creates a hacky HTML text field above your WebGL canvas or something. In the end my game didn't need any typing so I didn't investigate further.
  • UI: the built-in Event System component has a pretty bad bug where changing canvas size / fullscreening / triggering OnApplicationFocus(false) can break the Event System because iOS browsers don't reliably trigger OnApplicationFocus(true), which means the Event System never turns on again. This means that you won't be able to click on any Unity UI objects after the WebGL context loses focus for basically any reason. To fix this you need to use a custom Event System to override it, like this:
    using UnityEngine.EventSystems;
     
    // from https://forum.unity.com/threads/ui-button-stops-working-permanently-after-switching-tabs-on-mobile-safari.1029688/
    public class EventSystemWebGL : EventSystem
    {
        #if UNITY_WEBGL
        protected override void OnApplicationFocus(bool hasFocus)
        {
            // Do Nothing
        }
        #endif
    }
  • Texture Compression: for comprehensive support, you have to include DXT (desktop) and ASTC (mobile) texture formats. Unity provides a DXT-ASTC dual build editor script example but it's actually broken. Here's an actually working one that I fixed up (Gist), just put it in an /Editor/ folder.
    • To use the dual build setup, you have to modify the WebGL .html template too, or else it won't know how to switch data files. I recommend using the Better Minimal WebGL Template as a base, and mod in the data file switcher JS code.
    • My recommended simple option though: if you're making a fairly small 2D game, then just make a regular single WebGL build set to ASTC. Desktop browsers will decompress the texture and it'll still work in a less optimal way, we're really only worried about iOS browser perf anyway.
  • Spritesheets: using the Sprite Atlas for 2D games is a must, if you haven't been doing that already.
  • Player Settings: I read a lot of superstitions and rumors about what the best WebGL build and player settings are. Here's my superstitions...
    • set to Code Optimization: Speed, and Code Generation: Faster runtime ... iOS WebGL needs all the help it can get
    • enable Run In Background, or else switching iOS browser tabs / resizing the WebGL canvas can cause complete tab freezes (at least it did for me)
    • set Code Stripping: High, remove all unused packages and scripts
    • if you want maximum possible iOS stability, you can also set it to use WebGL 1.0 (though support for this will be removed for Unity 2023.x I think) and Gamma color space (WebGL 1 doesn't support Linear color space)
Anyway. Good luck. You're gonna need it.

***

UPDATE, 27 July 2024: 

I found this Unity Forum thread (https://discussions.unity.com/t/webgl-builds-for-mobile/711925/104) where user Johannski is doing some amazing WebGL testing and profiling with different settings and platforms. https://github.com/JohannesDeml/UnityWebGL-LoadingTest

Some good notes here are:
  • If you're on Unity 2022.x or before, then built-in 3D pipeline is best on WebGL 1. On iOS, built-in pipeline + WebGL 2 can be very slow. Although there's rumors that Apple has updated its shitty iOS WebGL this year as part of its Apple Vision push.
  • URP + WebGL 2 seems to work OK. Absolute minimum build size is about 5-7 mb (brotli compression).
  • However, note that itch.io does not support Unity's implementation of brotli.