iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🧱

Creative Coding with Rust Bevy

に公開

Creative coding is programming for the purpose of creating some form of expression. It differs from ordinary programming, which aims to implement specific functionality. Enthusiasts use it to create works such as visual art and sound art.

In a previous edition of Yumemi Daigirin, I wrote about creative coding using the Nannou[1] framework.

  • Creative Coding with Rust Nannou (Published in "Yumemi Daigirin '23 (2)")
  • Rust Nannou Drawing Examples (Published in "Yumemi Daigirin '24")

This time, I will introduce a method using Bevy[2] as the framework. Bevy is a framework (game engine) for game creation.

Creative Coding Methods

When doing creative coding, people often use frameworks that assist in production.

There are frameworks specifically for creative coding. Famous examples include Processing, p5.js, and openFrameworks. On the other hand, game engines are sometimes used instead of dedicated frameworks. Unity and Unreal Engine are frequently used.

Dedicated frameworks are specialized for creative coding, so the barrier to entry is low. There is a wealth of information and abundant samples. Game engines are general-purpose frameworks for creating various games, so even if you only want to do creative coding, the required knowledge increases, making the starting hurdle slightly higher. However, because of that versatility, there is much more you can achieve, enabling a wide variety of expressions.

Bevy and Nannou

If you love programming, you probably want to use your favorite language. Here, we will consider creative coding using the Rust language.

As introduced at the beginning, Nannou is a dedicated framework for creative coding that can be used with Rust. It has a low barrier to entry, allowing you to create visual art immediately.

On the other hand, Bevy is one of the game engines available for Rust. It is developed as open source and can be used freely and without charge. Although it is a relatively new game engine, development is active and the community is vibrant with information exchange. Its popularity is likely due to features such as being simple and fast, modular-oriented, and highly extensible. It is also well-suited for creative coding.

In fact, there is an ongoing effort to redesign and redevelop Nannou as a Bevy plugin. It aims to integrate Nannou's ease of use with Bevy's high extensibility. However, this is still under development and has not been officially released. It is something to look forward to in the future.

At this point, if you want to do creative coding in Rust, you essentially have two options:

  • Use Nannou (the traditional version, not the Bevy plugin)
  • Use Bevy

In this article, we will look at how to use Bevy for creative coding.

Bevy Setup

First, let's set up Bevy.

I will assume that your Rust development environment is already set up. Please refer to the official Rust website[3] for instructions on how to set up the development environment.

Create a new project using the cargo command.

cargo new my-bevy-project

Move to the created project directory and add Bevy to the project. Using the cargo add command is the easiest way.

cargo add bevy@0.16

This adds bevy to the dependencies in Cargo.toml. Note that you can also edit Cargo.toml manually instead of using the cargo add command.

In this article, we will use Bevy 0.16, which is the latest version at the time of writing. As for the current status of Bevy, major updates such as API changes often occur during version upgrades. Please be mindful of version differences.

Immediately after creating the project, src/main.rs looks like this:

fn main() {
    println!("Hello, world!");
}

Edit it as follows:

use bevy::prelude::*;

fn main() {
    App::new().add_systems(Update, hello).run();
}

fn hello() {
    println!("Hello, Bevy!");
}

The println! that was inside the main function has been moved to the hello function. I have also changed the message slightly. I will explain the contents of the main function later, but this process calls the hello function.

Running the program with cargo run will output Hello, Bevy! to the console.

ECS Pattern

Bevy apps adopt the ECS (Entity Component System) pattern. The ECS pattern is a data-oriented design pattern widely used in game development and simulations. It divides a program into three elements: entities, components, and systems.

An entity is a unique identifier representing a "thing" in the game world. For example, in a breakout game, the ball, the paddle, the walls, and each block can be represented as entities. An entity itself has no functionality; it gains meaning by having components attached to it.

A component is an attribute or state attached to an entity. For example, by attaching a Brick component representing the attribute of a destructible block to an entity, that entity takes on the meaning of a block. Similarly, by attaching a Paddle component representing the attribute of a paddle to an entity, that entity becomes a paddle. Furthermore, by additionally attaching a Collider component for collision detection to the block or paddle entities, they signify objects that perform collision detection.

A system is the logic executed on components. For example, moving the paddle or performing collision detection would be systems. Systems are executed according to the game's elapsed time or events to control the game's behavior.

In traditional object-oriented thinking, data and behavior were grouped together into classes. In ECS, data (components) and behavior (systems) are separated. Furthermore, it emphasizes composition over class inheritance, resulting in a flexible and highly extensible design.

Implementation of the ECS Pattern in Bevy

Systems, which are one of the elements of the ECS pattern, are implemented as regular functions in Bevy. The hello function in the first example ("Hello, Bevy!" output program) is also one of the systems.

