Snakeゲームのクローンのチュートリアルをやる [前編]

2020年に執筆された以下のチュートリアルを2024年の今実施してみたいと思います。

https://mbuffett.com/posts/bevy-snake-tutorial/

上記記事のチュートリアルでのBevyのバージョン"0.7.0"です。

今回はわたしが実施するBebyのバージョンは"0.15.0"です。

Bevyはバージョンによって記述がまったく変わりますので、試行錯誤しながら実装したいと思います。

:::note
Bevy初心者なので、何が新しい書き方なのか古いのかも、適切な書き方も知らないため、間違いがあるかもしれませんから、あくまでご参考までということで。
:::

# Creating a window

```rust
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);
}
```

https://docs.rs/bevy/latest/bevy/prelude/struct.Camera2d.html

0.15.0あたりから、カメラ作成は上記の記述になったらしいです。

前の記述と見比べると、シンプルに書けるようになったんだなあという印象です。

システムも、スケジュールラベルを指定して、追加するシステムを記述するのかな？　わからん。

# The beginnings of a snake

```rust
#[derive(Component)]
struct SnakeHead;
```

コンポーネントの作成はそのまま。

```rust
const SNAKE_HEAD_COLOR: Color = Color::srgb(0.7, 0.7, 0.7);
```

https://docs.rs/bevy/latest/bevy/color/enum.Color.html#implementations

`rgb()`は廃止されたらしいので、代わりに`Color::srgb()`を使いました。

```rust
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()
        }
    ));
}
```

https://docs.rs/bevy/latest/bevy/prelude/struct.SpriteBundle.html

`SpriteBundle`は廃止されたらしいので、`Sprite`コンポーネントを使いました。

SnakeHeadコンポーネントの追加は、`insert`でも書けるし、こうも書けるっぽい。

```rust
.add_systems(Startup, (setup_camera, spawn_snake))
```

`Startup`のスケジュールに`spawn_snake`を追加しました。

この場合は、`setup_camera`と`spawn_snake`はたぶん実行可能ならパラレルに処理されるのかな。

以下のチュートリアルのQuick Noteでそんなことを言っている。

https://bevyengine.org/learn/quick-start/getting-started/ecs/

# Moving the snake

```rust
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`を使っていますが、特別に新しめな記述はないと思います。

```rust
.add_systems(Update, snake_movement)
```

`Update`ラベル？で新しいシステムを追加しました。

# Controlling the snake

```rust

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コンポーネントの定義もそのままでよいようです。

```rust
fn spawn_snake(mut commands: Commands) {
    commands.spawn((
        SnakeHead,
        Sprite {
            color: SNAKE_HEAD_COLOR,
            ..default()
        },
        Position {x:3, y:3},
        Size::square(0.8),
    ));
}
```
新しいコンポーネントを並べて記述します。

```rust
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がリソースからコンポーネント扱いになったようなので、それにあわせて書きます。

クエリの対象がひとつだけなら、こうでいいらしい。

https://bevy-cheatbook.github.io/programming/queries.html#unique-entities

```rust
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

```rust
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

```rust
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))
```

以下のリファレンスを参考に上記のように書きました。

https://docs.rs/bevy/latest/bevy/window/struct.WindowPlugin.html

https://docs.rs/bevy/latest/bevy/window/struct.Window.html

`rgb()`は前回と同じく`srgb()`で書き換えました。

# Spawning food

```rust
const FOOD_COLOR: Color = Color::srgb(1.0, 0.0, 1.0);
```

FOOD_COLORは同じように追加します。

randとFoodコンポーネントも追加します。

```rust

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未満の少数をランダムで生成しますので、それにゲームグリッドの升目数をかけて、位置を決めます。

```rust

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();
}
```

https://bevy-cheatbook.github.io/fundamentals/fixed-timestep.html

https://bevyengine.org/learn/migration-guides/0-9-to-0-10/#fixed-timesteps

ここが適切な書き方がよくわかりませんでしたが、一定フレーム毎に処理するシステムはこう書けばそれっぽくなりました。

タイミングが二種類在る場合は、`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`の以下のコードが走っていることのようです。
```rust
let window = windows_query.single();
```

そこで、`get_single`に置き換え、Resultを受け取ってエラー処理するようにします。

```rust
let window = windows_query.get_single();
match window{
    Ok(window) => {
        /* to do something */
    },
    Err(error) => {
        println!("Error: {}", error);
    }
}
```

たぶん、これで何とかなった気がします。


# 続きは後編へ

長いので分割します。

後編はこちらからどうぞ。

https://qiita.com/fujino_tech/items/4086dd6235c0f4025a59