Hello everyone! When the Covid pandemic started, I've got some extra time and decided to use it to create a game for mobile and web platforms. I wanted to resurrect one cool multiplayer remake of a board game that 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 powerful type system that makes the development really joyful and 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 thinking about making a mobile app or a 2D game similar to an app. There are a few roads to choose from, and this article describes my path to hopefully make things a bit clearer.
HTML and SVG for graphics
Although 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 that would be hard to implement just with HTML + CSS. SVG to the rescue! What's cool is that SVG might be easily embedded into the big HTML picture. So, I'm using HTML for the top-level layout, 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 benefits from simple compatibility with various screen sizes and their aspect ratios. And you'll find there's almost an infinite number of screen parameter permutations on Android.
Does the HTML + SVG combo scale well for any graphic effects? Unfortunately, no. Even in my case, I stumbled upon the absence of a feature to manage raster image colors with a relatively simple scene. By design, a player may change the color of his/her car used as an avatar:
The cars themselves are quite complex art pieces, so they are rasterized before using them in the game. I need 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 a low-level fragment shader, and return the result back to the "web world." To be honest, I haven't done partial recoloring yet — the whole car is recolored at the moment — but it does not make a 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 it impossible to use 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 latter and recolor the cars right before the game when players' appearance is known.
To put summary, the HTML + SVG combo is excellent for graphics if you don't require some unique effects. For anything non-standard, OpenGL could help, but you'd either stick to OpenGL altogether, 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 the case of a complex app such as the game, it will quickly become quite complicated. 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 that fits perfectly with the 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. In turn, the components might be quickly 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 problems 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. The re-render will happen 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 make the 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 memoizing some heavy UI parts. Once you've spotted all such moments, 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 that 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 to limit the expressiveness, I didn't suffer from it in practice. At least in the game project where most UI elements are custom and created from scratch in any case. These 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 massive and unprecedented, but it almost made me scream and smash the laptop.
Here is a fraction of the 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 are available as a 3-rd party component, but it flickers on the 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, the 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 flexbox 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 implementing the rest of the game.
Animations is a pain
React Native offers its 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. I'm missing the ability to 100% offload some animations to an artist as 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 do something in JS at the same time when an animation is running, you lose frames, and the app looks snatchy.
There's a way to offload animation to another "fast" thread. Still, 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 are somewhat a bottleneck that 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 saw no road back. One of the project requirements was having a shared codebase 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, the exotic language is the most joyful and robust tier in all the technology stack. The strong yet flexible type system, very simple JS interop, FP-first, and built-in React markup syntax is a breath of fresh air compared to the vanilla JS or TypeScript.
If the project ended up to compile successfully, I'm very confident in the quality of the result. There are no null-pointer exceptions (no exceptions at all if you wish), no forgotten if/else and switch/case paths, no data inconsistency, and fearless refactoring. Any programming should look like this.
ReasonML/ReScript compiles to JavaScript, so I could 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 trivial JavaScript glue.
One particular outcome of choosing a functional language for the back-end was learning DDD (Domain Driven Development) development and its satellites: the onion architecture, CQRS, and friends. These techniques have initially been formulated using Java but the core ideas a so much better aligned with functional programming. I'm pleased with well-structured and easily extensible services that are simple and intensively tested with almost no mocks, stubs, fakes, and other hacks considered to be "normal" for some reason.
So, does ReasonML/ReScript is a perfect language? No, unfortunately. And the reason is the slash between the two words. To be more precise, the reasons are political and not technical. ReasonML and its successor (?) ReScript evolve since 2016. ReasonML is a language built on top of OCaml: the niche OCaml's power 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 the JS platform was fragmented a little: the old school part 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, the core team made a sudden step and released ReScript: the third syntax in a row that 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 minimal 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 before such risky updates. I'm waiting for some clarification and opacity. AFAIK, some Facebook funds are floating around ReScript (formerly around ReasonML), and it can be abandoned if Facebook will stop investing. It might be a good idea to hold on and see the direction of evolution and try to guess Facebook's 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 access to APIs mentioned in a cross-platform fashion.
My game uses the minimum of such APIs (splash screen, local storage, OpenGL interface). Still, 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 cool, but the most critical 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 the case of Google Play Store, even if the users are eager to get new features, any new version has 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 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 the 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 its 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 you build a stand-alone APK.
Last but not least, Expo provides its build server facilities to bundle the app for Android or iOS without having the respective toolchains installed. This provides a 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 building 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 an obvious way to eject if you want to jump off, and the benefits it gives are greatly outweighing the costs.
Version hell
One problem you should be mentally prepared 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 from what reason-react
wants. I'm not saying reason-expo
, react-native-svg
, and @reason-react-native/svg
are all separate packages with their own versioning rules and dependency styles 🤯
Solving this puzzle is not always a trivial task. In one update, I've come 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 succeed with familiar tools. Here's a quick summary of my way:
Scope | Tool | Am I happy | Alternatives to consider |
---|---|---|---|
Scene Tree | HTML/SVG/React | Happy | OpenGL, Pixi, Three.js |
GUI | React Native | Frustrated | Bare HTML5, Flutter |
Functional Language | ReasonML/ReScript | Suspicious happiness | TypeScript, PureScript, 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 I hope you'll find the game quite playable even in the current state.
Top comments (2)
Thanks for thinking this through and giving a detailed review, I appreciated your article but I think you expectations for somethings were unsubstantiated - like that React-Native didn't work as the mobile android app did, when you ran it on the web (kind of a novelty to have any choice in that, isn't it?). Second, saying that facebook may remove funding from rescript is saying that it will "just" become like many other languages that have no formal funding body.
It's like a starving man turning down soup at a homeless shelter saying - "the last place I went doesn't serve soup anymore - if this place stops one day, that would suck. I better not eat any now either."
Again, just some critiques but I think the form and content of your article is great! Thank you!
It's a time investment issue here. If you're going to have to support a project into the future then you'll want something that is stable. The community being small and fragmented is an actual problem. Facebook's funding is important because of the fragmentation and unclear direction. You're right that there are small languages that are just fine without major support but the situations are different. Nim, for example, is the passion project of one man and has a dedicated but small community who have used it for years and years and aren't going anywhere. Julia is focused and fits into a well defined niche. Clojure, like nim, is also the result of someone who is 100% dedicated to keep it going and a small dedicated base.
ReasonML/ReScript Isn't like these projects. From a technical standpoint it's actually really well done BUT, without a cohesive community who is dedicated to supporting the language then it could easily stall out and leave developers with codebases that need to be updated and bindings that no longer work.