Mobile Game with ReasonML/ReScript and React: My Experience
Hello everyone! When the Covid pandemic has started I’ve got some extra time and decided to use it for creating a game for mobile and web platforms. I wanted to ressurect one cool multiplayer remake of a board game which shut down ten years ago. Since a digital tabletop game sounds like something which might be done without fancy 3D graphics I decided to outwalk traditional tools like Unity and make everything like a web application. The first reason is that I have no experience with Unity, and the second reason is Reason 😄 I mean ReasonML, an emerging strongly typed language tightly integrated with React, which compiles to JavaScript.
ReasonML has a very powerful type system which make the development realy joyful and the result surprisingly reliable and bug-free. I’ve got some experience with ReasonML for classic web development, so it shouldn’t take more than 2-3 months of evening/weekend programming to complete the game. Oh, I was mistaken. Nevertheless, the game is released and playable.
And now I want to share the experience of making a mobile game using Expo + React Native + ReasonML/ReScript + NodeJS stack. I’m writing this article for JavaScript web developers who are thinking about making a mobile app or a 2D game which is similar to an app. There are a few roads to choose from and one describe my path to hopefuly make things a bit more clear.
HTML and SVG for graphics
Despite that I have no 3D graphics the game itself is far from being similar to a web page with text and pictures. The game screen looks like this:
As you may see there are plenty of elements which would be hard to implement just with HTML + CSS. SVG to the rescue! What’s cool is that SVG might be easilly embedded into the big HTML picture. So, for the top-level layout I’m using HTML whereas in tight places I employ SVG to draw some ellipses, arrows, shines, etc.
For example, the game board, player stats pane, and action buttons are laid out with HTML flex containers, whereas the elliptic TVs with player avatars and cash counters are rendered with SVG primitives. The use of HTML on the top level gives a benefit of simple compatibility with wide variety of screen sizes and their aspect ratios. And you’ll find there’s almost an infinite number of screen parameter permutations on Android.
And does the HTML + SVG combo scales well for any graphic effects? Unfortunatelly, no. Even in my case, with the quite simple scene I stumbled upon absence of a feature to manage raster image colors. By design a player may change the color of his/her car used as an avatar:
The cars themselves are pieces of quite complex art, so they are rasterized before using them in the game. What I need is to rotate the hue of the color in places denoted by a mask stored in another image. This cannot be done with SVG. The only option I found is going deeper and use OpenGL to solve this particular problem. That is: take the input images, do the required color processing with low-level fragment shader, and return the result back to the “web-world”. To be honest, I haven’t done particial recoloring yet — the whole car is recolored at the moment — but it does not make difference in understanding the big picture. Falling back to OpenGL when necessary works but not without some issues. The main problem here is performance: although rendering a frame is blazing fast (10 ms in my case), snapshotting and transferring the frame back to the world of image tags and PNGs introduces a penalty of ~150 ms. It makes impossible using OpenGL in this way in real-time. You have to either keep some parts of the screen (or the whole screen) in the OpenGL world forever, or use it only to prepare/process some resources once. Now I use the later and recolor the cars right before the game when the appearance of players is known.
To put summary, HTML + SVG combo is absolutely fine for graphics if you don’t require some unique effects. For anything non-standart OpenGL could help but you’d either stick to OpenGL completely dropping HTML and SVG or use it only when a game “level” loads.
React as GUI framework
OK, HTML and SVG can make the scene but how should we translate the current game state to the proper UI tree and UI actions back to game state handlers. One could use vanilla JS but in case of complex app such as the game it will quickly become quite complex. At the very best it would lead to creating a new framework from scratch. It might be interesting but wasn’t my purpose.
The natural choice for me was employing React. As you likely know, React is a declarative UI framework which fits perfectly with functional programming paradigm. The ReasonML/ReScript language is primarily functional and even includes support for React-style markup (like JSX) right into the language.
In general, using React Native along with React Native SVG is very productive to get the first results quickly. The whole game is easily split into dozens of well encapsulated components. The components in turn might be easily inspected visually and in various states one by one, without waiting for a proper game situation. Thanks Storybook for that.
Of course, nothing can be perfect and React is not an exception. One of the problem is performance. I’m not saying React is slow but you can easily make a “mistake” which will cause the whole component tree to re-render even if all that has been changed is the color of one hair-width line in the bottom-right corner of a small icon which is in fact hidden by another element right now. These excessive re-renders makes app jerky. You’ll have to carefully catch all such moments with React developer tools to analyze why the undesired computational spike has appeared and polish this snatch by properly memozing some heavy parts of UI. Once you’ve spotted all such spots the game becomes performant and joyful to play.
React Native for mobile
The original React framework is designed to drive in-browser single page applications. But the applications for Android and iOS are not web pages. They are freestanding beasts which should be developed natively with Kotlin and Swift. How should a web app appear as a full-fledged mobile app? Here comes React Native.
React Native is a specific subset of the general React which has <View>
’s instead of <div>
’s, <Text>
instead of <span>
, no <ul>
or <ol>
, own CSS-in-JS framework, etc. While it might seem limiting the expressiveness at the first glance I didn’t suffered from it on practice. At least in the game project where most UI elements are custom and created from scratch in any case. This all are minor problems compared to the HUUUGE benefit: you develop once and build for all the platforms at once: Web (for desktops and mobile without installation), Android, iOS.
This is what the docs promise. In practice React Native is buggy, glitchy, scattered, and non-obvious in many places. I’m not blaming anyone the framework is really massive and unprecedented; but many times it almost made me screaming and smashing the laptop.
Here is a fraction of problems you might face:
- No box shadows on Android: do it yourself
- At most one text shadow may be specified
- Text nested Text does not work on Android if it changes font face
- SVG nested in SVG does not work correctly on Android
- SVG images stored as built-in asset files do not work on Android
- SVG effects are not available: no shadows, no blur, nothing
- Custom fonts do not work in SVG on Android
- SVG interactions do not work
- Preloading of fonts does not work on web
- Preloading of SVG does not work on web
- Linear gradients are not available via styles; however, they available as a 3-rd party component which flickers on first render
- Radial gradients are not available
- CSS animations are not available
- Hardware-accelerated animations are not available on the web
- SVG stroke opacity animation is broken on Android
- In contrast to the browser, mobile app can suddenly crash on something as innocent as an arc path with zero radius; hard to find the reason
- Sub-pixel rounding is buggy on Android causing ±1 pixel gaps and overflows
- Absolute positioning inside a reverse-order flex box is broken on Android
- Z-index does not work on Android
- etc, etc, etc
I haven’t touched iOS yet but expect a pile of problems too extrapolating what I’ve got with Android. Making the already functional web-version work on Android took me ~30% of the time spent actually implementing the rest of the game.
Animations is a pain
React Native offers own animation subsystem known as Animated. So, what’s wrong with it? Well, nothing once you get it but the process of describing the animation is time consuming and somewhat non-intuitive, especially in cases with long tracks of tricky intermediate keyframes, sequences, and perfect-timing. It’s like trying to program an image directly out of your head bypassing any trial in a graphic editor: doable but complicated. What I’m missing here is an ability to 100% offload the question of some animations to an artist like I can do with illustrations. That’s the reason I had to skip implementing most of the animations before the release. Many of them are still on the TODO-list.
What makes animations even more problematic is the architecture of React Native which runs them by default on the same thread as the JavaScript code. So, if you you do something in JS at the same time when an animation is running, you loose frames and the app looks snatchy.
There’s a way to offload animation to another “fast” thread but it should be carefully planned and the only values allowed to animate in this case are non-layout properties such as translation, rotation, scale, and color.
In summary, animations in React Native is somewhat a bottleneck which can be worked around but it takes so much development energy.
ReasonML/ReScript as language
If I’d been a more mainstream web-developer, I use TypeScript to program the React Native app. But some time ago I was infected by the ideas of functional programming and see no road back. One of the requirements for the project was having a shared code base for the front (the app) and the back (multiplayer server). Filtering the possible language options (Elm, F#, Dart, PureScript, Haskell) through this matrix, not so many variants were left and I’ve chosen RasonML/ReScript.
Long story short, in all the technology stack, the exotic language is the most joyful and robust tier. The strong yet flexible type system, very simple JS interop, FP-first, and built-in React markup syntax is a breathe of fresh air in contrast to the vanilla JS or TypeScript.
If the project ended up to compile successfully, I’m very confident in quality of the result. No null-pointer exceptions (no exceptions at all if you wish), no forgotten if/else and switch/case paths, no data inconsistency, fearless refactoring. Any programming should look like this.
ReasonML/ReScript compiles to JavaScript, so I was able to write a shared game engine for both: the client app and multiplayer server. The client then is built further with React Native and the server is running with NodeJS. The project is 95% ReasonML/ReScript. The rest is a bit of JavaScript glue.
One particular outcome of choosing a functional language for the back-end was learning DDD (Domain Driven Development) development along with its satellites: the onion architecture, CQRS, and friends. These techniques were originally formulated using Java but the core ideas a so much better aligned with funtional programming. I’m very happy with well-structured and easily extensible services which are simple and intensively tested with almost no mocks, stubs, fakes, and other hacks which are considered to be “normal” for some reason.
So, does ReasonML/ReScript is a perfect language? No, unfortunatelly. And the reason is that slash between the two words. To be more precise, the reasons are political and not technical. ReasonML and its successor (?) ReScript develop since 2016. ReasonML is a language built on top OCaml: power of the niche OCaml with the syntax familiar to JS developers. Then, there was a thing called BuckleScript (BS) which compiles OCaml (or ReasonML) to JavaScript. The community targeting JS platform was fragmented a little: the old school part of it used OCaml syntax and the newcomers used ReasonML. This was annoying but since both languages are just different presentations of the same abstract syntax tree, the library ecosystem was (and is) 100% compatible. Arguably the community center of the mass has slowly moved toward ReasonML and it got the traction. But recently a sudden step was made by the core team and they released ReScript: the third syntax in a row which no longer 100% compatible with OCaml AST. At the same time ReasonML and OCaml BS were deprecated. This happened in a single day and many people (including me) were left with projects written in deprecated languages. The community was fragmented again:
- BS OCaml is killed
- ReasonML is forked now and maintained by others, slowly-slowly shifting toward OCaml
- ReScript is the new official but have a very little user base
Yes, there are tools to almost automatically convert ReasonML to ReScript (which look very similar at the bottom line). But I haven’t done it because I not sure what else harsh steps the core team might perform and I have many things to polish prior to such risky updates. I’m waiting for some clarification and opacity. AFAIK, some Facebook funds are floating around ReScript (formely around ReasonML) and it can be abandoned if Facebook will stop investing. So, it might be a good idea to hold on and see the direction of evolution and try to guess Facebook rationale.
Expo as app platform
Is React Native enough to get a working app targeted to multiple platforms? Technically it is. But apart from UI an app is likely to require some other features from the device: the camera, file system, location, or something like this. Here comes Expo. It’s a platform built on top of React Native which provides the access to APIs mentioned in cross-platform fashion.
My game use the a minimum of such APIs (splash screen, local storage, OpenGL interface) but even with such small requirements for me, a programmer who develops for mobile for the first time, Expo is very valuable and simplifies the standard tasks.
API access is fine but the most important thing Expo offers is the OTA (Over the Air) updates. Do you realize that mobile apps are much more familiar to the good old desktop apps in the sense of deployment: you publish an update and don’t know when a user will update your app and whether they are going to update it at all. Things get worse if your app is a client to some online service: evolving the service, you always have to keep in mind that some clients can use the one-year-old stale version of your app. In case of Google Play Store, even if the users are eager to get new features, any new version have to pass moderation which takes some random amount of time between two hours and several days. Albeit not a secret, it might come surprising for a web-developer that the deployment takes days, not seconds.
OTA updates help a lot here. When you publish an update, an incremental changeset is generated and gets stored on Expo’s CDN (or your CDN if you want). Then, when a user launches your app it downloads the required updates in the background and next time the app is restarted the user sees its latest version. All this without waiting for Google Play moderators or the mass app update night.
Another invaluable thing Expo offers is their mobile app to quickly preview what you get on the device without the full build/reinstall/restart cycles. Make a change, wait a few seconds and you see almost the same result you’ll get if build a stand-alone APK.
The last but not the least, Expo provides its build server facilities to bundle the app for Android or iOS without having the respective toolchains installed. This provides quick start and simplifies CI configuration. You can build locally if you want, but in my case, at least in theory, the feature will allow to build for iOS without having to buy a MacBook (I use Arch, BTW): iPhone stolen from my wife would be enough for tests.
In summary, Expo adds a lot to the React Native base. It is a for-profit project which introduces another little layer of WTF’s and bugs, and at the same time Expo offers a very clear way to eject if you want to jump off and the benefits it gives are greatly outweight the costs.
Version hell
One problem you should be mentally prepare for is package version hell. Do you remember that the ReScript platform (e.g. version 8.4.0) and ReasonML (e.g. version 3.6.0) are different things? To work with React a binding library is required (e.g. reason-react
version 0.9.1 and reason-react-native
version 0.62.3). Expo (e.g. version 39.0.0) has its own expectations on the version of react-native
(e.g. version 0.63.0) which in turn requires a specific version of react
(say, 16.3.1) which can differ with what reason-react
wants. I’m not saying reason-expo
, react-native-svg
, and @reason-react-native/svg
are all separate packages with own versioning rules and dependency styles 🤯
Solving this puzzle is not always a trivial task. In one update I've came to a situation when Yarn refused to install what I asked in the package.json
until I deleted yarn.lock
and started over. Not the most pleasant task to work on but so is reality.
Final words
Is it possible to make a full-stack game using only the web development tools of the JavaScript world? Yes, definitely! Does it worth it? It depends. If you have zero knowledge in web development and game development, go with traditional tools like Unity.
If you get some web development background, you can success with familiar tools. Here’s a quick summary of my way:
Scope | Tool | Am I happy | Alternatives to consider |
---|---|---|---|
Scene Tree | HTML/SVG | Happy | OpenGL, Pixi, Three.js |
GUI | React Native | Frustrated | Bare HTML5, Flutter |
Language | ReasonML/ReScript | Suspicious happieness | TypeScript, Dart |
Platform | Expo | Happy if forget about React Native | Cordova, Dart |
And have I mentioned my game? I welcome you to the Future if you have a spare hour to kill ;) I have literally dozens of things to complete yet, but even it the current state I hope you’ll find the game quite playable.