Maciej Główka
Blog Games Contact

← Return to Blog Index

Dec. 6, 2022

Text-based (JSON, Toml) resources in Bevy engine

rust bevy gamedev

Even when building a simple game, you might at some point want to keep the data / stats outside of your source code. A structured text file format, like JSON or Toml might sound like a good, not overly complicated, solution. Unfortunately, the Bevy engine does not support loading those out-of-the-box at the moment (I am at v 0.9).

You might just think, that loading text data is easy enough to handle through Rust's standard library - but that won't work in some contexts like WASM. Let's try to overcome this by creating a custom asset loader :)

As a an example I will use a hypothetical bird game, where we store the data in a Toml file, with a following structure:

mass = 0.05
speed = 200

mass = 4.0
speed = 160

mass = 3.0
speed = 50

Although Toml format is my preference here, it should be fairly easy to adapt the code to utilize JSON files (with a help of the serde_json crate).

I won't go into the details of starting a Bevy game project from scratch, however make sure that you have these in your project dependencies:

bevy = "0.9"
serde = { version = "1.0", features = ["derive"]}
toml = "0.5"

First things first - we need to create a structure to hold our loaded assets. Each asset type in Bevy needs to be identified by an UUID, which is conveniently achieved by deriving TypeUuid. As we are going to load the entire file content at once - we only need a single String field inside our struct.

Now we can create the loader - an empty struct implementing the AssetLoader trait (imported from Bevy). We just have to take care of creating two rather simple funcs:

From the signature of the load function, we can learn that we already receive all the necessary data, loaded from our drive, in a form of a &[u8] array. All we have to do is to parse it (in our case, convert to String), push it into Bevy's LoadContext and finally return an Ok result.
Nothing too fancy here.

Now to test our loader we have to register it with our Bevy game through add_asset and init_asset_loader inside of the app builder. We also need a simple startup system to actually invoke the data loading process.

(as I usually work in a WSL2 setup, it is handy for me to run Bevy project through a WASM server, but obviously you can omit the WASM specific parts of the code)

To see if everything went fine, we are going to use an info! macro to write messages to the console (browser's in my case)

If everything goes fine, we should see something in the shape of StrongHandle<TomlAsset>(AssetPathId(AssetPathId(SourcePathId(16694095303235740075), LabelId(10908985245426111277)))) in our console output.

Now, if you have been already fiddling a bit with Bevy's assets, you probably do know that they are loaded in an async fashion - which means we won't be able to use our string data immediately.

To handle this, I will create a second system, which will run after the startup stages. In this system we are going to use the data only after a space-bar key press (just to add some extra delay and make sure the assets are indeed loaded).

Running the above should produce TomlAsset("[swift]\nmass = 0.05\nspeed = 200\n\n[golden_eagle]\nmass = 4.0\nspeed = 160\n\n[heron]\nmass = 3.0\nspeed = 50") - finally our actual data!

As a last part of the process, we are going to parse the string into something more usable. In the end, I would like to have a hashmap containing bird data entities, where the keys are birds' names. So let's create a simple struct for the bird data and modify our load_data system.

Now if we run our example once again, we should see in the console output a nicely formatted piece of bird data, like so: {"heron": Bird { mass: 3.0, speed: 50 }, "golden_eagle": Bird { mass: 4.0, speed: 160 }, "swift": Bird { mass: 0.05, speed: 200 }}

And the complete working code:

← Making Axum server to listen over a Unix socket Bevy roguelike tutorial / devlog part 1→