Making an ECS WebAssembly Game with Rust and Bevy

Why Rust for games specifically?

To follow-up on my previous write-up wherein I describe the rationale for learning Rust, I decided to tackle the learning experience through writing a game.

Given that Rust is a fast, memory-safe, low level systems programming language that has all the batteries included, logically, the best use-case for such a language is in making games and/or game engines. There is even a website tracking progress of game engine development within the Rust ecosystem.

Gaming throughout the past 30 years of computing history has, after all, lived at the bleeding edge of performance. Modern games require real-time rendering, audio, networking, AI, and need to do all of this at 60 fps (72 fps if we’re talking about VR!). For example, the GPUs that power modern innovations like machine learning and cryptocurrency mining were originally developed for accelerating rendering for video games.

Additionally, as a hobby game developer myself with experience using the Unity game engine, I was curious to see whether the very large and growing open-source Rust community could one day produce a game engine to rival Unreal Engine, Unity, or even Godot.

Not every game engine developer might share this sentiment however. Here’s a tweet from Epic Games CEO and master programmer Tim Sweeney.

Nevertheless, for educational purposes, I’ve found that making a game is one of the best ways to learn a new language so onward to…

Selecting a Rust Game Engine

The first thing to do is to find a game engine. I’m not interested in making a game engine myself, and the Rust ecosystem has a surprising number of open-source cross-platform options. I’ve listed the leading options below:

Engines

Given that I am quite familiar with 3D modeling, and the fact that in many ways the performance of Rust is probably wasted on making 2D games, I ruled out any options that only deal with 2D. ggez is only 2d while macroquad and miniquad don’t have many 3d examples in place. Additionally, the Godot engine is very well developed but there are few resources or examples using Rust bindings, making it a bad choice for educational purposes.

This effectively leaves Bevy and Fyrox. Fyrox is an incredibly interesting engine insofar it looks like a Unity clone written in the Rust programming language and actually has an editor. Despite it’s level of feature completeness, I was quite astounded to learn that it was created mostly out of the herculean efforts of (mostly) a single developer.

On the other hand Bevy does not yet have an editor and is built off an ECS architecture. Furthermore the github has over 19,000 stars with over 535 contributors making it far more likely for there to be answers in the event of any snags. Game created in bevy (as with some of these other options) can also be compiled to WebAssembly quite easily.

Blender Models can easily be exported to Bevy

Blender Models can easily be exported to Bevy


Bevy, ECS, and Data Oriented Design

To understand why Bevy is unique compared to existing game engines, it’s first necessary to understand what an ECS architecture is and the type of problem it attempts to solve. ECS or “Entity-Component-System” is a game engine architecture for decoupling logic from data. To best explain this might require a bit of a history lesson.

In the traditional object-oriented (OOP) way of doing things, you might start by treating every actor in the game with a base class, and then use inheritance to make derivative actors. At the root of your inheritance tree, you might have a “MoverActor”

By the early 2000s, many developers realized the folly of using inheritance in such a way as classes at the bottom of complicated tree hierarchies take on more and more bloat.

To get a sense of how this can go wrong lets take a look at one branch of the inheritence tree that ended up being devised for the 1998 game Starcraft:

CUnit < CDoodad < CFlingy < CThingy

The classes on the inheritence tree either become super bloated, or have shim classes inserted between nodes as new features are added.

At the same time certain things become impossible due to diamond inheritance. Do you want a projectile that has also has an grenade? Too bad because Projectile and Grenade are on different branches of the inheritance tree that inherits from GameObject!

An RPG is both a Grenade and Projectile

An RPG is both a Grenade and Projectile

From inheritance to composition

One alternative to inheritance is to use composition instead. This “Entity-Component” strategy is what the Unity game engine does. Each Actor or GameObject in the game is simply a container class for atomic behaviors. These Monobehaviors typically only have one resposibility (such as physics, collider, player input) and contain both mutable data (ie: hp, lifetime, mana) as well as logic and update methods. In the Unity game engine, such MonoBehaviors are classes which can inherit from other monobehaviors.

Unity Monobehaviors in a GameObject container

Unity Monobehaviors in a GameObject container

From composition to data-oriented-design with ECS

As we move into 2022 where processors increasing consist of more and more cores, the “Entity-Component” approach begins to reach its limitations for two reasons - multi-threading and memory locality. If you recall, the components in an “Entity-Component” architecture are both data containers and logic containers. Therefore on every update tick of a game, the engine loops through all entities in the virtual world and runs the update tick logic for every component for every entity.

The logic within each component (Monobehaviour) is coupled with its parent entity (GameObject) such that memory is jumping back and forth to different addresses which can be very far apart and therefore not amenable to modern processor memory caching. Furthermore, running logic on components on multiple cpu cores can become very complicated particularly if data in a component being processed on one thread needs to be accessed by data being processed on another thread.

