Bevy is a Rust game engine that runs Entities, Components, and Systems in a update loop using functional paradigms. This system is commonly referred to as Bevy ECS
A month has passed since the beginning of this year, and a small “New Year’s resolution” of mine is to properly learn game programming. This turned out to be through the Bevy engine, giving a unique experience programming a graphical application in Rust, a language I am learning. This led to creating a 2D demo “game” where the player avoids a green enemy that constantly pursues them with a fixed velocity always pointing to the player.
The first setback was setting up a development environment. As with everything in NixOS, development dependencies are not installed, or made available to the user session by default, so a Nix shell is needed to provide the relevant packages and libraries.
This is a sample shell.nix
file to set up a Bevy environment. Here, note the buildInputs
property that sets up all required libraries, where either X11 or Wayland dependencies are needed, as determined by your desktop environment.
{ pkgs ? import <nixpkgs> { } }:
with pkgs;
mkShell rec {
nativeBuildInputs = [
pkg-config
cargo
rustc
rustfmt
clippy
];
buildInputs = [
udev alsa-lib vulkan-loader
xorg.libX11 xorg.libXcursor xorg.libXi xorg.libXrandr # To use the x11 feature
libxkbcommon wayland # To use the wayland feature
];
LD_LIBRARY_PATH = lib.makeLibraryPath buildInputs;
RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}";
}
Here, a shell environment is generated where a stable Rust tool-chain is exposed to the user, and the path to build inputs are exposed to LD_LIBRARY_PATH
, ensuring that dynamically loading the resulting binary succeeds. This does mean that, when exposed to another NixOS environment, it is very likely that the binary cannot be run. It is possible to solve this by using the musl
C compiler, which statically compiles libraries into the binary, sacrificing file size for compatibility.
Quite conveniently, the nix-direnv
package, installed by home.packages = [ pkgs.nix-direnv ]
, allows auto-loading approved directory .envrc
files. But, to let direnv
know that the current directory uses a nix shell, use nix
must be added as a directive to let it know of the shell.nix
configuration.
# File: /PROJECT_ROOT/.envrc
use nix
In summary, the declarative nature of NixOS allows an environment to be defined in a reliable and stable manner, with the trade-off being the rapid iteration velocity achievable in a highly mutable conventional system environment.
Bevy systems are just ordinary functions that take in inputs and produce side-effects in the game world. Bevy uses a special struct Res<T>
to get an available resource with the type T
. For example, to get the current time counter:
fn process_time(time: Res<Time>) {
...
}
Procuring entities, such as the player and enemy, is a bit more complicated. Here, a reference can be taken of the entity that you want, mutable or immutable, in a set as the first type parameter of the Query<T,F>
type, where T
is the entity reference, and F
are some filter options that are available.
Below you can find an example of getting a Player
entity reference from the world, and getting its properties:
use bevy::prelude::*;
#[derive(Debug)]
fn main() {
App::new()
.add_systems(Startup , setup_world)
.add_systems(Update, get_player)
...
}
pub struct Player {
name: String,
...
}
fn setup_world(mut commands: Commands) {
let s = Sprite::from_color(Color::srgb(1., 1., 1.), Vec2 { x: 10., y: 50. });
commands.spawn(Camera2d);
commands.spawn((
Player {
name: "Player Joe".to_string(),
},
s,
Transform::from_xyz(0., 0., 0.),
));
...
}
fn get_player(players: Query<(&Player, &Sprite, &Transform)>) {
for (player, sprite, transform) in &players {
println!("Discovered player with name: {}", player.name);
}
}
Made with ❤ and at Delft, the Netherlands.