Factini

2024-03-01

I wrote a game. In Rust. In html5 canvas through WASM. It's pretty cool and I figured I should write a couple of blog posts about it before releasing it because I tried to have as few dependencies as possible and no unsafe/unstable code etc. But that also means figuring out a few things along the way.

You can play the game here. (It's free and runs in your browser!)

The game



The game is called Factini and it's a tiny belting game. You have a 15x15 grid on which you can place belts and machines and you move parts from one machine to another to build bigger parts. If you've ever played Factorio (or Mindustry or any of them), you know the drill. Except Factini is a tiny fraction of what those games do.



The tech


Factini is fully written in Rust. I compile it to WASM so I can run it in the browser. In the browser there's a very thin JS layer to bootstrap the WASM and to support the clipboard access. There's also a fallback function for the vendor prefixed Fullscreen API, which I'm not sure Rust will do (at least not without unstable/unsafe).

On the web side, all of Factini happens completely inside a canvas. The production code literally has some script tags and a canvas tag. This means all the interactions (mouse and touch) are hand rolled. Heck, there's a lot that was hand rolled :)

It runs at 100fps on my machine (desktop, linux, both firefox and chrome). Easily. I can crank up the ticks/second rate a few factors before seeing frame drops. But that's also because there's not that much that happens inside a tick considering at the core there are only 15x15 tiles of things to update, plus some bookkeeping. Interactions are only handled once per frame. In fact, by default I think it's already running at a higher tick rate than would be required for the same visual outcome (1000 tick/sec).

Both mouse and touch devices are supported. It'll run fine on a mobile device, though it'll be a bit small. After playing Mindustry for a while I realized that there are some interactions that could be handled better for Factini but I only started playing Mindustry this year and I didn't want to spend more time on rewiring those core mechanics.

web_sys


I do have to call out the main library that I've depended on to use web stuff in Rust. The web_sys crate is fairly complete and does a good job at mapping web tech 1:1 to Rust land.

I expect the canvas stuff is painted off-screen within WASM and frames are sent to the canvas only at the time of painting (the WASM<>JS data pipeline is relatively expensive).

I did run into a few hickups. For example, the clipboard API is not supported in stable tech. The (stable?) fullscreen API does not appear to attempt vendor prefixes. That sort of thing.

You're also fully dependent on what the library exposes. So you can't just access some arbitrary new and/or experimental tech on window like you can in JS.

One other limitation that was a bit annoying is the apparent inability (or very difficult?) to do canvas pixel manipulation. You can read them but not write them. This apparently has to do with memory sharing and the way Rust works. I ultimately gave up trying to get that to work and worked around it.

The motive


I started writing this game to get better at Rust. And because I just wanted to play with a belting game like this.

I did for a time consider whether to publish this as some kind of paid game. But ultimately don't think this game would have significant income potential without significant further time investment and even then it's questionable. That trade-of is currently not worth the risk. And that's fine; I did not need this to make money. I wanted to have fun.

Once I release it I'll also publish the code on GitHub and who knows, maybe some talented artist skins it to something prettier :)

The time


The first commit is from May 2022. I've worked on it off-and-on for a year before getting a bit burned out on it. And then I picked it up at the start of this year (2024) to polish what was there and get it over the finish line.

The burnout was two-fold: lack of artist and lack of inspiration which was blocking progress.

As a result, there's no real end goal to the game. I started writing this maze thingie which looks cute but could not think of a reasonable game goal for it.

The art work


Initially I found a few random tile sets online but nothing I found was what I was looking for in terms of a belting game. So while I was working with placeholder art I found and paid an artist to create a bunch of tiles for me.

This got me the basis for a few things which I could extrapolate to completion. I spent a lot of time in GIMP (image editor) for this :)

For example, I got one tile of each variation of the belt and created all the other rotations and mirrors. They created an example animation for the belt which I could apply to all the variations.

becomes

Another example are the buckets. While I would have loved to get a full set of actual items, it was clear that this was not going to happen. So ultimately I went with the paint can idea. They supplied me an image of the paint can and I used GIMP to create all the other variations.



Other examples of their art are the main loading screen, the logo, and some (but not all) of the buttons. I guess all the more complex looking button art is theirs :p

To be fair, while I was developing the game looked a lot better with the set of placeholder items, rather than the same image reskinned with different colors. But the good news is that the game is highly moddable so have a go if you can!



There's also some art that I would have loved to seen happen but didn't. For example, the machines you place are just an orange background. That could have been something pretty. I also would have loved to see a concrete background and factory backdrop rather than what it is now.

AI


I tried using some AI for this but it was a bit disappointing. Doesn't help that I didn't want to pay for any of the services so there's that.

I thought of setting up a local stable diffusion instance but haven't gotten around to it. Also because the yield was questionable.

As it stands, there's one item in the game that was created by chatGPT (the gold badge). There's also a development item "trash" which it generated. It actually looks great but the item is not part of the game.

