snen.dev blog

Building With Bevy

Part 0 - Introduction

For a year or two now, I have been following Bevy, toying with it in different ways to practice Rust and, simultaneously, game development. I first heard about it some years ago after some brief experiments with the Amethyst game engine (which is now deprecated). I was drawn to the concept of an ECS game engine and to using Rust, so it was an easy choice to use Bevy when I finally felt I had the time (and the sufficient experience with Bevy) to try a more complicated project.

This series of posts will describe my experiences and try to show some advantages to the approaches I have found. It includes a number of (what I consider) rather ambitious goals, primarily focused around some nebulous concepts of ergonomics, maintainability, and capability. Sort of a "how easy can I make X" litmus test.

At the end of this series, I aim to show a codebase with:

  • a convenient development environment and experience
  • local and online multiplayer
  • cross-platform development and deployment with cross-play
  • portable, testable code
  • strong layers of abstraction
  • a minimal amount of spaghetti

There are plenty of places where it can be improved, and I won't claim that the code is anywhere near perfect. However, I believe it should showcase, at a minimum, how strong the APIs in the Bevy ecosystem are, and what the tools in its ecosystem can do. In particular, I will focus on its ability to create strong abstractions which maximize code reusability even when enabling highly complex behaviors such as networking and cross-platform development.

If you want to read more about my decisions and some musing about these ideas, continue reading below. If you want to get right into the action, feel free to jump into the next post.

Prerequisites

Readers will need some fundamental know-how about Rust and (maybe) Bevy. I will go over the Bevy concepts whenever they come up, and I try to link to external resources that can provide additional information. There's a lot of content to get through here, though, so I may miss some things.

Regarding the developer environment, I didn't run into any trouble building this on Windows and Linux, and have not tested it on Mac. Install Rust if you don't have it.

Finally, this approach can take up a significant amount of disk space. A fresh cargo build of the workspace by the end of post 4 results in ~5GB of disk space on Linux, which is not bad. However, on Windows I have seen my target/ directory grow beyond ~80GB when I was not paying attention. This is quite a lot of space for such a small game, but there are plenty of options for mitigating this in practice. Still, for those that choose to follow along in-code, it's worth keeping an eye on this.

Why ECS & Why Rust

In the past, I have used other engines such as Unity and Godot for a number of small toy-projects, but I have never quite been satisfied with them. Even with such a small scope, though, I often felt like I couldn't control for the growing scope of the code. Of course it's never so easy as that: making a game is hard, and it involves a lot of hard subproblems, and those subproblems often have deeply entangled relationships.

After my first experiences with Bevy and Amethyst, I was convinced that ECS-based approaches provided a developer experience that was much closer to what I wanted to work with than what I previously had worked with. For me, Rust's strong type system and flexible build system makes development very smooth. I can write code that maintains the guarantees and guardrails I want to have, preventing bugs and unexpected behavior. I also feel much less limited in terms of what I can do, since I have the whole Rust ecosystem to draw from. Most importantly, however, the development experience feels really powerful when the right strategies are in place.

For readers interested in other systems languages, I'm sure many of the ideas here carry over to other languages and other choices of ECS framework. However, I have found working with Rust to be a perfect fit for game development. Macros are used heavily in this repository to make boilerplate like serialization code a one-line additions. This is one example, but the theme overall is that these practices make it really easy to define layers of abstraction that keep file sizes relatively small and focused. Furthermore, the build system and package management with cargo makes cross platform a breeze.

Project Goals

The project I chose was a Frogger clone. A Frogger clone with as many batteries and flexibilities as I could try to include. I haven't finished everything yet, but in general, I'm happy with what I've been able to do so far with these goals in mind.

Since the release of Bevy 0.10, I have been able to get my code to the point where I feel it would be useful to share some of the lessons I have learned. I want to go through each topic in an appropriate level of depth, so it will be broken into a series of posts:

  1. Getting Started with a Strong Development Environment
  2. Implementing a Small Frogger Clone
  3. Integrating Online Networking (feat. NAIA)
  4. Maximizing Ergonomics: Generic Plugins and Small Crates

Each of these posts will be accompanied by a commit in an accompanying git repository "workbook" with all the changes discussed. I hope to expand it in the future with topics like:

  • Cross-platform deployment and automated CI
  • Managing application state (routing between screens, managing assets, etc.)
  • Error handling and designing error states
  • Integrating AI controllers
  • Adding "flavor" (like audio, particle effects, animated graphics, etc.)
  • Evaluating, benchmarking, and profiling performance
  • More complex game systems and interactions

Finally, for anyone that would try to follow this in code, beware, there's a lot of experimental stuff being used. Some APIs are likely to change and "your mileage may vary".

My Approach

My intent with this project was to find a project barely more complex than a tutorial. Usually this means I run into some problems -- often times I have to rewrite it once or twice -- but I always learn many things. And I often learn them twice.

This approach forces me into situations where I feel the repercussions of whatever mistakes I have made. Creating a bad abstraction, misunderstanding something about my code, or whatever else. Since the amount of business logic is still relatively small, though, complex issues are often somewhat isolated and are more easily debugged.

But game code is known to become unwieldy fast. How far do these tools actually go? What does it actually look like to stuff a ton of various categories of features into a Bevy codebase?

For me, the results have felt very convincing!

Conclusion

In the rest of the series, I will try to show how each specific implementation layer contributes to the overall goals, and why I like working with it.

The topics are not ordered in the order that I undertook them. Instead, I tried to make the flow of development make a little more sense than what I did, which was much more roundabout and confusing. I am trying to cover a lot of ground which does not help with that. But hopefully I can provide reasonable insight into what choices were made and why they are powerful.

Finally, I'd like to thank all the open source developers who have contributed to the crates and projects listed in this series. Keep in mind that many of these are still in early development: APIs may change and, furthermore, these contributors are sacrificing a lot to provide these tools. If this post inspires you to learn more about any of the projects mentioned here, please be kind and respectful of their time and effort.

Thank you for reading.

Continue reading