Learn how Rust, ECS, and WebAssembly come together to create a turn-based roguelike game, with challenges, solutions, and valuable lessons for game developers.
"The Bit Knight, the king calls you to serve. If you get the princess back, you will be rewarded."
This article will focus on turn-based roguelike games' design/architecture level. For implementation details, please visit my repository or repo of the author of the book titled “Hands-on Rust” (link to the repo). The game is based on the proprietary bracket-lib library, but the knowledge contained in the book can be transferred to another engine, e.g., bevy (work in progress :)).
From an early age, I was fascinated by video games. After mastering the art of getting the bits, one of my first thoughts was, "I'm going to be a game dev programmer." However, I've ended up "web dev," mainly dealing with the front-end. However, my mind wandered to the time when I was reading "C ++ language . The School of Programming" book from cover to cover.
Three years ago, I found out about the Rust language. It is perfect for me because it combines many concepts I know from functional programming in JS and the power of a low-level language. I immediately thought about making a game. However, I put off the idea, wanting first to bring my Rust skills to the right level. I did just that when I found the book titled "Hands-on Rust: Effective Learning through 2D Game Development and Play." Roguelike + Rust? Only the RPG + Rust combination could have been more beautiful. I heard a voice in my head telling me to try it.
"The knight visited the all-powerful oracle and asked:
What kind of monsters will I face?
I see I can see clearly these are the most powerful beings on earth, Rust, ECS, and WebAssembly."
Rust is a relatively new language. Its first official version was released in 2010. It is a multi-paradigm and general-purpose language. The fascinating fact about Rust is that it is not reinventing the wheel. On the contrary, it borrows many concepts from different languages, e.g.,
The main features of the language:
Rust is not a simple language. A lot of time is needed to learn to use Rust's sword. We will not find a "garbage collector" here (I will use the version of this word later in the below :)). There is a concept of a 'static garbage collector' (for curious ones, see here), and in a way, Rust is equipped with such a mechanism as well. It is nothing but a compiler that cares and helps the programmer understand common mistakes (dangling pointer, anyone?).
When we first use Rust, the compiler is our enemy. I advise you to make friends with it as soon as possible because this tool is an invaluable guide to the world of good programming practice.
While working as a programmer, I repeatedly read code in a language I did not know.
Learning Rust was already difficult. The ECS turned out to be a monster I had never heard of before. I learned that the ECS is an architectural pattern that relies on modeling through composition. I also learned that inheritance is a big problem in the game dev world. Here my mental model slowly began to clear up. As a web developer, I have often dealt with inheritance and know how challenging it can be. Especially, C ++ allows inheritance from many base classes, leading to a "diamond problem."
To better understand the concept of ECS, I reached into my toolbox, and I used the visualization technique (I used Miro and plain colored cards). I find it an excellent tool for understanding new concepts because images speak much more to us than the text itself. The result of my work can be seen below:
As you can see, a component is nothing but a programmer-defined data type. Rust has no classes. Instead, we have structures available. A good source is here if you haven't figured out what a macro "derive" is.
Here again, macros come in handy. From lines 4 to 7, the components to which the function "entity_render" is to have access are defined. It is worth noting that as the function's first argument, we get an object of type "SubWorld." The name indicates that it is a part of the world (entity database) that is limited to types defined by macros. In line 9, the function "query" is called, which will return an entity list containing the components "Point" and "Render," e.g., the entity "Player" is composed of the components "Point" and "Render," so the entity "Player" will be returned from the inquiry. An analogy is pseudo SQL code:
1. Select * front Entities where entity.type includes Point
and
Render
With the basic concepts of "ECS" explained, the game development algorithm looks like this:
WebAssembly is a new technology for me that I have not dealt with so far. So, I decided to try to understand the very concept of compiling and running the project in Rust. However, I left the understanding of WebAssembly in the browser for later.
We start work on WebAssembly with the finished game from chapter 3, which looks like this:
The Rust ecosystem is constantly evolving. One of the things that immediately caught the attention of the Rust community was the ability to compile code into WebAssembly. It is because Rust does not have a virtual machine (which makes it easier to support a new architecture) or its runtime environment (NodeJS).
The environment itself is not an obstacle. C # can also run in WebAssembly, but the resulting code must contain net environment code. Rust is a language compiled into machine code. Generally, WebAssembly is a kind of machine code built into browsers (more here). The libraries you will be using must also support building to WebAssembly. In other words, a library with dependencies related to a specific architecture or system (e.g., one that works only on Linux) will not compile to WebAssembly without prior preparation.
So the first thing you need to do is install the appropriate compilation support:
1. rustup target add wasm32-unknown-unknown
Next, you install:
1. cargo install wasm-bindgen-cli
It is a communication tool between JavaScript and Rust. It allows you to call JavaScript functions from Rust and vice versa (more here).
Of course, the WebAssembly code runs in the browser, so we need the HTML file. We build the application with the command:
1. cargo build --target wasm32-unknown-unknown –release
And next, you tight the object code:
1. wasm-bindgen target/wasm32-unknown-unknown/release/dungeoncrawl.wasm --out-dir ./docs --no-modules --no-typescript
The "docs" folder prepared in this way can be put on a static file server. I used Github pages (https://rengare.github.io/dungeoncrawler_rs/ ).
We made it to the end, and the brave knight saved the princess. Our hero defeated Rust, ECS, and WebAssembly. However, this is not the end, as the adventure is still ongoing, and dragons and monsters are alive and well. The brave hero dedicated his treasure to upgrading his equipment. Armed with "Bevy and Takio" he is ready for new adventures.
WebAssembly allows you to execute code written in languages such as C ++, Rust, or C #. Thanks to the provided JavaScript API, communication between JS and WebAssembly is possible. One of many applications’ features is to port native games and run them in the browser.
The adventure of writing the game was not the easiest. Many questions popped up in my head, e.g.,:
Fortunately, the book mentioned above helped to walk me through the process. However, it wasn't always easy, and I had to put in a lot of work to understand the new concepts. For this purpose, I have often used visualization. With my article, I would like to encourage you, the reader, to test the Rust language of the WebAssembly technology and explore unknown topics 😉.