The orange "factory" icon at the bottom is also based on something an AI generated, a long time ago.

The challenges


I think the biggest challenges I've faced in this project were inspiration, not having an artist with enough time, and Rust not supporting certain web features (like clipboard).

Of course I handicapped myself by setting certain design goals.

Fast


I wanted the game to be fast. I wanted to see what Rust could do with a game like this.

I think that ended up pretty good. There's a lot going on in the screen. And even if you consider that the game tick doesn't have to do that much (in the grand scheme of things), it still has to all happen in a coherent manner and 1000x per second by default.

It's also painting 100 frames per second, which gives it ample time to put it all on the canvas.

To achieve this I did make some conscious decisions around how the game tick would work, separating UI processing from world ticks, limiting direct JS access, preloading images, etc.

There's definitely more performance to be squeezed out if I wanted to, though. Especially certain images could be pre-rendered more aggressively. The backdrop could be pre-rendered on a second canvas (and even put as the background).

One thing I wonder is whether it would be more efficient to loop over all the parts in flight and handle them as individual particles. Right now the belt cells do all the handling, meaning that I have at most 15x15=225 particle ticks to bother with. But on average you have far fewer belt items and even fewer parts-in-flight to tick. Note that it wouldn't matter for the painting side of things, because that happens separately and is based on a static read of state.

A downside of the web is that you don't truly control the fps. For example, on an ipad the fps would not go above 30. On my laptop on battery, the browser would cap it at 60fps. This is the downside of requestAnimationFrame. And not something you could easily or reliably improve with setTimeout either. Plus, that has a bunch of other downsides. Perhaps at some point you could request/"force" a certain fps. But that has problems too, like rogue websites draining your battery etc.

One other thing I did not even begin to attempt is partial rendering. As it stands, the game will paint the entire canvas from scratch for every frame. But arguably only the floor is fluid. Everything around it is relatively static. And you could use layers to improve that (at the cost of transparency, so hard to predict). If painting performance was an issue I would probably look at dirty rectangle rendering first.

No text


Since everything is canvas driven and I did not want to have to do deal with keyboard inputs I figured I would stay away from anything that required you to write any text. I think this would mostly concern save games anyways.

This idea then evolved into a goal of having as little text as possible. The game would have to be intuitive enough to play without textual explanation. This would also mean the game is language agnostic.

Ironically, the only place where you really see text is the main screen ("start" and "reset"). The rest of the game is icon / image driven.

I went with a parody IKEA manual to give the player some guidance. And I've added an "AI" button which would show the user how to get started. I'm not sure if the manual truly hits the mark but hopefully it'll at least get some laughs :)

Kid friendly


I wanted my kids to be able to play this game. While developing, the kids were between four and eight years old.

I did not want the game to have any violence. No fighting, no war, no enemies. This was fine but it leaves the game end goal kind of questionable. Even Factorio fought against an unknown alien race and half the time you're building stuff just to be able to protect yourself.

Not having this "survival" gimmick kind of limited my options. Or perhaps that just shows my limited view of designing game goals ;) Either way, I stuck to that. No violence. No weapons to craft. No enemies to worry about.

Moddable


Since Rust is a compiled language you have to supply everything ahead of time. For development that's a bit annoying (although that got a lot better once I started using --dev and dropped compile time down from 30s so 4s) but to quickly test changes nothing beats the "edit and refresh" of vanilla web. Also, at least when I started this, cargo did not support a proper "watch" mode.

Additionally it quickly became apparent I needed a simple way of maintaining all configuration of the game assets. And not in actual code.

So I wrote a parser for a custom "markdown-esque" syntax through which all the assets could be configured. The amount of control it gives you is still relatively basic. Like you can define the asset information. The url, the sprite frame offsets and size, but also things like frame index, frame looping, frame delay, etc. Everything you need to create a simple sprite sheet based animated asset.

The definitions can "overload", meaning the last value wins. It's permissive like that but you'll crash the whole game if the syntax goes off the rails. This is because Rust does not support a generic try/catch and by the time I realized that I had already put .expect() all over the place and did not want to bother changing that. At least the crash happens at startup, albeit with a slightly cryptic error message.

These definitions are loaded at startup and obviously add some delay but I don't think you can notice it. Parsing is byte-by-byte with a very simple (implied) grammar. This part does not worry me.

In dev mode I used Mermaid to draw the quest tech tree and show progress. That's helpful since the game itself has no real UI view for the tech tree.

So using these configs you can reskin most of the game. Obviously it controls the part assets and storyline tech tree, but also the buttons, the backdrop, and pretty much every image in the game. It's just their positioning that is hardcoded in Rust code. And hey, if there's a request I could even fairly easily add an offset (abs or rel) to the config to allow you to control positioning through this config file too. lmk!

Fully canvas


While web_sys gives you access to the whole DOM, I wanted to stick to canvas.

While obvious for graphics, this also means having to do all the interactions yourself. Browsers can't help you there.

