Простая 2d игра, Rust, Bevy | Блог python программиста
Изображение гика

Блог питониста

Простая 2d игра, Rust, Bevy

20 апреля 2025 г.

Недавно стал изучать язык Rust по очень хорошей книге. Захотелось что-то написать на нем, ну а что я могу написать, конечно, решил сделать простой прототип 2d игры на нём, а именно на движке Bevy. Судя по всему, это один из самых популярных игровых движков на Rust сейчас. Но это не точно.

Данный движок умеет как в 3d, так и в 2d. Пока немного документации на него, но зато куча очень крутых примеров. Как говорится, лучшая документация - это примеры кода. Пока мне очень нравится в нем что-то пробовать. Конечно, я пока не могу сделать сколько-нибудь интересную игру на нём, но зато могу сделать простой прототип. И в этом посте я сделаю этот прототип.

Также, покажу как можно довольно просто собрать проект под web. То есть, на выходе у нас будет просто страничка html, немного "связующего"js и собственно wasm. То есть наш проект будет "собран" в WebAssembly.

Я не буду рассказывать как установить Rust, можете прочитать на официальном сайте.

Буду использовать cargo для создания, запуска проекта и для менеджмента зависимостей. Создам проект:

cargo new simple-2d-bevy-game
cd simple-2d-bevy-game
cargo run

Будет выведено в консоль:

hello world

Добавлю bevy в проект как зависимость:

cargo add bevy

Теперь начинаю делать игру, так меняю файл main.rs:

 1// имопртируем все, что нужно из bevy
 2use bevy::prelude::*;
 3use bevy::render::settings::*;
 4use bevy::render::RenderPlugin;
 5
 6// размер прямогульника
 7const SPRITE_SIZE: f32 = 50.;
 8
 9// здесь просто базовая настройка 
10fn main() {
11    App::new()
12        .add_plugins((
13            DefaultPlugins.set(RenderPlugin {
14                render_creation: WgpuSettings {
15                    backends: Some(Backends::GL),
16                    ..default()
17                }
18                .into(),
19                ..default()
20            }),
21            MeshPickingPlugin,
22        ))
23        .add_systems(Startup, setup)
24        .run();
25}
26
27// здесь создаю камеру и потом создаю один зеленый квадрат,
28// расположенный по координатам `x=100`, `y=100`
29fn setup(
30    mut commands: Commands,
31    mut meshes: ResMut<Assets<Mesh>>,
32    mut materials: ResMut<Assets<ColorMaterial>>,
33) {
34    commands.spawn(Camera2d);
35
36    commands.spawn((
37        Mesh2d(meshes.add(Rectangle::new(SPRITE_SIZE, SPRITE_SIZE))),
38        MeshMaterial2d(materials.add(Color::srgb(0., 0.2, 0.))),
39        Transform::from_xyz(100.0, 100.0, 0.0),
40    ));
41}

Теперь, если сделать cargo run, то должно появиться окно с одним квадратом зеленого цвета.

Теперь я добавлю обработчик, который будет вызываться, каждый раз при клике на данный квадрат:

 1fn setup(
 2    mut commands: Commands,
 3    mut meshes: ResMut<Assets<Mesh>>,
 4    mut materials: ResMut<Assets<ColorMaterial>>,
 5) {
 6    commands.spawn(Camera2d);
 7
 8    commands.spawn((
 9        Mesh2d(meshes.add(Rectangle::new(SPRITE_SIZE, SPRITE_SIZE))),
10        MeshMaterial2d(materials.add(Color::srgb(0., 0.2, 0.))),
11        Transform::from_xyz(100.0, 100.0, 0.0),

13    )).observe(on_rect_click);

15}
16

