前編からの続きです。
読んでいるチュートリアルはこちらです。
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_movement
とsnake_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_positions
とsegments.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を楽しんでいきたいと思います。
ここまで読んでいただき、ありがとうございました。