2020年に執筆された以下のチュートリアルを2024年の今実施してみたいと思います。
上記記事のチュートリアルでのBevyのバージョン"0.7.0"です。
今回はわたしが実施するBebyのバージョンは"0.15.0"です。
Bevyはバージョンによって記述がまったく変わりますので、試行錯誤しながら実装したいと思います。
Bevy初心者なので、何が新しい書き方なのか古いのかも、適切な書き方も知らないため、間違いがあるかもしれませんから、あくまでご参考までということで。
Creating a window
use bevy::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup_camera)
.run();
}
fn setup_camera(mut commands: Commands) {
commands.spawn(Camera2d);
}
0.15.0あたりから、カメラ作成は上記の記述になったらしいです。
前の記述と見比べると、シンプルに書けるようになったんだなあという印象です。
システムも、スケジュールラベルを指定して、追加するシステムを記述するのかな? わからん。
The beginnings of a snake
#[derive(Component)]
struct SnakeHead;
コンポーネントの作成はそのまま。
const SNAKE_HEAD_COLOR: Color = Color::srgb(0.7, 0.7, 0.7);
rgb()
は廃止されたらしいので、代わりにColor::srgb()
を使いました。
fn spawn_snake(mut commands: Commands) {
commands.spawn((
SnakeHead,
Sprite {
color: SNAKE_HEAD_COLOR,
..default()
},
Transform {
scale: Vec3::new(10.0, 10.0,10.0),
..default()
}
));
}
SpriteBundle
は廃止されたらしいので、Sprite
コンポーネントを使いました。
SnakeHeadコンポーネントの追加は、insert
でも書けるし、こうも書けるっぽい。
.add_systems(Startup, (setup_camera, spawn_snake))
Startup
のスケジュールにspawn_snake
を追加しました。
この場合は、setup_camera
とspawn_snake
はたぶん実行可能ならパラレルに処理されるのかな。
以下のチュートリアルのQuick Noteでそんなことを言っている。
Moving the snake
fn snake_movement(mut head_positions: Query<&mut Transform, With<SnakeHead>>) {
for mut transform in head_positions.iter_mut() {
transform.translation.y += 2.0;
}
}
一足先に以降の項目で出てくるWith
を使っていますが、特別に新しめな記述はないと思います。
.add_systems(Update, snake_movement)
Update
ラベル?で新しいシステムを追加しました。
Controlling the snake
fn snake_movement(
keyboard_input: Res<ButtonInput<KeyCode>>,
mut head_positions: Query<&mut Transform, With<SnakeHead>>,
) {
for mut transform in head_positions.iter_mut() {
if keyboard_input.pressed(KeyCode::ArrowLeft) {
transform.translation.x -= 2.0;
}
if keyboard_input.pressed(KeyCode::ArrowRight) {
transform.translation.x += 2.0;
}
if keyboard_input.pressed(KeyCode::ArrowDown) {
transform.translation.y -= 2.0;
}
if keyboard_input.pressed(KeyCode::ArrowUp) {
transform.translation.y += 2.0;
}
}
}
Input
がButtonInput
へ変わったのと、KeyCodeの矢印キーの名前が変わりました。
Slapping a grid on it
ARENA_WIDTH、ARENA_HEIGHTの定義はそのままです。
PositionとSizseコンポーネントの定義もそのままでよいようです。
fn spawn_snake(mut commands: Commands) {
commands.spawn((
SnakeHead,
Sprite {
color: SNAKE_HEAD_COLOR,
..default()
},
Position {x:3, y:3},
Size::square(0.8),
));
}
新しいコンポーネントを並べて記述します。
fn size_scaling(
windows_query: Query<&Window,With<PrimaryWindow>>,
mut sprite_query: Query<(&Size, &mut Transform)>,
) {
let window = windows_query.single();
for (sprite_size, mut transform) in sprite_query.iter_mut() {
transform.scale = Vec3::new(
sprite_size.width / ARENA_WIDTH as f32 * window.width() as f32,
sprite_size.heigth / ARENA_HEIGHT as f32 * window.height() as f32,
1.0,
)
}
}
Windowがリソースからコンポーネント扱いになったようなので、それにあわせて書きます。
クエリの対象がひとつだけなら、こうでいいらしい。
fn position_translation(
windows_query: Query<&Window,With<PrimaryWindow>>,
mut sprite_query: Query<(&Position, &mut Transform)>,
) {
fn convert(pos: f32, bound_window: f32, bound_game:f32) -> f32 {
let tile_size = bound_window / bound_game;
pos / bound_game * bound_window - (bound_window / 2.0) + (tile_size / 2.0)
}
let window = windows_query.single();
for(pos, mut transform) in sprite_query.iter_mut() {
transform.translation = Vec3::new(
convert(pos.x as f32, window.width(), ARENA_WIDTH as f32),
convert(pos.y as f32, window.height(), ARENA_HEIGHT as f32),
0.0,
);
}
}
SpriteコンポーネントはデフォルトでTransformを持っているので、それのtranslationを書き換えて、座標を指定するようです。
size_scaling
もposition_translation
も、Transformを持っているエンティティをクエリして、それらすべての座標(translation)、サイズ(Scale)を変更します。
これによって、現在作成済みのSnakeHeadだけでなく、これからつくるFoodやSnakeSegmentも座標、サイズの変換が適用されます。
Using our grid
fn snake_movement(
keyboard_input: Res<ButtonInput<KeyCode>>,
mut head_positions: Query<&mut Position, With<SnakeHead>>,
) {
for mut position in head_positions.iter_mut() {
if keyboard_input.pressed(KeyCode::ArrowLeft) {
position.x -= 1;
}
if keyboard_input.pressed(KeyCode::ArrowRight) {
position.x += 1;
}
if keyboard_input.pressed(KeyCode::ArrowDown) {
position.y -= 1;
}
if keyboard_input.pressed(KeyCode::ArrowUp) {
position.y += 1;
}
}
}
このへんはそのままで動きました。
Resizing the window
App::new()
.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "SNAKE the Snake".to_string(),
resolution: (500.0,500.0).into(),
..default()
}),
..default()
}))
.insert_resource(ClearColor(Color::srgb(0.04, 0.04, 0.04)))
.add_systems(Startup, (setup_camera, spawn_snake))
以下のリファレンスを参考に上記のように書きました。
rgb()
は前回と同じくsrgb()
で書き換えました。
Spawning food
const FOOD_COLOR: Color = Color::srgb(1.0, 0.0, 1.0);
FOOD_COLORは同じように追加します。
randとFoodコンポーネントも追加します。
fn food_spawner(mut commands: Commands) {
commands.spawn((
Food,
Sprite {
color: FOOD_COLOR,
..default()
},
Position {
x: (random::<f32>() * ARENA_WIDTH as f32) as i32,
y: (random::<f32>() * ARENA_HEIGHT as f32) as i32,
},
Size::square(0.8),
));
}
これまでの応用で、Foodエンティティをつくります。
random::<f32>()
は[0, 1)
の範囲、つまり0以上1未満の少数をランダムで生成しますので、それにゲームグリッドの升目数をかけて、位置を決めます。
fn main() {
App::new()
.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "SNAKE the Snake".to_string(),
resolution: (500.0, 500.0).into(),
..default()
}),
..default()
}))
.insert_resource(ClearColor(Color::srgb(0.04, 0.04, 0.04)))
.insert_resource(Time::<Fixed>::from_seconds(1.0))
.add_systems(Startup, (setup_camera, spawn_snake))
.add_systems(PostUpdate, (size_scaling, position_translation))
.add_systems(Update, snake_movement)
.add_systems(FixedUpdate, food_spawner)
.run();
}
ここが適切な書き方がよくわかりませんでしたが、一定フレーム毎に処理するシステムはこう書けばそれっぽくなりました。
タイミングが二種類在る場合は、on_timer
を使うようです。
番外
ここで、ウィンドウを閉じてアプリケーションが終了した際にpanicを起こしているので、それを防ぎたいと思います。
2024-12-31T11:42:03.593106Z INFO bevy_winit::system: Creating new window "SNAKE the Snake" (0v1#4294967296)
thread 'Compute Task Pool (0)' panicked at src/main.rs2024-12-31T11:42:10.944625Z INFO bevy_window::system: No windows are open, exiting
:109:32:
called `Result::unwrap()` on an `Err` value: NoEntities("bevy_ecs::query::state::QueryState<&bevy_window::window::Window, bevy_ecs::query::filter::With<bevy_window::window::PrimaryWindow>>")
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Encountered a panic in system `hatt_the_snake::size_scaling`!
Encountered a panic in system `bevy_app::main_schedule::Main::run_main`!
error: process didn't exit successfully: `target\debug\hatt-the-snake.exe` (exit code: 101)
原因は、ウィンドウを閉じたタイミングでsize_scaling
とposition_translation
の以下のコードが走っていることのようです。
let window = windows_query.single();
そこで、get_single
に置き換え、Resultを受け取ってエラー処理するようにします。
let window = windows_query.get_single();
match window{
Ok(window) => {
/* to do something */
},
Err(error) => {
println!("Error: {}", error);
}
}
たぶん、これで何とかなった気がします。
続きは後編へ
長いので分割します。
後編はこちらからどうぞ。