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.
Rust is C++ minus powerful template abstraction features plus stronger safety guarantees through shifting proofs of liveness to the programmer. Kind of one step backward and one step forward, not any closer to ideal.
— Tim Sweeney (@TimSweeneyEpic) January 5, 2020
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.
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
!
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.
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:
“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.
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 moreHost 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 moreAll 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