And let me tell you: browsers do a lot for you. It's no news for me, I've done this before, but it's quite some work to deal with the various interactions like clicking, dragging, and even hover. You have to do bookkeeping for every button, every interactable pixel, every part of the game world. You have to track dragging. And you have to deal with the fact that the lowest level access to this data are browser events.

There was one other quirk that was new to me; turns out the canvas scales implicitly when used in the fullscreen api. The scale can't be read and you have to infer it from a combination of "is the canvas in fullscreen mode" and then checking the width/height of the window. Oof.

One downside of canvas is performance and quality implications of painting at fractions. For example, a well known gotcha is painting a line at integer pixel offsets versus at half pixels. Another is scaling problems, even when requesting the right scaling kind. I don't think this game is painting enough to make it very relevant for performance but you can definitely see quality problems when doing it wrong.

I think I could do better in pre-scaling certain images to catch this but then what happens when you go fullscreen. I suppose you could introduce an extra set of "large" images ("hi res") to support fullscreen better but since you can't predict the screen size you'll always have to deal with resize artifacts. And without pixel manipulation access you're left at the mercy of the browser's scaling algorithm. Spoiler: they have no mercy.

safe and stable


For reasons I can't explain I tried very hard to not use any features from Rust that required unsafe"unsafe" or "unstable".

I dunno. It just felt better that way.

I don't remember concrete things where this caused problems. I think there were a few code constructs that couldn't be done without allowing "unsafe". And there web features that would require "unstable" if you wanted them.

In the end I was able to get here without unsafe and unstable features. So that's pretty cool. I guess :)

The Rust


So what about the core goal of it: do I know Rust now?

Well. A bit.

I would say I'm more comfortable in many parts of the core language. But there's also clearly a bunch of things I've avoided or copy/pasted without fully understanding them (hello *g.borrow_mut() = Some(Closure::wrap(Box::new(move |time: f64| { wat?!).

Somebody advised me to use "god objects" (a pseudo-global object with all state that you juggle around) as a simple way of avoiding the paradigm limitations of the language creating read and write access. That worked well for me but I fear it also limits me a little bit in understanding dogmatic Rust. I need to start reading a proper book on it :)

In the game I juggle about five state objects around. Options (configurable at runtime). State (non-configurable app state). Config (static at build/compile/init time). MouseState (obviously containing interaction state). And Factory (game state). There's also a set of canvas elements ("canvii") which contains cached images.

Almost all main functions accept the three or four objects (options, state, config, factory) and as the functions get "further removed" from the main game loop, they tend to require fewer of these objects.

Like I said, this model works for me and I understand what I should and shouldn't do with them.

That's not to say I'll still fumble a bit with strings (&str versus String) or references.

I feel like I'm cheating with references by using primitives (what does it actually mean to pass in a &f64, anyways) and will arbitrarily use & and * assuming the primitives are copied on stack for "free".

At the same time, any complex struct explicitly does not get the copy/clone trait because I do not want to accidentally introduce copy-move when I don't expect it. If you give it the copy/clone trait it may implicitly copy the reference when it would otherwise move.

I'm also very freely using f64 for many things and convert between usize and f64 a lot. Not entirely sure what the perf implication is for doing so (clearly not massive) or if there is any at all in WASM land. But if I was micro optimizing then in many cases I would not need an f64 and maybe not even a float at all and would pick a more suitable size.

There's also many language features that I just don't use. Some were dodged, like the class system. I tried that initially but then ran into typing problems and threw it all out in favor of flat simple structs.

I applied the PartialEq trait to most enums so I could do simple item == ItemEnum::Xyz to dodge verbose match item {} constructs. I don't mind match but not for all the things. Not sure if this is a "bad practice".

Oh and for web_sys, many of even the most trivial canvas operations have a Result. So I splash .expect("it to work") all over the place. Also not sure if there are real performance implications for this though I'm not sure if you have a choice anyways (aside from not trapping them, of course).

The end


Okay I feel I've written enough for this general overview.

The plan is to write a bunch of posts to expand on certain topics for the game. In no particular order:

- internals
- paint loop
- sprite animations
- animation engine
- custom config md
- sprite map generator
- belt entity
- clipboard access
- map serialization as state
- truck animations
- xorshift for rng
- options, state, config, factory god objects
- bouncer animation
- auto builder
- zones interaction
- manual ui handling
- canvas in fullscreen
- log!() macro

But I'm not going to promise anything here. It's just a plan. These posts are very time consuming to create and I'm not very sure if there's anyone who actually reads them fully.

I really enjoyed writing this game. Even despite getting burned out on it for a bit. It was more of a "now what do I do with the maze" and lack of art work to continue to improve.

I like what the game ended up being and I'm not sure I can properly express the time and effort that went into it, considering it turned out to be such a relatively small game.

Anyways. You can play Factini here. I hope you enjoy it. Let me know what you think :)