fn hello() {
    println!("Hello, Bevy!");
}

To execute a system, you register the system's function with the app. I postponed the explanation of the main function in the first example, but I will explain it here.

fn main() {
    App::new().add_systems(Update, hello).run();
}

add_systems registers the hello function as a system in the app. Since the Update schedule is specified, it is called once per rendering frame (however, since this app does not have a window yet, it is called once and then finishes).

As for the main function as a whole, it performs the process of creating the App (new()), registering the systems, and then starting execution (run()). With this, the main function from the first example has been explained.

Now, in the first example, only systems appeared, while entities and components did not. The following example includes them.

use bevy::prelude::*;

fn main() {
    App::new()
        .add_systems(Startup, setup)
        .add_systems(Update, hello)
        .run();
}

#[derive(Component)]
struct Person {
    name: String,
}

fn setup(mut commands: Commands) {
    commands.spawn(Person { name: "Alice".to_string() });
    commands.spawn(Person { name: "Bob".to_string() });
}

fn hello(query: Query<&Person>) {
    for person in &query {
        println!("Hello, {}!", person.name);
    }
}

Running this will output the following to the console:

Hello, Alice!
Hello, Bob!

In this example, two systems are registered using add_systems. The setup system specifies the Startup schedule, so it is called when the app starts. The hello system specifies the same Update schedule as before.

Components, which are one of the elements of the ECS pattern, are structs that implement the Component trait in Bevy.

#[derive(Component)]
struct Person {
    ...
}

Regarding entities, an Entity struct is defined within Bevy. The Entity struct has only one integer field.

struct Entity(u64);

As a side note on the code, this is Rust syntax for declaring a struct without field names (a tuple struct).

Instances of this Entity struct are the entities in the ECS pattern. commands.spawn in the setup system creates an entity. Here, it is creating an entity with a Person component attached to it.

commands.spawn(Person { ... });

A Bevy app has a World container that stores and holds objects from the game world. Created entities are automatically stored in the World.

I previously explained that a system is the logic executed on components. To actually handle components, you make the system function accept a Query as a parameter.

The following hello system handles the Person component. It receives components of type Person from the entities held in the World container.

fn hello(query: Query<&Person>) {
    ...
}

With this, you have a general understanding of the flow of using the ECS pattern in Bevy.

Bevy Plugins

Bevy has a plugin system, so let's touch upon that.

The previous ECS example can be rewritten using a plugin as follows. First, create a hello_plugin.rs file separate from the main.rs file. Then, move the implementation of the components and systems that were written directly in main.rs over there.

use bevy::prelude::*;

pub struct HelloPlugin;

impl Plugin for HelloPlugin {
    fn build(&self, app: &mut App) {
        app.add_systems(Startup, setup);
        app.add_systems(Update, hello);
    }
}

#[derive(Component)]
struct Person {
    name: String,
}

fn setup(mut commands: Commands) {
    commands.spawn(Person { name: "Alice".to_string() });
    commands.spawn(Person { name: "Bob".to_string() });
}

fn hello(query: Query<&Person>) {
    for person in &query {
        println!("Hello, {}!", person.name);
    }
}

To implement a plugin, simply implement the Plugin trait. Then, modify main.rs as follows:

use bevy::prelude::*;
mod hello_plugin;
use hello_plugin::HelloPlugin;

fn main() {
    App::new().add_plugins(HelloPlugin).run();
}

The plugin is added to the app using the add_plugins function.

In this way, Bevy allows you to easily modularize various functions. Bevy apps are then implemented by leveraging these plugin modules.

Drawing Shapes

Now that we have a basic grasp of Bevy, let's display a window and draw some shapes.

Features commonly used when creating games are provided as DefaultPlugins. Simply by adding this plugin, a window will be displayed when you run the app (though it will be a completely black, empty window).

use bevy::prelude::*;

fn main() {
    App::new().add_plugins(DefaultPlugins).run();
}

Next, let's draw a shape in the window. The following code draws a single circle in the center.

use bevy::color::palettes::css::*;
use bevy::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_systems(Startup, setup)
        .run();
}

fn setup(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<ColorMaterial>>,
) {
    commands.spawn(Camera2d);

    let mesh = Mesh2d(meshes.add(Circle::new(100.)));
    let material = MeshMaterial2d(materials.add(Color::from(SKY_BLUE)));
    let transform = Transform::from_xyz(0., 0., 0.);
    commands.spawn((mesh, material, transform));
}

Running it with cargo run opens a window where a circle is drawn.

The drawing process is performed within the setup system. Let's examine a few new elements that have appeared.

To display objects existing in the game world in a window, a "camera" is required. Therefore, we create a camera entity. Camera2d is a 2D display camera that looks down on the game world from directly above (the positive Z-axis direction).

