RustのBevy(0.14.2)でスネークゲームを作る
use bevy::prelude::*;
fn main() {
App::new().run();
}
ここは問題なし
rand
クレートを依存関係に追加する.
fn main() {
App::new().add_plugins(DefaultPlugins).run();
}
も問題なし
cameraのセットアップを行う.
fn setup_camera(mut commands: Commands) {
commands.spawn((
Camera2dBundle {
camera: Camera {
hdr: true,
..default()
},
..default()
},
BloomSettings::NATURAL,
));
}
これはかなり変更が必要.
まずspawn_bundle
ではなくspawn
を利用し,OrthographicCameraBundle
も無いので他で代替する.公式githubのこの例をコピペさせてもらった.
App::new()
.add_systems(Startup, setup_camera)
.add_plugins(DefaultPlugins)
.run();
システムを付与するのは全てadd_system()
になっており,第1引数にシステムを呼び出すタイミングを与える.
これで実行すると,灰色の画面が出てくる.
蛇の頭を作成する.
#[derive(Component)]
struct SnakeHead;
これがコンポーネントらしい.
次に蛇の頭の色を定義する.
const SNAKE_HEAD_COLOR: Color = Color::linear_rgb(0.7, 0.7, 0.7);
rgb()
は今ではよくない書き方らしいので,linear_rgb()
に変えておく.
スネークヘッドエンティティを作成するspawn_snake
というシステムを作成する.
fn spawn_snake(mut commands: Commands) {
commands
.spawn(SpriteBundle {
sprite: Sprite {
color: SNAKE_HEAD_COLOR,
..default()
},
transform: Transform {
scale: Vec3::new(10.0, 10.0, 10.0),
..default()
},
..default()
})
.insert(SnakeHead);
}
先程と同様,spawn()
で代替する.他は変わらない.
App
にspawn_snake
を追加するが,add_systems()
を2つ以上連ねる必要はなく
App::new()
.add_systems(Startup, (setup_camera, spawn_snake))
.add_plugins(DefaultPlugins)
.run();
という風にまとめて書ける.(呼び出しのタイミングが同じ場合のみ)
次に蛇をうごかすシステムを定義する.
fn snake_movement(mut head_positions: Query<(&SnakeHead, &mut Transform)>) {
for (_head, mut transform) in head_positions.iter_mut() {
transform.translation.y += 2.;
}
}
これはそのまま書ける.y += 2.
としているが,上に移動する.珍しい.
Query<D, F>でデータDの属性Fに対するクエリということかな?
App.add_system(Update, snake_movement)
としてフレームごとにこれを呼び出すように設定する.
snake_movement
をキーボード入力によって蛇が移動するように変更する.
fn snake_movement(
keyboard_input: Res<ButtonInput<KeyCode>>,
mut head_position: Query<(&SnakeHead, &mut Transform)>,
) {
for (_head, mut transform) in head_position.iter_mut() {
if keyboard_input.pressed(KeyCode::ArrowUp) {
transform.translation.y += 2.;
}
if keyboard_input.pressed(KeyCode::ArrowDown) {
transform.translation.y -= 2.;
}
if keyboard_input.pressed(KeyCode::ArrowRight) {
transform.translation.x += 2.;
}
if keyboard_input.pressed(KeyCode::ArrowLeft) {
transform.translation.x -= 2.;
}
}
}
引数のkeyboard_input
の型はRes<ButtonInput<KeyCode>>
に変更し,KeyCode
はそれぞれ向きの前にArrow
をつける.
これで実行すると,蛇を自由自在に動かせるようになった.
Position
とSize
というコンポーネントを導入する.これはそのままで大丈夫.
その後,spawn_snake
にPosition
とSize
をinsertする.これも変わらない.
このままでは画面は何も変わらないので,位置と大きさを設定するシステムを定義していく.
大きさを設定するシステムを定義していく.
fn size_scaling(window: Query<&Window>, mut q: Query<(&Size, &mut Transform)>) {
let window = window.single();
for (sprite_size, mut transform) in q.iter_mut() {
transform.scale = Vec3::new(
sprite_size.width / ARENA_WIDTH as f32 * window.width(),
sprite_size.height / ARENA_HEIGHT as f32 * window.height(),
1.0,
);
}
}
元のコードでは引数のリソースからウィンドウサイズを取得しているが,今のバージョンではウィンドウ関連でResource
を継承しているものが無さそうだったので,クエリにした.(公式githubでそうしているから問題なさそう)
そのためwindow
を上書きする形になってたり,ウィンドウサイズにas f32
がついていなかったりと細々した変更がある.
次に位置を設定するシステム.
fn position_translation(window: Query<&Window>, mut q: 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.) + (tile_size / 2.)
}
let window = window.single();
for (pos, mut transform) in q.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,
);
}
}
変更点はさっきと一緒なので,言うことはない.
これらのシステムをApp
に追加するが,これはエンティティの状態更新直後に行ってほしいので,add_systems(PostUpdate, (position_translation, size_scaling))
とする.
これで実行すると,画面の少し左下の方につぶれた蛇が移される.
キーボード入力が無効になってしまっているが,問題ないらしい.
snake_movement
を少し変更.
fn snake_movement(
keyboard_input: Res<ButtonInput<KeyCode>>,
mut head_position: Query<&mut Position, With<SnakeHead>>,
) {
for mut pos in head_position.iter_mut() {
if keyboard_input.pressed(KeyCode::ArrowUp) {
pos.y += 1;
}
if keyboard_input.pressed(KeyCode::ArrowDown) {
pos.y -= 1;
}
if keyboard_input.pressed(KeyCode::ArrowRight) {
pos.x += 1;
}
if keyboard_input.pressed(KeyCode::ArrowLeft) {
pos.x -= 1;
}
}
}
ここで書いたクエリについての考察は,文章はだいたい合っているけど自分の中でのイメージとは違ってたぽい.このF
にあたるフィルターはWith
とかWithout
で書く必要がある.
次にウィンドウの初期設定を行っていく.insert_resource
では引数にResource
を継承した構造体をとるが,先ほど言ったようにそんなものが無さそうなので,またまた公式githubを参照させていただく.既存のコードの.add_plugins(DefaultPlugin)
を以下のように変更.
.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "Snake!".into(),
resolution: (500., 500.).into(),
..default()
}),
..default()
}))
次のClearColor
はrgb
をlinear_rgb
に変更するだけ.
これで実行すると,正方形の画面に正方形の蛇ができた.キーボード入力も受け取ってくれている.
ただ,これはウィンドウの形に合わせて蛇も伸び縮みするようになっているので,少し修正する.
size_scaling
を以下のように変更するだけ.
let window = window.single();
let window_size = window.width().min(window.height());
for (sprite_size, mut transform) in q.iter_mut() {
transform.scale = Vec3::new(
sprite_size.width / ARENA_WIDTH as f32 * window_size,
sprite_size.height / ARENA_HEIGHT as f32 * window_size,
1.0,
);
}
ウィンドウの縦と横の小さいほうに合わせて蛇の大きさを設定するようにした.これで常に正方形になってくれるはず.
次は餌を生成するシステムを作る.
fn food_spawner(mut commands: Commands) {
commands
.spawn(SpriteBundle {
sprite: Sprite {
color: FOOD_COLOR,
..default()
},
..default()
})
.insert(Food)
.insert(Position {
x: (random::<f32>() * ARENA_WIDTH as f32) as i32,
y: (random::<f32>() * ARENA_HEIGHT as f32) as i32,
})
.insert(Size::square(0.8));
}
spawn_bundle
のところ以外は変更はない.
これをApp
に追加して1秒間隔で呼び出してもらうようにするのだが,例のコードが今のバージョンでは全く使えないので,大きく変更する必要がある.
.add_systems(FixedMain, food_spawner)
...
.insert_resource(Time::<Fixed>::from_duration(Duration::from_secs(1)))
これで同じように実装できているはず.
ただ,ここで1つ問題がある.今は〇秒間隔で使用するシステムが1つだけだから大丈夫だが,他にもそのようなシステムを実装した場合,今の実装では全て同じ秒数にしか設定できない.
これ以降も〇秒間隔で使用するシステムなどいくらでも出てくると思うので,そこを柔軟に対応できるようにするために実装してみた.
まず,ジェネリックに使えるIntervalSetting
という型を作成.
#[derive(Resource)]
struct IntervalSetting<T> {
last_update: Instant,
phantom: PhantomData<T>,
}
impl<T> Default for IntervalSetting<T> {
fn default() -> Self {
Self {
last_update: Instant::now(),
phantom: PhantomData,
}
}
}
impl<T> IntervalSetting<T> {
fn check(&self, time: Instant, diff: Duration) -> bool {
time - self.last_update >= diff
}
fn update(&mut self, time: Instant) {
self.last_update = time;
}
}
前回の更新との差分が設定インターバルに達しているかを確認するcheck
と,時間を更新するupdate
を実装している.
ここで面倒なのは,Resource
として使用できるのは構造体のインスタンスではなく構造体そのものであるということなので,このような回りくどい実装になってしまった.PhantomData<T>
を属性として持つことで,ジェネリックに対応できるようにした.PhantomData<T>
なんて初めて使ったよ...(ドキドキ)
次にfood_spawner
を以下のように変更.
fn food_spawner(mut commands: Commands, mut time: ResMut<IntervalSetting<Food>>) {
let now = Instant::now();
if !time.check(now, Duration::from_secs(1)) {
return;
}
time.update(now);
// ...
// 元の処理
// ...
}
Resource
をミュータブルに使用するためにResMut
を引数にとり,Food
を型パラメータとして受け取るようにした.
そしてこれをinsert_resource(IntervalSetting::<Food>::default())
でApp
に追加する.
より蛇らしい動きをつけていく.今のままでは蛇は基本止まっていて,キーボード入力がある間だけその方向に動くようになっている.これを常に動いてキーボード入力で向きを変えるだけにする.まず向きを定義していく.
#[derive(PartialEq, Clone, Copy)]
enum Direction {
UP,
DOWN,
LEFT,
RIGHT,
}
impl Direction {
fn opposite(self) -> Self {
match self {
Self::UP => Self::DOWN,
Self::DOWN => Self::UP,
Self::LEFT => Self::RIGHT,
Self::RIGHT => Self::LEFT,
}
}
}
opposite
は後で使う.
次に,SnakeHead
の属性にdirection
を付与する.
#[derive(Component)]
struct SnakeHead {
direction: Direction,
}
これによって影響を受けるところを修正する.
fn spawn_snake(mut commands: Commands) {
// ...
.insert(SnakeHead {
direction: Direction::UP,
})
// ...
}
フレームごとに移動するとすごい速さになるので,0.15秒に1度だけ移動するようにする.これはさっきのIntervalSetting
を使う.
fn snake_movement(
keyboard_input: Res<ButtonInput<KeyCode>>,
mut head_position: Query<(&SnakeHead, &mut Transform)>,
mut time: ResMut<IntervalSetting<SnakeHead>>,
) {
let now = Instant::now();
if !time.check(now, Duration::from_millis(150)) {
return;
}
time.update(now);
// ...
// 元の処理
// ...
}
今のままだとキーボード入力も0.15秒間隔なので,キーボード入力受付と蛇の移動を分離させる.
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: Direction = if keyboard_input.pressed(KeyCode::ArrowUp) {
Direction::UP
} else if keyboard_input.pressed(KeyCode::ArrowDown) {
Direction::DOWN
} else if keyboard_input.pressed(KeyCode::ArrowRight) {
Direction::RIGHT
} else if keyboard_input.pressed(KeyCode::ArrowLeft) {
Direction::LEFT
} else {
head.direction
};
if dir != head.direction.opposite() {
head.direction = dir;
}
}
}
fn snake_movement(
mut heads: Query<(&mut Position, &SnakeHead)>,
mut time: ResMut<IntervalSetting<SnakeHead>>,
) {
let now = Instant::now();
if !time.check(now, Duration::from_millis(150)) {
return;
}
time.update(now);
if let Some((mut head_pos, head)) = heads.iter_mut().next() {
match &head.direction {
Direction::UP => {
head_pos.y += 1;
}
Direction::DOWN => {
head_pos.y -= 1;
}
Direction::RIGHT => {
head_pos.x += 1;
}
Direction::LEFT => {
head_pos.x -= 1;
}
};
}
}
(蛇の移動が呼び出されるなら)キーボード入力受付は蛇の移動より前に呼び出されるべきであるので,そのようにApp
に追加する.
.add_systems(
Update,
(
snake_movement_input.before(snake_movement),
snake_movement,
food_spawner,
),
)
snake_movement_input.before(snake_movement)
とsnake_movement
はどちらも書く.(私はこれで1時間ほど悩まされた)
これで実行するとよくあるスネークゲームの動きをしてくれるようになった!
今のままでは蛇はただの正方形なので,体を伸ばしていく.
そのために新たにSnakeSegments
を用意する.
const SNAKE_SEGMENT_COLOR: Color = Color::linear_rgb(0.3, 0.3, 0.3);
#[derive(Component)]
struct SnakeSegment;
#[derive(Default, Resource)]
struct SnakeSegments(Vec<Entity>);
後でリソースとして追加するので,Resource
をderiveしておく.
これをリソースとしてApp
に追加する.
.insert_resource(SnakeSegments::default())
次に,セグメントを新たに作成してそのEntity
を返すシステムを定義していく.
fn spawn_segment(mut commands: Commands, position: Position) -> Entity {
commands
.spawn(SpriteBundle {
sprite: Sprite {
color: SNAKE_SEGMENT_COLOR,
..default()
},
..default()
})
.insert(SnakeSegment)
.insert(position)
.insert(Size::square(0.5))
.id()
}
spawn
以外は変更なし.
これに伴ってspawn_snake
も変更する.
fn spawn_snake(mut commands: Commands, mut segments: ResMut<SnakeSegments>) {
*segments = SnakeSegments(vec![
commands
.spawn(SpriteBundle {
sprite: Sprite {
color: SNAKE_HEAD_COLOR,
..default()
},
transform: Transform {
scale: Vec3::new(10.0, 10.0, 10.0),
..default()
},
..default()
})
.insert(SnakeHead {
direction: Direction::UP,
})
.insert(SnakeSegment)
.insert(Position { x: 3, y: 3 })
.insert(Size::square(0.8))
.id(),
spawn_segment(commands, Position { x: 3, y: 2 }),
]);
}
特にいうことはない.
このまま実行すると,セグメントは生成されているが頭についてきていない.これを修正する.
fn snake_movement(
segments: ResMut<SnakeSegments>,
mut heads: Query<(Entity, &SnakeHead)>,
mut positions: Query<&mut Position>,
mut time: ResMut<IntervalSetting<SnakeHead>>,
) {
let now = Instant::now();
if !time.check(now, Duration::from_millis(150)) {
return;
}
time.update(now);
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::UP => {
head_pos.y += 1;
}
Direction::DOWN => {
head_pos.y -= 1;
}
Direction::RIGHT => {
head_pos.x += 1;
}
Direction::LEFT => {
head_pos.x -= 1;
}
};
segment_positions
.iter()
.zip(segments.0.iter().skip(1))
.for_each(|(pos, segment)| {
*positions.get_mut(*segment).unwrap() = *pos;
});
}
}
segments
だけでは中のSnakeSegments
にアクセスできないので,.0
を追加する.
これで体が頭についてくるようになった.
蛇が餌を食べるシステムを定義していく.
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 (ent, food_pos) in food_positions.iter() {
if food_pos == head_pos {
commands.entity(ent).despawn();
growth_writer.send(GrowthEvent);
}
}
}
}
そしてこれをApp
に追加
.add_systems(
Update,
(
snake_movement_input.before(snake_movement),
snake_movement,
snake_eating.after(snake_movement),
food_spawner,
),
)
GrouwthEvent
を定義してApp
に追加する.
#[derive(Event)]
struct GrowthEvent;
.add_event::<GrowthEvent>()
次に,一番後ろのしっぽの位置を記録するためにLastTailPosition
を定義する.
#[derive(Default, Resource)]
struct LastTailPosition(Option<Position>);
これをリソースとしてApp
に追加する.
.insert_resource(LastTailPosition::default())
蛇が動くたびにLastTailPosition
を変更するためにsnake_movement
を少し変更する.
fn snake_movement(
// ...
mut last_tail_position: ResMut<LastTailPosition>,
// ...
) {
// ...
if let Some((head_entity, head)) = heads.iter_mut().next() {
// ...
*last_tail_position = LastTailPosition(Some(*segment_positions.last().unwrap()))
}
}
このコード,「わざわざSome
で囲うんやったらunwrap
いらんやんけ!」と思ったけど,想定しているのがOption<Position>
で,そのまま渡してしまうとOption<&Position>
になってしまうからダメだった.
やっと,蛇が成長するシステムを定義していく.
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(),
Size::square(0.5),
));
}
}
EventReader
をイテレータとして返すread
があるのでこれを使用した.(というかこれでするしかない)
あといつも通りApp
に追加するだけ
.add_systems(
Update,
(
snake_movement_input.before(snake_movement),
snake_movement,
snake_eating.after(snake_movement),
snake_growth.after(snake_eating),
food_spawner,
),
)
最後に,ゲームオーバーを定義していく.
このゲームでは,「画面外に出てしまったとき」と「頭が体にあたったとき」にゲームオーバーとなる.まずはイベントを作成する.
#[derive(Event)]
struct GameOverEvent;
これをApp
に追加.
.add_event::<GameOverEvent>()
このイベントをsnake_movement
に付与していく.まず「画面外に出てしまったとき」の場合のみ書く.
fn snake_movement(
// ...
mut game_over_writer: EventWriter<GameOverEvent>,
// ...
) {
// ...
if let Some((head_entity, head)) = heads.iter_mut().next() {
// ...
match &head.direction {
// ...
};
if head_pos.x < 0
|| head_pos.y < 0
|| head_pos.x as u32 >= ARENA_WIDTH
|| head_pos.y as u32 >= ARENA_HEIGHT
{
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 ent in food.iter().chain(segments.iter()) {
commands.entity(ent).despawn();
}
spawn_snake(commands, segments_res);
}
}
ゲームオーバーイベントを受け取るとすべての餌と蛇の体を消して蛇を初期位置に配置している.
ここで,spawn_snake
をシステムとしてではなく,ただの関数として使用していることに注目してほしいらしい.
次に,「頭が体にあたったとき」のコードを書いていく.
fn snake_movement(
// ...
) {
// ...
if let Some((head_entity, head)) = heads.iter_mut().next() {
// ...
if head_pos.x < 0
|| head_pos.y < 0
|| head_pos.x as u32 >= ARENA_WIDTH
|| head_pos.y as u32 >= ARENA_HEIGHT
{
game_over_writer.send(GameOverEvent);
}
if segment_positions.contains(&head_pos) {
game_over_writer.send(GameOverEvent);
}
// ...
}
}
最後にgame_over
システムをApp
に追加して終了.
.add_systems(
Update,
(
snake_movement_input.before(snake_movement),
snake_movement,
snake_eating.after(snake_movement),
snake_growth.after(snake_eating),
game_over.after(snake_movement), // <--
food_spawner,
),
)
これで実行すると,スネークゲームができた.
汎用性を上げるために色々変更したが,最終的には以下のようになった.
すべてのコード
use std::{
marker::PhantomData,
time::{Duration, Instant},
};
use bevy::{core_pipeline::bloom::BloomSettings, prelude::*};
use rand::random;
const INITIAL_WIDTH: f32 = 1280.;
const INITIAL_HEIGHT: f32 = 720.;
const SNAKE_HEAD_COLOR: Color = Color::linear_rgb(0.7, 0.7, 0.7);
const SNAKE_SEGMENT_COLOR: Color = Color::linear_rgb(0.3, 0.3, 0.3);
const FOOD_COLOR: Color = Color::linear_rgb(1.0, 0.0, 1.0);
const ARENA_WIDTH: u32 = 30;
const ARENA_HEIGHT: u32 = 30;
fn in_arena(x: i32, y: i32) -> bool {
0 <= x && x < ARENA_WIDTH as i32 && 0 <= y && y < ARENA_HEIGHT as i32
}
#[derive(Component, Clone, Copy, PartialEq, Eq)]
struct Position {
x: i32,
y: i32,
}
#[derive(Component)]
struct Size {
width: f32,
height: f32,
}
impl Size {
pub fn square(x: f32) -> Self {
Self {
width: x,
height: x,
}
}
}
#[derive(PartialEq, Clone, Copy)]
enum Direction {
UP,
DOWN,
LEFT,
RIGHT,
}
impl Direction {
fn opposite(self) -> Self {
match self {
Self::UP => Self::DOWN,
Self::DOWN => Self::UP,
Self::LEFT => Self::RIGHT,
Self::RIGHT => Self::LEFT,
}
}
}
#[derive(Resource)]
struct IntervalSetting<T> {
last_update: Instant,
phantom: PhantomData<T>,
}
impl<T> Default for IntervalSetting<T> {
fn default() -> Self {
Self {
last_update: Instant::now(),
phantom: PhantomData,
}
}
}
impl<T> IntervalSetting<T> {
fn check(&self, time: Instant, diff: Duration) -> bool {
time - self.last_update >= diff
}
fn update(&mut self, time: Instant) {
self.last_update = time;
}
}
#[derive(Component)]
struct SnakeHead {
direction: Direction,
}
#[derive(Component)]
struct SnakeSegment;
#[derive(Default, Resource)]
struct SnakeSegments(Vec<Entity>);
#[derive(Default, Resource)]
struct LastTailPosition(Option<Position>);
#[derive(Component)]
struct Food;
#[derive(Event)]
struct GrowthEvent;
#[derive(Event)]
struct GameOverEvent;
fn main() {
App::new()
.add_systems(Startup, (setup_camera, spawn_snake))
.add_systems(
Update,
(
snake_movement_input.before(snake_movement),
snake_movement,
snake_eating.after(snake_movement),
snake_growth.after(snake_eating),
game_over.after(snake_movement),
food_spawner,
),
)
.add_systems(PostUpdate, (position_translation, size_scaling))
.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "snake-game".into(),
name: Some("snake.app".into()),
resolution: (INITIAL_WIDTH, INITIAL_HEIGHT).into(),
enabled_buttons: bevy::window::EnabledButtons {
maximize: false,
..default()
},
..default()
}),
..default()
}))
.insert_resource(SnakeSegments::default())
.insert_resource(LastTailPosition::default())
.insert_resource(ClearColor(Color::linear_rgb(0.04, 0.04, 0.04)))
.insert_resource(IntervalSetting::<SnakeHead>::default())
.insert_resource(IntervalSetting::<Food>::default())
.add_event::<GrowthEvent>()
.add_event::<GameOverEvent>()
.run();
}
fn setup_camera(mut commands: Commands) {
commands.spawn((
Camera2dBundle {
camera: Camera {
hdr: true,
..default()
},
..default()
},
BloomSettings::NATURAL,
));
}
fn size_scaling(window: Query<&Window>, mut q: Query<(&Size, &mut Transform)>) {
let window = window.single();
let window_size = window.width().min(window.height());
for (sprite_size, mut transform) in q.iter_mut() {
transform.scale = Vec3::new(
sprite_size.width / ARENA_WIDTH as f32 * window_size,
sprite_size.height / ARENA_HEIGHT as f32 * window_size,
1.0,
);
}
}
fn position_translation(window: Query<&Window>, mut q: 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.) + (tile_size / 2.)
}
let window = window.single();
for (pos, mut transform) in q.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,
);
}
}
fn spawn_snake(mut commands: Commands, mut segments: ResMut<SnakeSegments>) {
*segments = SnakeSegments(vec![
commands
.spawn(SpriteBundle {
sprite: Sprite {
color: SNAKE_HEAD_COLOR,
..default()
},
transform: Transform {
scale: Vec3::new(10.0, 10.0, 10.0),
..default()
},
..default()
})
.insert(SnakeHead {
direction: Direction::UP,
})
.insert(SnakeSegment)
.insert(Position { x: 3, y: 3 })
.insert(Size::square(0.8))
.id(),
spawn_segment(&mut commands, Position { x: 3, y: 2 }, Size::square(0.5)),
]);
}
fn spawn_segment(commands: &mut Commands, position: Position, scale: Size) -> Entity {
commands
.spawn(SpriteBundle {
sprite: Sprite {
color: SNAKE_SEGMENT_COLOR,
..default()
},
..default()
})
.insert(SnakeSegment)
.insert(position)
.insert(scale)
.id()
}
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: Direction = if keyboard_input.pressed(KeyCode::ArrowUp) {
Direction::UP
} else if keyboard_input.pressed(KeyCode::ArrowDown) {
Direction::DOWN
} else if keyboard_input.pressed(KeyCode::ArrowRight) {
Direction::RIGHT
} else if keyboard_input.pressed(KeyCode::ArrowLeft) {
Direction::LEFT
} else {
head.direction
};
if dir != head.direction.opposite() {
head.direction = dir;
}
}
}
fn snake_movement(
segments: ResMut<SnakeSegments>,
mut heads: Query<(Entity, &SnakeHead)>,
mut positions: Query<&mut Position>,
mut last_tail_position: ResMut<LastTailPosition>,
mut game_over_writer: EventWriter<GameOverEvent>,
mut time: ResMut<IntervalSetting<SnakeHead>>,
) {
let now = Instant::now();
if !time.check(now, Duration::from_millis(150)) {
return;
}
time.update(now);
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::UP => {
head_pos.y += 1;
}
Direction::DOWN => {
head_pos.y -= 1;
}
Direction::RIGHT => {
head_pos.x += 1;
}
Direction::LEFT => {
head_pos.x -= 1;
}
};
if !in_arena(head_pos.x, head_pos.y) {
game_over_writer.send(GameOverEvent);
}
if segment_positions.contains(&head_pos) {
game_over_writer.send(GameOverEvent);
}
segment_positions
.iter()
.zip(segments.0.iter().skip(1))
.for_each(|(pos, segment)| {
*positions.get_mut(*segment).unwrap() = *pos;
});
*last_tail_position = LastTailPosition(Some(*segment_positions.last().unwrap()))
}
}
fn food_spawner(mut commands: Commands, mut time: ResMut<IntervalSetting<Food>>) {
let now = Instant::now();
if !time.check(now, Duration::from_secs(1)) {
return;
}
time.update(now);
commands
.spawn(SpriteBundle {
sprite: Sprite {
color: FOOD_COLOR,
..default()
},
..default()
})
.insert(Food)
.insert(Position {
x: (random::<f32>() * ARENA_WIDTH as f32) as i32,
y: (random::<f32>() * ARENA_HEIGHT as f32) as i32,
})
.insert(Size::square(0.8));
}
fn snake_eating(
mut commands: Commands,
mut growth_writer: EventWriter<GrowthEvent>,
food_position: Query<(Entity, &Position), With<Food>>,
head_position: Query<&Position, With<SnakeHead>>,
) {
for head_pos in head_position.iter() {
for (ent, food_pos) in food_position.iter() {
if food_pos == head_pos {
commands.entity(ent).despawn();
growth_writer.send(GrowthEvent);
}
}
}
}
fn snake_growth(
mut 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(
&mut commands,
last_tail_position.0.unwrap(),
Size::square(0.5),
));
}
}
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 ent in food.iter().chain(segments.iter()) {
commands.entity(ent).despawn();
}
spawn_snake(commands, segments_res);
}
}
知らない間にbevyのバージョンが0.15.0に上がっていたので,またまた変更を追加していく.
コードを見ると,Camera2dBundle
とSpriteBundle
がDeprecatedになったようなので,ここを変更していく.
ドキュメントを見ると,Camera2dBundle
には
👎Deprecated since 0.15.0: Use the Camera2d component instead. Inserting it will now also insert the other components required by it automatically.
SpriteBundle
には
👎Deprecated since 0.15.0: Use the Sprite component instead. Inserting it will now also insert Transform and Visibility automatically.
と書かれてあるので,これに従っていく.
まずCamera2dBundle
を変更する.
fn setup_camera(mut commands: Commands) {
commands.spawn(Camera2d);
}
Camera2d
を挿入するだけでそのほか必要なコンポーネントも自動的に挿入されるため,ここまで簡素にまとまった.
BloomSettings
もバージョン0.15.0からDeprecatedになっており,Bloom
に変更するように言われているが,まずコードから消えるため言及していない.
次に,SpriteBundle
を変更していく.
.spawn(Sprite {
color: SNAKE_SEGMENT_COLOR,
..default()
})
Sprite
を挿入するだけでTransform
もVisibility
も自動的に挿入されるため,Transform
について記述する必要がなくなった.