Rust Bevy を使ってみたメモ テトリスを作ってみる④
前回書いた、テトリミノが自重でつぶれる問題を解決できたのでブログを更新していきます!!!!!
いやー長い戦いでしたね…3日くらい悩んでたんじゃないでしょうか
問題を解決した方法はずばり、親子関係を使う!ですね。
これは、Unityとかを触ったことがある人はわかると思うんですけど、親オブジェクト、子オブジェクトという概念がありまして、いい例が思い浮かびませんがまぁ依存関係みたいなものです(適当)
前回はどこで終わったかというと一定時間ごとにテトリミノが落下するところまでやったっぽいですね
とりあえず順番に整理していきたいと思います
よって、今回は
・テトリミノの落下を判定する
の一本立てでやっていきます!え?一本なら書く必要な言って?いやいいじゃんテンプレを作りたいんだよ…!
bevyのバージョン:0.13.0
rundのバージョン:0.8.5
参考:Rustゲームエンジンbevyでテトリスを作る | makibishi throw
List of all items in this crate (docs.rs)
目次
テトリミノの落下を判定する
テトリミノの落下を判定するにはどうするか。これを考えるときはまずテトリミノがどうなれば落下したことになるのかを考えるといいと思います。
テトリミノが落下するのは地面(フィールドの一番下)についたときか、すでに存在するテトリミノに衝突した時の2パターンがあると思います。
というか基本的な考え方は Rustゲームエンジンbevyでテトリスを作る | makibishi throw このサイトの方を参考にしているので、胸を張っていうことでもないのですが…
落下判定をする手順として
1.ゲームフィールドの位置ごとにテトリミノが存在するかどうかを記憶する構造体を用意する
2.上で作成した構造体と、次の位置を比較し、落下できれば落下、できなければ次のテトリミノを生成する
という感じですかね?文字で書くととってもわかりにくいです
ゲームフィールド構造体を作成する
これをなんのために作るかというと、ゲームフィールドのどこに過去のテトリミノが存在しているかを判定するためです
これがないと、落下判定が地面に落ちたときのみとなり、ゲームとして成り立たなくなります
まぁ、構造体を作ること自体は簡単で
//ゲームフィールド中のどの位置にテトリミノが存在しているか
#[derive(Resource)]
struct IsExsist (Vec<Vec<bool>>);
fn main() {
App::new()
//省略
.insert_resource(IsExsist(vec![vec![false; 20]; 10]))
//省略
.run()
}
二次元のVec型を使えばよさそうですね
テトリミノが存在しているかどうかはbool値で表します。
Appに登録するのを忘れずに!
なんか0~20までのVecな気がするけど…まぁいっか!
ここで、なぜリソースの派生にしているかは、おそらく読み取りやすさでしょう!
これは参考サイト様のを丸パクリです…
落下を判定する
次に落下を判定していきます
落下の判定は、
前回作成したtetorimino_fall関数で行うといいと思います
落下判定自体は if文で行えばいいので、そんなに難しくないと思います
とりあえず最終形態を
//テトリミノの落下と衝突判定
fn tetrimino_fall(
mut commands: Commands,
time: Res<Time>,
mut game_timer: ResMut<GameTimer>,
mut tetrimino_query: Query<(Entity, &mut Position, &Children), (With<Tetrimino>, With<Active>)>,
tetrimino_flagment_query: Query<&Transform, With<TetriminoFlagment>>,
mut spawn_tetrimino_event_writer: EventWriter<SpawnTetriminoEvent>,
//とりあえずココ!を見てください!
mut is_exsist: ResMut<IsExsist>,
) {
let mut flag: bool = false;
if game_timer.0.tick(time.delta()).just_finished() {
tetrimino_query
.iter_mut()
.for_each(|(entity, mut pos, children)| {
//ココは後で解説します…
for &child in children.iter() {
println!("{:?}", tetrimino_flagment_query.get(child).unwrap().translation[1]);
let child_x: i32 = tetrimino_flagment_query.get(child).unwrap().translation[0] as i32;
let child_y: i32 = tetrimino_flagment_query.get(child).unwrap().translation[1] as i32;
//とりあえずココ!を見てください!
if child_y + pos.y == 0 || is_exsist.0[(child_x + pos.x) as usize][(child_y + pos.y - 1) as usize] {
println!("落下できません\n新しいテトリミノを生成します");
commands.entity(entity).remove::<Active>();//後で解説します…
spawn_tetrimino_event_writer.send(SpawnTetriminoEvent);//イベントの送信
flag = true;
break;
} else {
continue;
}
}
//ココは後で解説します…
if flag {
for &child in children.iter() {
let child_x: i32 = tetrimino_flagment_query.get(child).unwrap().translation[0] as i32;
let child_y: i32 = tetrimino_flagment_query.get(child).unwrap().translation[1] as i32;
is_exsist.0[(pos.x + child_x) as usize][(pos.y + child_y) as usize] = true;
}
} else {
pos.y -= 1;//何もなかったら落下
}
});
}
}
うわー全然優しくない…
とりあえず、IsExsist はフィールド上のテトリミノの有無を記憶します
そして、if文でテトリミノが地面にあるか、はたまた他のテトリミノの上にあるかを判定している感じですね
地面に落ちたときor他のテトリミノの上にあるときは新しいテトリミノを生成するために、イベントを送信します
そうじゃないときは落下を行うという具合ですかね…上のコードは最終形態なのでめちゃくちゃ煩雑になっています
まぁ、コードがわからなくても理屈がわかれば大丈夫でしょう!私自身他人が書いたコード読むの苦手ですし…
上の状態でコンパイルをすると、前回の記事に書いたみたいにテトリミノが崩れます
理由は前回の記事を見てください。いややっぱ見なくていいです…
.
ということで、前回の記事で書いたように以前のコードをそのまま流用すると、正方形ひとつひとつに落下判定が発生してしまうわけですね。これが何を引き起こすかなんですが、少し放置するとすごい量の処理が同時に発生します。ひとつのテトリミノに対して4つの正方形を使っているので4回落下判定が発生するわけです。落下判定に合わせてテトリミノを生成しているので、えぐい量になるのはドラえもんのバイバイン回を見たことのある人ならお判りでしょう。
それを解決するのにとても手間取ったわけですね
コードを修正する
コードの修正も何も自分が招いた結果なわけで、頑張って自分の尻拭いをしていきます
修正点①:テトリミノバンドル
まず手始めにテトリミノバンドルを修正していきます
今のままだと、テトリミノの正方形ひとつひとつがテトリミノバンドルという目印を持っている上に、落下し終わったはずのテトリミノが永遠と落下判定を送ることになるのでテトリミノが無限に生成されます
それを修正するために親子関係と新たな目印”Active”を取り入れます
//テトリミノの目印
#[derive(Component)]
struct Tetrimino;
//テトリミノフラグメントの目印
#[derive(Component)]
struct TetriminoFlagment;
//テトリミノがアクティブかどうかの目印
#[derive(Component)]
struct Active;
//テトリミノバンドル
//親オブジェクト(基準ブロック)
#[derive(Bundle)]
struct TetriminoBundle {
name : Tetrimino,
sprite : SpriteBundle,
position : Position,
active : Active,
}
//子オブジェクト
#[derive(Bundle)]
struct TetriminoFlagmentBundle {
name : TetriminoFlagment,
sprite : SpriteBundle,
}
ここはだいぶ迷走しました…まぁ紆余曲折ありこの形に落ち着いたという感じですかね
修正点②:spawn_tetrimino関数
テトリミノバンドルを変更したので、それに伴ってspawn_tetrimino関数も修正します
//テトリミノの生成
fn spawn_tetrimino(
mut commands: Commands,
tetrimino_color_and_shape: Res<TetriminoColorAndShape>,
mut fancy: Option<Res<TetriminoColorAndShape>>,
mut spawn_tetrimino_event_reader: EventReader<SpawnTetriminoEvent>,//イベントリーダー(イベントを感知する)
){
//イベントを感知
for _ in spawn_tetrimino_event_reader.read() {
//乱数を生成
let mut rng = rand::thread_rng();
let i: u8 = rng.gen();
let index: usize = (i % 7) as usize;
//テトリミノを生成
if let Some(_fancy) = &mut fancy {
commands.spawn(TetriminoBundle {
name : Tetrimino,
sprite : SpriteBundle {
sprite: Sprite {
color: tetrimino_color_and_shape.tetrimino[index].color.clone(),
..default()
},
transform: Transform {
scale: (
SQUARE_WIDTH as f32,
SQUARE_HEIGHT as f32,
0.0
).into(),
..default()
},
..default()
},
position : Position {
x: 5,
y: 10,
},
active : Active,
}).with_children(|child_builder| { //目玉ポイント!!!!
for shape in tetrimino_color_and_shape.tetrimino[index].shape.clone() {
child_builder.spawn(TetriminoFlagmentBundle {
name: TetriminoFlagment,
sprite: SpriteBundle {
sprite: Sprite {
color: tetrimino_color_and_shape.tetrimino[index].color.clone(),
..default()
},
transform: Transform {
translation: (
shape.0 as f32,
shape.1 as f32,
0.0).into(),
..default()
},
..default()
},
});
println!("({}, {})", shape.0, shape.1);
}
});
println!("spawn {} block", index);
}else {
println!("Err");
}
}
}
なげー
60行くらいありますね…まぁ目玉ポイントは.with_children以下の部分ですかね
.with_childrenとchild_builder.spawnを使って子オブジェクトを生成していきます。親オブジェクトは.with_childrenよりも手前の部分です
ちなみにTransformは相対位置なので親オブジェクトが基準になります
どうやら親オブジェクトの大きさにも影響されるっぽいです
修正点③:tetrimino_fall関数
最後にtetrimino_fall関数を修正していきます
//テトリミノの落下と衝突判定
fn tetrimino_fall(
mut commands: Commands,
time: Res<Time>,
mut game_timer: ResMut<GameTimer>,
mut tetrimino_query: Query<(Entity, &mut Position, &Children), (With<Tetrimino>, With<Active>)>,
//ココ!を見てください!
tetrimino_flagment_query: Query<&Transform, With<TetriminoFlagment>>,
mut spawn_tetrimino_event_writer: EventWriter<SpawnTetriminoEvent>,
mut is_exsist: ResMut<IsExsist>,
) {
let mut flag: bool = false;
if game_timer.0.tick(time.delta()).just_finished() {
tetrimino_query
.iter_mut()
.for_each(|(entity, mut pos, children)| {
//ココ!を見てください!
for &child in children.iter() {
println!("{:?}", tetrimino_flagment_query.get(child).unwrap().translation[1]);
let child_x: i32 = tetrimino_flagment_query.get(child).unwrap().translation[0] as i32;
let child_y: i32 = tetrimino_flagment_query.get(child).unwrap().translation[1] as i32;
if child_y + pos.y == 0 || is_exsist.0[(child_x + pos.x) as usize][(child_y + pos.y - 1) as usize] {
println!("落下できません\n新しいテトリミノを生成します");
commands.entity(entity).remove::<Active>();//ココ!を見てください!
spawn_tetrimino_event_writer.send(SpawnTetriminoEvent);//イベントの送信
flag = true;
break;
} else {
continue;
}
}
//ココ!を見てください!
if flag {
for &child in children.iter() {
let child_x: i32 = tetrimino_flagment_query.get(child).unwrap().translation[0] as i32;
let child_y: i32 = tetrimino_flagment_query.get(child).unwrap().translation[1] as i32;
is_exsist.0[(pos.x + child_x) as usize][(pos.y + child_y) as usize] = true;
}
} else {
pos.y -= 1;//何もなかったら落下
}
});
}
}
親子関係を導入したので、それに伴ってコードが変化しています
for &child in children.iter() の部分は親オブジェクトにchildrenというコンポーネントが新たに付いたので、そこから子オブジェクトの情報を得ている部分になります。
childrenコンポーネントにはエンティティIDの形で子オブジェクトの情報が入っているのでtetrimino_flagment_query.get(child)のような呼び出し方になっています
あとは、子オブジェクトひとつひとつで落下判定を判定し、ひとつでも落下している判定がでたらflagを立ててループを脱出します
flagが立っている場合、現在の子オブジェクトの位置のIsExsistをtrueにして処理終了です
修正の概要としてはこんな感じかな??
まとめ
今回はほとんどコードの修正で終わった気がするけど、理屈としては書いた通りですね
あと子オブジェクト、親オブジェクトって言ってるけどBevyでは子エンティティ、親エンティティって呼び方になるのかも??
使ってるファンクションの詳しい使い方はList of all items in this crate (docs.rs) を読んでくれとしか言えない…私自身、ちゃんと理解しているか怪しいです
失敗作
成功
今回はこんな感じかな??わからないところがあったらこの記事を見返すか、参考サイト様を見てくれ
目次
・
・
・
雑記
いやー前回フォントを変更できるようにするとか言ってた気がするけど多分気のせいだよね??
ちょっと調べた感じなんかめんどくさそうだったのでやめました
あと、Rustの実行でmain.rsしか選択できないのは少し不便ですね…ちょっとファンクションを試しに使ってみようと思ってもなかなか難しいものがありました。
この記事で書くほどでもなかったこととして、commands.spawn()でSpriteはスポーンできないっぽいですね。どうやらSpriteBundleじゃないとダメっぽいです
なんか他に書きたいことがあったような気がするけど…忘れたからいいや
そういえば、今日は閏日ですね。できれば2月中にテトリスを作り終わらせたかったんですけど無理でした…2月ちゃんは一日延ばしてくれたのに…
次回はどこまで進めるかな?キーボード操作の受け付けは比較的簡単に実装できそうだけど…回転…考えたくねぇ
おそらく今後、親子関係を追加した影響で苦しむと思うんですよね…多分列消去のところで…
次回もなるべく早く更新できるように頑張ります…!!!