commands.spawn(Camera2d);

We place the object to be drawn (a circle in this case) into the game world. The following entity is assigned a triplet of components: Mesh2d, MeshMaterial2d, and Transform.

commands.spawn((mesh, material, transform));

Mesh2d defines the shape of a 2D object. Here, it is set to a Circle with a radius of 100.

let mesh = Mesh2d(meshes.add(Circle::new(100.)));

MeshMaterial2d defines the decoration of a 2D object. Here, we are providing a color.

let material = MeshMaterial2d(materials.add(Color::from(SKY_BLUE)));

Transform defines the position of the object. Here, it is placed at the origin of the XYZ coordinate axes.

let transform = Transform::from_xyz(0., 0., 0.);

By creating a camera and the object to be drawn in this way, what is seen through the camera is rendered in the window.

Shape Animation

Next, let's also try animating the shapes.

use bevy::color::palettes::css::*;
use bevy::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_systems(Startup, setup)
        .add_systems(Update, swing)
        .run();
}

#[derive(Component)]
struct SwingAnimation {
    speed: f32,
    amplitude: f32,
}

fn setup(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<ColorMaterial>>,
) {
    commands.spawn(Camera2d);

    commands.spawn((
        Mesh2d(meshes.add(Circle::new(100.))),
        MeshMaterial2d(materials.add(Color::from(SKY_BLUE))),
        Transform::from_xyz(0., 0., 0.),
        SwingAnimation { speed: 2., amplitude: 400. },
    ));
}

fn swing(
    time: Res<Time>,
    mut query: Query<(&SwingAnimation, &mut Transform)>,
) {
    for (animation, mut transform) in &mut query {
        let position = (time.elapsed_secs() * animation.speed).sin();
        transform.translation.x = animation.amplitude * position;
    }
}

Running it with cargo run opens a window where a circle is drawn, just like the previous example. Then, that circle swings back and forth in an animation.

The setup system creates the circle entity, and the swing system moves it.

To animate the circle, we use the Update schedule for the system. This is the schedule called once per rendering frame, as mentioned earlier. Since this app has a window, it is called repeatedly. By changing the circle's position at each timing, the position of the circle drawn through the camera changes as well. This creates an animation when repeated quickly.

To change the circle's position, we modify the values held by the Transform component. If you use the mut keyword when the system function receives the component via Query, you can modify the values within the component. In other words, you can change the coordinates and move the position.

fn swing(mut query: Query<&mut Transform>) {
    for mut transform in &mut query {
        transform.translation.x = ...;
    }
}

We create a new SwingAnimation component to pass animation parameters.

#[derive(Component)]
struct SwingAnimation {
    ...
}

Then, we additionally attach the SwingAnimation component to the entity we want to animate.

commands.spawn((
    ...
    SwingAnimation { ... },
));

The system uses a Query for both the SwingAnimation and Transform components. This allows it to receive the entity to be animated and change its position.

fn swing(
    time: Res<Time>,
    mut query: Query<(&SwingAnimation, &mut Transform)>,
)

Note that Res refers to a resource. Resources are objects that exist globally in the game world and are held in the World container. In this case, we use the Time resource to get the elapsed time since the game world started.

As a side note, the ResMut for meshes and materials used in the shape drawing also referred to resources. By registering the shapes and decorations to be used as resources, efficient management and reuse are achieved.

In this way, we have achieved shape animation by leveraging Bevy's ECS pattern framework.

Further Information

In this article, I covered the basics of Bevy and described how to draw and animate shapes. Once you grasp this much, you should be able to read various documents and samples on your own. Therefore, I will introduce some information sources for what follows.

As an information source, you should first check the official Bevy page. You can find many documents and samples there.

https://bevyengine.org/

In fact, many examples are introduced in the Examples section. For shape drawing and animation, the 2D Rendering samples are useful. The content introduced in this article used Mesh2d for drawing. In addition to that, drawing with Sprites is also frequently used. The Math, 3D Rendering, and Animation examples also seem like good references for creative coding. If this article has sparked your interest in Bevy, I recommend checking out those samples and documentation as well.

You can also find plenty of information about Bevy online. Please feel free to explore.

Introduction to Yumemi Daigirin

This article is featured in "Yumemi Daigirin '25," which is distributed at Tech Book Fest 18. There are many other interesting articles included, so please take a look.

https://techbookfest.org/product/n5jRU01unxcQHkqtP6FVki

脚注
  1. Nannou Official Site https://nannou.cc/ ↩︎

  2. Bevy Official Site https://bevyengine.org/ ↩︎

  3. Rust Official Site https://www.rust-lang.org/ ↩︎

GitHubで編集を提案

Discussion