18fn on_rect_click(
19    _click: Trigger<Pointer<Click>>,
20    mut mat_query: Query<
21        &mut MeshMaterial2d<ColorMaterial>,
22    >,
23    mut materials: ResMut<Assets<ColorMaterial>>,
24) {
25    println!("click on rect happened");
26
27    let elem: Mut<'_, MeshMaterial2d<ColorMaterial>> = mat_query.single_mut();
28    let asset_id: AssetId<ColorMaterial> = elem.0.id();
29    materials.get_mut(asset_id).unwrap().color = Color::BLACK;
30}

После добавления этого куска, цвет квадрата будет меняться на черный при клике на него. Теперь создам константу для обозначения зеленого:


2const GREEN: Color = Color::srgb(0., 0.2, 0.);

И поменяю эту строку:


2materials.get_mut(asset_id).unwrap().color = Color::BLACK;

На эти, чтобы цвет менялся с зеленого на черный по кругу:


2if materials.get_mut(asset_id).unwrap().color == Color::BLACK {
3    materials.get_mut(asset_id).unwrap().color = GREEN;
4}
5else {
6    materials.get_mut(asset_id).unwrap().color = Color::BLACK;
7}

Теперь вместо создания одного квадрата, создам два в цикле:

 1for i in 0..2 {
 2    commands
 3        .spawn((
 4            Mesh2d(meshes.add(Rectangle::new(SPRITE_SIZE, SPRITE_SIZE))),
 5            MeshMaterial2d(materials.add(GREEN)),
 6            Transform::from_xyz(i as f32 * 100.0, i as f32 * 100.0, 0.0),
 7            RectangleIndexes(i),
 8        ))
 9        .observe(on_rect_click);
10}

И поменяю функцию on_rect_click, чтобы она правильно обрабатывала клики по каждому из двух квадратов:

 1fn on_rect_click(
 2    click: Trigger<Pointer<Click>>,
 3    mut mat_query: Query<
 4        (&mut MeshMaterial2d<ColorMaterial>, &RectangleIndexes),
 5        With<RectangleIndexes>,
 6    >,
 7    mut rect_indexes_q: Query<&RectangleIndexes>,
 8    mut materials: ResMut<Assets<ColorMaterial>>,
 9) {
10    println!("click on rect happened");
11
12    let rect_index: &RectangleIndexes = rect_indexes_q.get_mut(click.target).unwrap();
13
14    for elem in mat_query.iter_mut() {
15        let some_mut: Mut<'_, MeshMaterial2d<ColorMaterial>> = elem.0;
16        let asset_id: AssetId<ColorMaterial> = some_mut.0.id();
17        let ind: i32 = elem.1.0;
18        if ind == rect_index.0 {
19            if materials.get_mut(asset_id).unwrap().color == Color::BLACK {
20                materials.get_mut(asset_id).unwrap().color = GREEN;
21            } else {
22                materials.get_mut(asset_id).unwrap().color = Color::BLACK;
23            }
24        }
25    }
26}

