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ゲームのクローンのチュートリアルをやる [後編]

Posted at

前編からの続きです。

読んでいるチュートリアルはこちらです。

More snake-like movement

Direction、SnakeHeadのコードはそのままです。

fn spawn_snake(mut commands: Commands) {
    commands.spawn((
        SnakeHead {
            direction: Direction::Up,
        },

生成時に方向の初期値としてUpを設定しておきます。

ふたつのタイミングでシステムを動かしたくなったので、on_timerを使うことにします。

.add_systems(
    Update,
    (
        snake_movement_input.before(snake_movement),
        food_spawner.run_if(on_timer(Duration::from_secs_f32(1.0))),
        snake_movement.run_if(on_timer(Duration::from_secs_f32(0.5))),
    ),
)

on_timerモジュール?を追加しておきます。

use bevy::{prelude::*, time::common_conditions::on_timer, window::PrimaryWindow};

snake_movementsnake_movement_input自体の実装は以下のような感じで、そのままです。

fn snake_movement(mut heads: Query<(&mut Position, &SnakeHead)>) {
    if let Some((mut head_pos, head)) = heads.iter_mut().next() {
        match head.direction {
            Direction::Left => head_pos.x -= 1,
            Direction::Right => head_pos.x += 1,
            Direction::Up => head_pos.y += 1,
            Direction::Down => head_pos.y -= 1,
        }
    }
}

fn snake_movement_input(
    keyboard_input: Res<ButtonInput<KeyCode>>,
    mut heads: Query<&mut SnakeHead>,
) {
    if let Some(mut head) = heads.iter_mut().next() {
        let dir = if keyboard_input.pressed(KeyCode::ArrowLeft) {
            Direction::Left
        } else if keyboard_input.pressed(KeyCode::ArrowRight) {
            Direction::Right
        } else if keyboard_input.pressed(KeyCode::ArrowDown) {
            Direction::Down
        } else if keyboard_input.pressed(KeyCode::ArrowUp) {
            Direction::Up
        } else {
            head.direction
        };
        if dir != head.direction.opposite() {
            head.direction = dir;
        }
    }
}

Adding a tail

SNAKE_SEGMENT_COLOR、SnakeSegmentコンポーネントの追加はそのままです。

#[derive(Resource, Default)]
struct SnakeSegments(Vec<Entity>);

リソースの作成には、Resourceの記述が必要になっているっぽいので、書きます。

リソースの挿入はそのままです。

fn spawn_segment(mut commands: Commands, position: Position) -> Entity{
    commands.spawn((
        SnakeSegment,
        Sprite {
            color: SNAKE_SEGMENT_COLOR,
            ..default()
        },
        position,
        Size::square(0.65),
    ))
    .id()
}

これまでどおりに、エンティティを生成し、最後にエンティティのid()を返す関数をつくります。

fn spawn_snake(mut commands: Commands, mut segments: ResMut<SnakeSegments>) {
    *segments = SnakeSegments(vec![
        commands.spawn((
            SnakeHead {
                direction: Direction::Up,
            },
            SnakeSegment,
            Sprite {
                color: SNAKE_HEAD_COLOR,
                ..default()
            },
            Position { x: 3, y: 3 },
            Size::square(0.8),
        ))
        .id(),
        spawn_segment(commands, Position { x: 3, y: 2 }),
    ]);
}

これもこれまでと同じように書いています。

SnakeSegmentコンポーネントはSnakeHeadコンポーネントのついたエンティティにもつけるんですね。

Making the tail follow the snake

fn snake_movement(
    segments: ResMut<SnakeSegments>,
    mut heads: Query<(Entity, &SnakeHead)>,
    mut positions: Query<&mut Position>,
) {
    if let Some((head_entity, head)) = heads.iter_mut().next() {
        let segment_positions = segments
            .0.iter()
            .map(|e| *positions.get_mut(*e).unwrap())
            .collect::<Vec<Position>>();
        let mut head_pos = positions.get_mut(head_entity).unwrap();
        match &head.direction {
            Direction::Left => head_pos.x -= 1,
            Direction::Right => head_pos.x += 1,
            Direction::Up => head_pos.y += 1,
            Direction::Down => head_pos.y -= 1,
        };
        segment_positions
            .iter()
            .zip(segments.0.iter().skip(1))
            .for_each(|(pos, segment)| {
                *positions.get_mut(*segment).unwrap() = *pos;
            });
    }
}
- .iter()
+ .0.iter()

この変更があります。

SnakeSegments構造体は要素がひとつのタプル構造体のため、それにアクセスするには上記の記載が必要そうでした。

以下のあたりが、まだ理解できていません。

segment_positions
    .iter()
    .zip(segments.iter().skip(1))
    .for_each(|(pos, segment)| {
        *positions.get_mut(*segment).unwrap() = *pos;
    });

segment_positionssegments.iter().skip(1)を同期させて、ひとつのタプルに詰め合わせ、それに対してfor_eachしてるっぽいです。

segmentsのほうはひとつ飛ばしをしていますが、その情報がsegment_positionsにも反映され、どちらも一つ目の要素を飛ばして読むように同期しているようです。

Growing the snake

fn snake_eating(
    mut commands: Commands,
    mut growth_writer: EventWriter<GrowthEvent>,
    food_positions: Query<(Entity, &Position), With<Food>>,
    head_positions: Query<&Position, With<SnakeHead>>,
) {
    for head_pos in head_positions.iter() {
        for (food_entity, food_position) in food_positions.iter() {
            if head_pos == food_position {
                commands.entity(food_entity).despawn();
                growth_writer.send(GrowthEvent);
            }
        }
    }
}

今はdespawnのやり方が変わって、こんな感じらしいです。

GrowthEventとAppへの追加はそのままです。

.add_systems(
    Update,
    (
        snake_movement_input.before(snake_movement),
        food_spawner.run_if(on_timer(Duration::from_secs_f32(1.0))),
        snake_movement.run_if(on_timer(Duration::from_secs_f32(0.5))),
        snake_eating.after(snake_movement),
    ),
)

.after()で順番を指定します。

#[derive(Resource, Default)]
struct LastTailPosition(Option<Position>);

リソース指定し、insert_resourceしておきます。

*last_tail_position = LastTailPosition(Some(*segment_positions.last().unwrap()));

作成したリソースにSnakeSegmentsの最後の要素を入れておきます。

fn snake_growth(
    commands: Commands,
    last_tail_position: Res<LastTailPosition>,
    mut segments: ResMut<SnakeSegments>,
    mut growth_reader: EventReader<GrowthEvent>,
) {
    if growth_reader.read().next().is_some() {
        segments
            .0
            .push(spawn_segment(commands, last_tail_position.0.unwrap()));
    }
}

今のイベントの読み出し方は上記のように書いたら動きました。

.add_systems(
    Update,
    (
        snake_movement_input.before(snake_movement),
        food_spawner.run_if(on_timer(Duration::from_secs_f32(1.0))),
        snake_movement.run_if(on_timer(Duration::from_secs_f32(0.5))),
        snake_eating.after(snake_movement),
        snake_growth.after(snake_eating),
    ),
)

今回の項目では、Updateは最終的に上記のようになりました。

Hitting the wall (or our tail)

#[derive(Event)]
struct GrowthEvent;

イベントを定義し、add_eventします。

game_over_writer.send(GameOverEvent)の記述はそのままです。

fn game_over(
    mut commands: Commands,
    mut reader: EventReader<GameOverEvent>,
    segments_res: ResMut<SnakeSegments>,
    food: Query<Entity, With<Food>>,
    segments: Query<Entity, With<SnakeSegment>>,
) {
    if reader.read().next().is_some() {
        for entity in food.iter().chain(segments.iter()) {
            commands.entity(entity).despawn();
        }
        spawn_snake(commands, segments_res);
    }
}

イベントが発行されていたら、処理します。

chainでFoodとSnakeSegmentsをつなげて処理します。

そして、新しい蛇を生み出します。

.add_systems(
    Update,
    (
        snake_movement_input.before(snake_movement),
        food_spawner.run_if(on_timer(Duration::from_secs_f32(1.0))),
        snake_movement.run_if(on_timer(Duration::from_secs_f32(0.5))),
        snake_eating.after(snake_movement),
        snake_growth.after(snake_eating),
        game_over.after(snake_movement),
    ),
)

最後にゲームオーバー用のシステムを追加して完成です。

終わりに

早足となりましたが、Snakeゲームクローンのチュートリアルを終えました。

バージョンによって書き方がだいぶ違うところもあり、試行錯誤が多少ありましたが、RustやBevyが出すコンパイルエラーのメッセージや、VSCodeの拡張機能での型情報の補完があることで、試行錯誤の手数は少なくて済んだような印象があります。

リファレンスを読むのにも慣れてきたかなあと勝手に評価しています。

この経験を活かして、これからも何か小さいゲームをつくって、RustとBevyを楽しんでいきたいと思います。

ここまで読んでいただき、ありがとうございました。

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?