Простая 2d игра, Rust, Bevy
Недавно стал изучать язык 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