Далее добавлю Текст, который будет меняться по клику на кнопку, добавлю ee позднее:

 1fn setup(
 2    mut commands: Commands,
 3    mut meshes: ResMut<Assets<Mesh>>,
 4    mut materials: ResMut<Assets<ColorMaterial>>,
 5) {
 6    commands.spawn(Camera2d);

 8    // Text with one section
 9    commands.spawn((
10        // Accepts a `String` or any type that converts into a `String`, such as `&str`
11        Text::new("Plz click on some rect"),
12        TextFont {
13            // This font is loaded and will be used instead of the default font.
14            // font: asset_server.load("fonts/FiraSans-Bold.ttf"),
15            font_size: 32.0,
16            ..default()
17        },
18        // Set the justification of the Text
19        TextLayout::new_with_justify(JustifyText::Right),
20        // Set the style of the Node itself.
21        Node {
22            justify_self: JustifySelf::Center,
23            ..default()
24        },
25        Label,
26    ));

Добавляю кнопку:

 1commands
 2    .spawn((
 3        Button,
 4        Node {
 5            top: Val::Px(60.0),
 6            border: UiRect::all(Val::Px(5.0)),
 7            justify_self: JustifySelf::Center,
 8            ..default()
 9        },
10        BorderColor(Color::BLACK),
11        BorderRadius::MAX,
12    ))
13    .with_children(|builder| {
14        builder.spawn((
15            Text::new("Click on me"),
16            TextFont {
17                font_size: 32.0,
18                ..default()
19            },
20            TextColor(Color::srgb(0.9, 0.9, 0.9)),
21        ));
22    });

И обрабатываю клик по ней, чтобы менялся текст:

1.add_systems(Update, button_system)
 1fn button_system(
 2    mut interaction_query: Query<&Interaction, (Changed<Interaction>, With<Button>)>,
 3    mut text_query: Query<&mut Text, With<Label>>,
 4) {
 5    for interaction in &mut interaction_query {
 6        match *interaction {
 7            Interaction::Pressed => {
 8                println!("Btn is pressed");
 9                text_query.get_single_mut().unwrap().0 = String::from("Btn is clicked");
10            }
11            _ => {}
12        }
13    }
14}

Вот такой, небольшой прототип 2d игры получился, ссылка на github проекта. В конце расскажу как собрать любой проект на bevy под веб. Этот процесс отлично описан тут. Но я расскажу, как этот подход применять к данному прототипу.

Сначала нужно поставить 2 зависимости:

rustup target add wasm32-unknown-unknown
cargo install wasm-bindgen-cli

Затем, в корневой директории проекта создаю папку www с одним файлом index.html такого содержания:

 1<html lang="en">
 2  <head>
 3    <meta charset="UTF-8" />
 4    <style>
 5      body {
 6        background: linear-gradient(
 7          135deg,
 8          white 0%,
 9          white 49%,
10          black 49%,
11          black 51%,
12          white 51%,
13          white 100%
14        ) repeat;
15        background-size: 20px 20px;
16      }
17      canvas {
18        background-color: white;
19      }
20    </style>
21    <title>Wasm Example UP</title>
22  </head>
23  <script type="module">
24    import init from './simple_2d_game.js'
25    init()
26  </script>
27</html>

В файл Cargo.toml, нужно добавить всякое, для компиляции и оптимизаций разных:

 1# Enable a small amount of optimization in the dev profile.
 2[profile.dev]
 3opt-level = 1
 4
 5# Enable a large amount of optimization in the dev profile for dependencies.
 6[profile.dev.package."*"]
 7opt-level = 3
 8
 9# Enable more optimization in the release profile at the cost of compile time.
10[profile.release]
11# Compile the entire crate as one unit.
12# Slows compile times, marginal improvements.
13codegen-units = 1
14# Do a second optimization pass over the entire program, including dependencies.
15# Slows compile times, marginal improvements.
16lto = "thin"
17
18# Optimize for size in the wasm-release profile to reduce load times and bandwidth usage on web.
19[profile.wasm-release]
20# Default to release profile values.
21inherits = "release"
22# Optimize with size in mind (also try "z", sometimes it is better).
23# Slightly slows compile times, great improvements to file size and runtime performance.
24opt-level = "z"
25# Strip all debugging information from the binary to slightly reduce file size.
26strip = "debuginfo"

Далее, из корня проекта нужно скомпилировать wasm:

cargo build --profile wasm-release --target wasm32-unknown-unknown

В результате должен создаться скомпилированный wasm в директории target/wasm32-unknown-unknown/wasm-release/. Ок, осталось сгенеритьjs, который будет все это "склеивать":

wasm-bindgen --out-name simple_2d_game --out-dir www --target web target/wasm32-unknown-unknown/wasm-release/simple-2d-bevy-game.wasm

Запускаем сервер и проверяем результат:

python -m http.server --directory www

Метки

bevy rust
Если вам понравился пост, можете поделиться им в соцсетях: