How I learned to love.js again, or how to suffer slightly less when trying to make your LÖVE game playable on the web.

boxels, in motion boxels, in all its epilepsy-inducing glory.

Prelude

So I’d released this game called boxels in late 2019 as a submission to Devtober, largely pushed it out because I’d been sitting on it for years and having literally anything to show someone if I was gonna apply for a job felt like a good idea at the time (and it turned out it was! I got employed).

But disregarding that for now, I started making this game years back (originally titled: negative space) using LÖVE, a cute little framework to make games with in Lua, which is all well and good.

… Except the support for building to web has always been some version of shaky, having to compile the whole shebang through emscripten or something similar, with often obscure steps in between that are hard to introspect.

Either way, I now wanted my game playable directly in browser, because who downloads games this small?

So what is love? (.js?)

There’s a very cool project called love.js, (originally by TannerRogalsky here, and then carried on by other members of the community) which is basically as close to a direct port of LÖVE to the web as you can get by cramming it through emscripten, and largely any LÖVE game that runs on desktop would often run after having been emscripten-ed through this process.

So if most games run fine, what’s the big deal?

Well, it turns out after having been built by love.js, there’s at least one Lua feature that is no longer supported, namely goto 4, and this was a fairly annoying roadblock as Lua does not have continue and the first listed workaround is … to use goto, which is exactly what my code did in a number of places to get around this!

So this is stumbling block #1, Lua on the web with love.js is Lua 5.1, without goto, but this is fixable.

… and hey, at least we still have break!
fixing goto issues

… So is that it, are we done now?

Well, not exactly, here’s stumbling block #2!

OpenGL ES.

More precisely, how strict OpenGL ES is with regards to numeric conversions vs desktop OpenGL.

Basically, given an expression like: int v = 2.0;, desktop OpenGL will simply convert the float to an int implicitly, whereas OpenGL ES will give you a nice little cannot convert from 'mediump float' to 'int' or similar.

And given there’s plenty of little numeric literals scattered about any shadercode, this will trip you up.

… And so we fix it, note the other part of the issue here was floating-point suffixes being unsupported.
fixing shader issues with implicit conversions

… So are we done now?

Well, not exactly, I wish we were, but we are not. Another fun issue that came up is some apparent difference between either Lua 5.2 and Lua 5.1, or LuaJIT and Lua 5.1 is that string.format behaves differently in the web build, notably conversions from booleans to strings was implicit in the desktop build, and so expressions like: string.format("is_winner: %s", true) worked fine on the desktop build, but fell apart on the web build.

This turns out to have been a Lua 5.1 <-> Lua 5.2 difference, where Lua internally now calls the equivalent of tostring on formatted values, but presumably LuaJIT 5.1 with extensions did the same thing, and so I just never saw the issue until the web build.

And so we fixed the issue, by adding an explicit tostring call to every place we expected an implicit one:
boxels, fixing tostring #1

boxels, fixing tostring #2

Annoyingly, by the time I found this last issue, the game was already running fine on web and I thought all was well. … Until my lovely friend jammybread tried it and instantly enabled the AI debug mode, which triggered it.

… In Summary

  • goto does not exist in Lua 5.1, and thus does not exist in a love.js build.
  • OpenGL ES shaders are way more particular about numeric conversions than desktop OpenGL drivers!
  • OpenGL ES shaders may sometimes not support floating point suffixes for literals! (… like in my case)
  • string.format will break if you pass it bools and try to build to web, without explicit tostring.
  • Any LuaJIT only features are not supported, such as FFI and bitops (through pablomayobre)

Addendum #1 - added on 2023-01-26

After posting this article in the LÖVE discord, pablomayobre added the following:

Keep in mind that LuaJIT only features are not supported, FFI is particuarly useful when working with Buffers and directly modifying memory, Bitops is just generally nice to have, filling in the gaps of Lua 5.1 that doesn’t have bit operations, and the JIT speeds up the execution so your game may run slower - pablomayobre

Further Reading

1. LÖVE, the cute little framework. https://love2d.org/

2. love.js, or how i got my game onto the web at all https://github.com/Davidobot/love.js

3. emscripten, without which none of this would be possible https://emscripten.org/

4. goto is supported in Lua in LuaJIT as it comes with some extensions, which may lead users (like myself) to make their code depend on Lua 5.2 features as LuaJIT extensions support them and on desktop, LÖVE normally runs under LuaJIT.