0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-12-31

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_cameraspawn_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;
        }
    }
}

InputButtonInputへ変わったのと、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_scalingposition_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_scalingposition_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);
    }
}

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

続きは後編へ

長いので分割します。

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?