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.
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.
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.
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
<Text> instead of
<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.
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.
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.
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.
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.
- 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.
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.
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-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.
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.