Hi friends,
Here are some tips and tricks I’ve used to get great WebGL builds that are fully compatible with mobile devices and work great on desktops too:
-
InputField not being supported on mobile: I ended up making an on-screen keyboard in Unity, and it works fine! Don’t overthink it. Authoring a jslib that calls the javascript
prompt
works fine too. It has been mentioned elsewhere here a bajillion times. -
InputField apparently not supported anywhere. The underlying issue is that the input fields from UGUI and TextMeshPro both use IMGUI Event, which apparently is broken, disabled or otherwise malfunctioning in Unity 2021 LTS and does not interact correctly with Input System. Set Input Handling to “Both” to resolve the issue. If you are reading this and pay for Unity support, kindly ticket this issue.
-
Use flexbox to layout your canvas, and design for a fixed size. I choose 375x635, since that’s the iPhone X layout, and my content is in the iPhone SE safe area of 320x568. Then, I use flexbox to position my canvas in the center, and a post-process step to replace the JSON’s background color. Grab my modified responsive template here. You can discover responsive design sizes by entering “Responsive Design Mode” in Safari (see Imgur: The magic of the Internet). Stop using stupid margin or padding or other dumb CSS tricks and get rid of all the crap that’s in the default frame. Just do the modern thing. Alternatively, design for a responsive size, and full screen the canvas.
-
Use asmdef to control what is Editor versus Player code. This improves your compile speed too.
-
Always try to use a link.xml with high code stripping. There’s lots of documentation now about how link.xml works, but the overall jist is that the code stripper has a very hard time with static methods and third-party code. Be aggressive, don’t just give up and use Medium!
-
Disable modules you don’t need in the Package Manager window. This will require modifying your external libraries.
-
Use the Build Report Tool from the asset store to find assets and scripts you aren’t expecting to include in your build. For me, the biggest surprise was (1) System.Xml being referenced somewhere, and (2) plugins that pollute their Resources directory instead of using Editor Default Resources.
-
Compile for wasm targets and asm.js as a backup. iOS 13.0-13.1.2 do not work with WASM when the build is large, and iOS 12.1 and earlier do not work with Unity’s WASM at all. WASM also occasionally breaks on some specific iOS versions. However, Unity now supports it well for Mobile Safari and works with Apple to resolve issues. You can keep targeting both wasm and asm.js to prevent your experience from ever breaking on Mobile Safari.
-
Use sprite atlases to fine-tune your 2D asset compression. They’re great for controlling which assets need alpha channels and which don’t, and which assets can use crunch compression and which can’t. You should set each sprite’s import settings to correspond to the import settings on the asset, or just use RGBA32 on the individual sprites. Otherwise, the atlas builder will use the compressed version as its input, which is obviously a UX smell but what can we do.
-
Test on iOS 15 with WebGL 2.0, because it isn’t guaranteed to work. Otherwise, disable auto graphics and specify only WebGL 1.0. It’s the only engine supported by Safari for iOS, WebGL 2.0 as Unity implements it doesn’t work for the most part. There’s no point shipping around shaders or code for a WebGL 2.0 target you don’t really need.
-
On 2018-2020, use a UnityLoader.js and UnityLoader.min.js that doesn’t show a popup on mobile and switches to asm.js for iOS 12.1 and earlier. For 2018, I modified them to fix this. You can copy the files from here into your /Applications/Unity/Hub/Editor/2018.3.13f1/PlaybackEngines/WebGLSupport/BuildTools directory or copy them using a post-processing step. Feel free to use your favorite diff tool to visualize what I changed. My minified version isn’t actually minified since it’s tricky to minify this code.
-
Set Application.targetFramerate to -1. This ensures you use requestAnimationFrame to render your frames as quickly as possible.
-
Use asm.js and the Profiler to pin down weird stuff. For example, I discovered that only on iOS, the first call to DateTime.Now took 2275ms! Since you don’t have deep profiling, you should familiarize yourself with Profiler.BeginSample and Profiler.EndSample. This is really slow and annoying but it is super informative for finding real issues.
-
Native calls do not work in coroutines. They just don’t. A native call is one to a function with the [DllImport] attribute. That means if you use e.g. a websocket jslib and try to call a jslib declared method in the coroutine, it will silently fail.
-
Enable demangling in your development builds. This gives you more useful stack traces:
PlayerSettings.SetPropertyString("emscriptenArgs", "-s DEMANGLE_SUPPORT=1 -s ASSERTIONS=1 -s -Werror", BuildTargetGroup.WebGL);
-
Use Unity’s native JSON. You really shouldn’t be using JsonFX/Newtonsoft/JsonObject, it will massively balloon your code size due to the compilation of generic specializations.
-
Use WebAudio yourself, and activate it on a click. I use this for looping music. Contemporary browsers require you to
.play()
audio in a click. Set the handler using htmlonclick="playAudio()"
, otherwise the canvas will eat the clicks. -
Don’t be stupid with third party code. Asset store stuff is good because it works but it can be incredibly poorly written, accidentally referencing stuff like System.Xml, System.Data or System.Runtime and tank everything.
-
Don’t be stupid with fonts. If you only need a single size and a single character set, configure it. If you can use Unity’s UGUI instead of TextMesh Pro, do it. But overall, avoid showing too much text in your game, since that’s what HTML is good for!
-
Test on iOS devices. I know this scene is really big on Windows + Android but you gotta actually buy an iOS device and try it with your mobile site. You simply cannot reproduce the issues experienced by the ordinary user with the iOS Simulator that ships with XCode.
-
Don’t use DateTime. There are weird interactions with locale support and Unity player. Use NodaTime for time.
-
Test for the appropriate Chrome build to test Android devices. My recommendation is Chrome 44, likely the oldest version of Chrome you’ll have to deal with in the real world that can actually play WebGL games distributed this way. I had to use a polyfill for
enumerateDevices
and fix the worker code in the 2018.3 loader to get it to work on this platform. Google how to download old Chromium builds. This platform doesn’t support WASM. -
Use a CDN, like CloudFront. You should definitely never serve directly from S3 or object buckets like that, because it’ll be way too slow over LTE.
-
Use brotli compression. It’s so much better than GZip. If you plan to use CloudFront with an S3 origin, make sure to set the .unityweb file’s Content-Encoding to br, and use an https (with http redirect) configuration on your CDN because Chrome only decompresses br natively for you from TLS URIs. Executing this code from your project directory will fix the “Exception: Failed building WebGL player” you have
-
Build with hashes. It’s imperative you build with hashes for simple setup with your CDN. Otherwise your assets will be perpetually mixed and buggy and you won’t really know why.
-
For compatibility questions, use caniuse.com. This is helpful for platforms you don’t have. Unity3D needs localStorage and WebGL 1.0 in canvas support.
-
Addressables is overrated. Since the content is decompressed and decoded on the main thread, your whole game will freeze, a worse experience that simply downloading the original content.
-
Assetbundles freeze for a while when loading. Loading is single threaded, so you shouldn’t expect to have any interactivity while external files, no matter how slow, are loaded into the Unity player.
-
Application.isMobilePlatform works except for iPads. Use this to determine if you are on a mobile phone browser. iPads will not report correctly.
-
Use PlayerPrefs to store local data. It uses IndexedDB internally and is well supported across browsers. However like all local browser storage it has interactions with playing from the localhost domain (for local testing) and cannot be read by other domains generally.
The single greatest obstacle is that iOS constantly breaks WASM. Apple simply does not care. So always build asm.js.
A 2017 iPhone X is extremely fast, and supports WASM in 12.2. Its graphics and CPU are significantly faster than a Nintendo Switch and any shipping Android device. I guarantee you can make a great-performing WebGL game in Safari as long as you don’t do anything stupid!