One solution is to expand the engine architure to a third part, into an “Entity-Component-System” or ECS. In ECS, the “Entity” is no longer a container, but rather just a number (like an index value in an excel spreadsheet), the “Component” now loses its logic bits and now only consists of data, and finally the “System” consists of the actual business logic of the game. In an ECS, there are no more “Entity” containers. Rather data and processing are completely decoupled:

Separating data from logic to enable parallelization

Separating data from logic to enable parallelization

“Entities” are like the row_id of a database, “components” are the columns, and “systems” run queries on the database to enact logic. Importantly, in some ECS designs, the memory for components with the same groupings are placed right next to each other for rapid cache access, and the boundaries between components are clear enough to enable system multithreading. Thus in a well designed ECS architecture, all processing for mutually exclusive system queries can be run in parallel on multiple cores without the programmer having to know about the details.

Naturally, streaming data such as terrain and entities for very large digital worlds can become much more straightforward as well.

Interestingly the most famous major AAA game I know of that is built off an ECS architecture happens to be OverWatch according to this GDC talk. Additionally Unity has spent years and millions of dollars attemping to overhaul their engine to use an ECS backend known as DOTs.

ECS with Bevy

This brings us to the Bevy game engine. Bevy is an open source ECS game engine written in Rust with a very ergnomic ECS syntax. In Bevy, “Systems” are simply regular ol’ Rust functions that run Query on data. For example here is the “System” for moving the ball in Pong:

fn apply_velocity( mut query: Query<(&mut Transform, &Velocity),With<Velocity>>){
    for (mut transform, velocity) in &mut query {
        transform.translation.x += velocity.x _ TIME_STEP;
        transform.translation.y += velocity.y _ TIME_STEP; 
    } 
}

Likewise “components” are just regular ol’ Rust structs. Here’s the Weapon component in my experimental game StarRust:

pub struct Weapon {
    pub bullet_type: BulletType,
    pub offset: Vec2, pub firing_audio_clip: Handle<AudioSource>,
    pub cooldown_timer:Timer, 
} 

And Entities, which are merely integer ids, are things you don’t really think about at all except when deleting them.

Starting with a basic ECS version of Pong (Click on image for source code)

Starting with a basic ECS version of Pong (Click on image for source code)

Are We Really Game Yet?

Right now the answer (at least for Rust+Bevy) is no.

For Bevy, there is almost certainly a very long road ahead before reaching parity with something like Godot, and certainly quite a long time before reaching parity with something like Unity.

Over the 2 week course of developing my prototype ECS game StarRust, I ran into quite a few difficulties with the ecosystem such as:

  • Breaking API changes (every 3 months a new semantic version rolls out)
  • Poor rendering performance (no material batching implemented for 3d yet)
  • Hardware specific webassembly issues (StarRust does not render correctly on older intel integrated gpus)
  • Webassembly is still WIP (currently no threading, weak performance)
  • Very few examples of actual 3d games to reference

The community is very strong however, and I strongly suspect underlying foundations on which the engine is built will make it one day surpass Unity in both flexibility and functionality.

(Bonus) Embedding a game into this HUGO blog

One of the nice surprises I learned over the course of this project is that Bevy games compiled down to Webassembly can be continuously deployed using Github actions to Github Pages and embedded directly into this Hugo Blog using an iframe shortcode as follows:

 {{ if .Get "src" }}

<div style="display: block; position: relative; padding-right: 1024px; padding-left: 0px; padding-bottom: 100%;overflow: hidden;">
    <iframe
        sandbox="allow-same-origin allow-scripts"
        width = 1024
        height = 800
        src="{{ .Get `src` }}"
        style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0; scale: 0.75"
    ></iframe>
</div>
{{ end }}

StarRust

As promised, here is the game StarRust, written using an ECS paradigm in Rust and compiled to Webassembly:

Disclaimer: There may be rendering issues with older intel integrated graphics and performance hiccups

Arrow keys to move Space to fire

SOURCE CODE HERE

Related Posts

Why Big Tech Wants to Make AI Cost Nothing

Earlier this week, Meta both open sourced and released the model weights for Llama 3.1, an extraordinarily powerful large language model (LLM) which is competitive with the best of what Open AI’s ChatGPT and Anthropic’s Claude can offer.

Read more

Host Your Own CoPilot

GitHub Co-pilot is a fantastic tool. However, it along with some of its other enterprise-grade alternatives such as SourceGraph Cody and Amazon Code Whisperer has a number of rather annoying downsides.

Read more

All the Activation Functions

Recently, I embarked on an informal literature review of the advances in Deep Learning over the past 5 years, and one thing that struck me was the proliferation of activation functions over the past decade.

Read more