Rust Bevy を使ってみたメモ テトリスを作ってみる⑥
移動と回転が実装されたので隙間なく積むことができるようになりました!!!
今回は回転を実装したのですが、けっこう手間取ってしまいました…ごり押しコードですね
コードを自分で見返してもぐちゃぐちゃだ…ごり押しみたいなコード…
ということで今回は
・テトリミノを回転させる
の
一本立てでやっていきます!
bevyのバージョン:0.13.0
rundのバージョン:0.8.5
参考:Rustゲームエンジンbevyでテトリスを作る | makibishi throw
List of all items in this crate (docs.rs)
目次
回転(反時計回り)
半時計周り回転を実装していきます
手順として、
1.回転移動予想地のポジションを求める
2.回転可能なら回転、不可能なら何もしない
3.落下時に回転情報をもとにポジションの補正
4.補正されたポジションで当たり判定とIsExsistの変更
といった感じでしょうか
ひとつずつ説明していきます
前提
回転角は見慣れた度数法(°)ではなく、弧度法(rad)を用いています
また、回転移動予想地やポジション補正地を求める際、回転行列を用いています
行列計算の詳細はここでは省きます…調べれば出てくると思いますが、けっこう難しい概念だと思うので…
まぁ、この手順を踏むと回転先のポジションが求められるんだな くらいの理解で大丈夫です
重要なところはそこではないので
回転移動予想地のポジションを求める
テトリミノを反時計回りに回転するとき、回転角は-90°になります
2×2の回転行列は
cosθ -sinθ
sinθ cosθ
で求められるので、
θ=-90°の回転行列は
0 1
-1 0
となります
予想地のポジションを(next_x, next_y)、元のポジションを(prev_x, prev_y)とすると
next_x = 0 x prev_x + 1 x prev_y
next_y = -1 x prev_x + 0 x prev_y
で求められます
0が掛けられている項を無視して計算すると
next_x = prev_y
next_y = -prev_x
で求められることがわかります
よって、これをrustで書くと以下のようになります
let next_pos_x: i32 = 1 * prev_position.1;
let next_pos_y: i32 = -1 * prev_position.0;
これで、回転移動予想地のポジションは求められました
回転可能かの判定と回転
回転移動予想地のポジションが求められたので回転可能かどうかの判定を行います
回転可能判定条件として、
テトリミノの各ブロック(テトリミノフラグメント)の回転移動予想地において
Xポジションが0以上9以下である
Yポジションが0以上である(テトリスフィールド内)
回転移動予想地にブロックが存在していない
の3つがすべて満たされる場合、回転ができるということになります
ここでは、回転不可能の場合、フラグを立ててループを脱出したいので、上の条件の逆が判定条件ということになりますね
よって、以上のことをrustで書くと以下のようになります
for prev_position in &prev_positions {
let next_pos_x: i32 = 1 * prev_position.1;
let next_pos_y: i32 = -1 * prev_position.0;
if next_pos_x + pos.x < 0 || next_pos_x + pos.x > 9 || next_pos_y + pos.y < 0 ||
is_exsist.0[(next_pos_x + pos.x) as usize][(next_pos_y + pos.y) as usize]
{
flag = true;
break;
}
}
前回のtetrimino_movementファンクション内に書いているので、prev_positionsを使っています
これで、回転ができない場合にフラグを立ててループを脱出することができました
フラグが立っているかどうかで回転するかしないかを分岐させることができます
回転を実装する方法として、Sprite中のTransformのrotationを変更するという方法があります
よって以上のことをrustで書くと以下のようになります
for prev_position in &prev_positions {
//省略
}
if flag {
println!("衝突!");
}else {
transform.rotate_z(-PI/2.0);//Z軸を中心として-π/2回転(ラジアン表記)
}
これで、反時計回りに90°回転するプログラミングができました
注意点として、transformをqueryに登録しておく必要があります
また、定数 PI(π)を使っているので、これもuseに追加しておきます
これらは、前回書いてなかったので新たに追加します
use std::f32::consts::PI;
fn tetrimino_movement(
mut tetrimino_query: Query<(&mut Transform/*追加*/, &mut Position, &Children), (With<Tetrimino>, With<Active>, Without<TetriminoFlagment>)>,
)
{
tetrimino_query
.iter_mut()
.for_each(|(mut transform/*追加*/, mut pos, children)| {
}
}
落下時に回転情報をもとにポジションの補正
回転が実装できたので、次に落下判定等を行いたいのですが、Transformのrotationで回転を行った場合、子オブジェクトのTransformが変化しません…
これが何を意味しているかというと、そのまま実行すると
上の画像のように、実際の当たり判定と表示されているブロックの当たり判定がずれてしまうということです
そこで、テトリミノの回転情報をもとにポジションを補正する必要があります
テトリミノの回転情報は
let rotate_axis: f32 = transform.rotation.to_scaled_axis()[2];
上のコードで得ることができます(rad)
あとはここで得られた値によって場合分けをするだけですね
回転した値を求める方法として、先ほどと同様に回転行列を用います
θ=180°の場合
-1 0
0 -1
θ=90°の場合
0 -1
1 0
となります
よって、以上のことをまとめてrustで書くと以下のようになります
if rotate_axis == PI/2.0 || rotate_axis == -3.0*PI/2.0 {
//90°回転
adjust_child_x = -1 * child_y;
adjust_child_y = 1 * child_x;
}else if rotate_axis == PI || rotate_axis == -PI {
//180°回転
adjust_child_x = -1 * child_x;
adjust_child_y = -1 * child_y;
}else if rotate_axis == 3.0*PI/2.0 || rotate_axis == -PI/2.0 {
//-90°回転
adjust_child_x = 1 * child_y;
adjust_child_y = -1 * child_x;
}else {
//回転なし
adjust_child_x = child_x;
adjust_child_y = child_y;
}
補正されたポジションで当たり判定とIsExsistの変更
最後に補正されたポジションで当たり判定とIsExsistの変更を行っていきます
当たり判定は移動可能か、回転可能か、落下しているかを判定をまとめて呼んだものです
いままでchild_xとchild_yを使っていた部分をadjust_child_xとadjust_child_yに変更するだけですね
//落下判定
if adjust_child_y + pos.y == 0 || is_exsist.0[(adjust_child_x + pos.x) as usize][(adjust_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;
}
//IsExsitの変更
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;
let adjust_child_x: i32;
let adjust_child_y: i32;
if rotate_axis == PI/2.0 || rotate_axis == -3.0*PI/2.0 {
//省略
}
is_exsist.0[(pos.x + adjust_child_x) as usize][(pos.y + adjust_child_y) as usize] = true;
}
} else {
pos.y -= 1;
}
//移動判定
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;
let adjust_child_x: i32;
let adjust_child_y: i32;
if rotate_axis == PI/2.0 || rotate_axis == -3.0*PI/2.0 {
//省略
}
prev_positions.push((adjust_child_x, adjust_child_y));
}
//回転判定
//省略
これで-90°の回転が実装できました
最後にKeyAをこの操作に対応させます
if key_input.pressed(KeyCode::KeyA) {
for prev_position in &prev_positions {
let next_pos_x: i32 = 1 * prev_position.1;
let next_pos_y: i32 = -1 * prev_position.0;
if next_pos_x + pos.x < 0 || next_pos_x + pos.x > 9 || next_pos_y + pos.y < 0 ||
is_exsist.0[(next_pos_x + pos.x) as usize][(next_pos_y + pos.y) as usize]
{
flag = true;
break;
}
println!("prev:({}, {}), next({}, {})", prev_position.0, prev_position.1, next_pos_x, next_pos_y);
}
if flag {
println!("衝突!");
}else {
transform.rotate_z(-PI/2.0);
}
}
これで完了です
回転(時計回り)
半時計周りの回転とほぼ同じです
回転行列が
0 -1
1 0
に変わっただけです
当たり判定等は上で実装済みなので、KeyDをこの操作に対応させます
if key_input.pressed(KeyCode::KeyD) {
for prev_position in &prev_positions {
let next_pos_x: i32 = -1 * prev_position.1;
let next_pos_y: i32 = 1 * prev_position.0;
if next_pos_x + pos.x < 0 || next_pos_x + pos.x > 9 || next_pos_y + pos.y < 0 ||
is_exsist.0[(next_pos_x + pos.x) as usize][(next_pos_y + pos.y) as usize]
{
flag = true;
break;
}
println!("prev:({}, {}), next({}, {})", prev_position.0, prev_position.1, next_pos_x, next_pos_y);
}
println!("rotation : {:?}", transform.rotation.to_scaled_axis()[2]);
if flag {
println!("衝突!");
}else {
transform.rotate_z(PI/2.0);
}
}
まとめ
回転の実装はこんな感じでしょうか…回転行列については参考サイト様のやり方を参考に(パクり)させていただきました
あんな方法思いつかんて…
目次
最後に今回変更した部分の全コードを載せておきます(デバック用のプリントもそのままです)
//テトリミノの落下と衝突判定
fn tetrimino_fall(
mut commands: Commands,
time: Res<Time>,
mut game_timer: ResMut<GameTimer>,
mut tetrimino_query: Query<(Entity, &mut Position, &Children, &Transform), (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, transform)| {
let rotate_axis: f32 = transform.rotation.to_scaled_axis()[2];
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;
let adjust_child_x: i32;
let adjust_child_y: i32;
if rotate_axis == PI/2.0 || rotate_axis == -3.0*PI/2.0 {
//90
adjust_child_x = -1 * child_y;
adjust_child_y = 1 * child_x;
}else if rotate_axis == PI || rotate_axis == -PI {
//180
adjust_child_x = -1 * child_x;
adjust_child_y = -1 * child_y;
}else if rotate_axis == 3.0*PI/2.0 || rotate_axis == -PI/2.0 {
//-90
adjust_child_x = 1 * child_y;
adjust_child_y = -1 * child_x;
}else {
adjust_child_x = child_x;
adjust_child_y = child_y;
}
if adjust_child_y + pos.y == 0 || is_exsist.0[(adjust_child_x + pos.x) as usize][(adjust_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;
let adjust_child_x: i32;
let adjust_child_y: i32;
if rotate_axis == PI/2.0 || rotate_axis == -3.0*PI/2.0 {
//90
adjust_child_x = -1 * child_y;
adjust_child_y = 1 * child_x;
println!("90");
}else if rotate_axis == PI || rotate_axis == -PI {
//180
adjust_child_x = -1 * child_x;
adjust_child_y = -1 * child_y;
println!("180");
}else if rotate_axis == 3.0*PI/2.0 || rotate_axis == -PI/2.0 {
//-90
adjust_child_x = 1 * child_y;
adjust_child_y = -1 * child_x;
println!("-90");
}else {
adjust_child_x = child_x;
adjust_child_y = child_y;
println!("0");
}
is_exsist.0[(pos.x + adjust_child_x) as usize][(pos.y + adjust_child_y) as usize] = true;
}
} else {
pos.y -= 1;
}
});
}
}
//入力を受けとってテトリミノを移動と回転
fn tetrimino_movememt (
time: Res<Time>,
mut game_timer: ResMut<InputCoolTime>,
mut tetrimino_query: Query<(&mut Transform, &mut Position, &Children), (With<Tetrimino>, With<Active>, Without<TetriminoFlagment>)>,
tetrimino_flagment_query: Query<&Transform, (With<TetriminoFlagment>, Without<Tetrimino>)>,
is_exsist: ResMut<IsExsist>,
key_input: Res<ButtonInput<KeyCode>>
) {
let mut prev_positions: Vec<(i32, i32)> = vec![];
let mut flag: bool = false;
if game_timer.0.tick(time.delta()).just_finished() {
return;
}
if game_timer.0.tick(time.delta()).just_finished() {
tetrimino_query
.iter_mut()
.for_each(|(mut transform, mut pos, children)| {
//回転角
let rotate_axis: f32 = transform.rotation.to_scaled_axis()[2];
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;
let adjust_child_x: i32;
let adjust_child_y: i32;
if rotate_axis == PI/2.0 || rotate_axis == -3.0*PI/2.0 {
//90
adjust_child_x = -1 * child_y;
adjust_child_y = 1 * child_x;
}else if rotate_axis == PI || rotate_axis == -PI {
//180
adjust_child_x = -1 * child_x;
adjust_child_y = -1 * child_y;
}else if rotate_axis == 3.0*PI/2.0 || rotate_axis == -PI/2.0 {
//-90
adjust_child_x = 1 * child_y;
adjust_child_y = -1 * child_x;
}else {
adjust_child_x = child_x;
adjust_child_y = child_y;
}
prev_positions.push((adjust_child_x, adjust_child_y));
}
if key_input.pressed(KeyCode::ArrowLeft) {
for prev_position in &prev_positions {
if prev_position.0 + pos.x == 0 ||
is_exsist.0[(prev_position.0 + pos.x - 1) as usize][(prev_position.1 + pos.y) as usize] {
flag = true;
}
}
if flag {
println!("衝突!");
}else {
println!("左移動");
pos.x -= 1;
}
}else if key_input.pressed(KeyCode::ArrowRight) {
for prev_position in &prev_positions {
if prev_position.0 + pos.x == 9 ||
is_exsist.0[(prev_position.0 + pos.x + 1) as usize][(prev_position.1 + pos.y) as usize] {
flag = true;
}
}
if flag {
println!("衝突!");
}else {
println!("右移動");
pos.x += 1;
}
}else if key_input.pressed(KeyCode::ArrowDown) {
for prev_position in &prev_positions {
if prev_position.1 + pos.y == 0 ||
is_exsist.0[(prev_position.0 + pos.x) as usize][(prev_position.1 + pos.y - 1) as usize] {
flag = true;
}
}
if flag {
println!("衝突!");
}else {
println!("下移動");
pos.y -= 1;
}
}else if key_input.pressed(KeyCode::ArrowUp) {
for _ in 0..20 {
for prev_position in &prev_positions {
if prev_position.1 + pos.y == 0 ||
is_exsist.0[(prev_position.0 + pos.x) as usize][(prev_position.1 + pos.y - 1) as usize] {
flag = true;
}
}
if flag {
println!("衝突!");
break;
}else {
pos.y -= 1;
}
}
}else if key_input.pressed(KeyCode::KeyA) {
for prev_position in &prev_positions {
let next_pos_x: i32 = 1 * prev_position.1;
let next_pos_y: i32 = -1 * prev_position.0;
if next_pos_x + pos.x < 0 || next_pos_x + pos.x > 9 || next_pos_y + pos.y < 0 ||
is_exsist.0[(next_pos_x + pos.x) as usize][(next_pos_y + pos.y) as usize]
{
flag = true;
break;
}
println!("prev:({}, {}), next({}, {})", prev_position.0, prev_position.1, next_pos_x, next_pos_y);
}
println!("rotation : {:?}", transform.rotation.to_scaled_axis()[2]);
println!("参考:(90:{}, 180:{}, -90:{})", PI/2.0, PI, -PI/2.0);
if flag {
println!("衝突!");
}else {
transform.rotate_z(-PI/2.0);
}
}else if key_input.pressed(KeyCode::KeyD) {
for prev_position in &prev_positions {
let next_pos_x: i32 = -1 * prev_position.1;
let next_pos_y: i32 = 1 * prev_position.0;
if next_pos_x + pos.x < 0 || next_pos_x + pos.x > 9 || next_pos_y + pos.y < 0 ||
is_exsist.0[(next_pos_x + pos.x) as usize][(next_pos_y + pos.y) as usize]
{
flag = true;
break;
}
println!("prev:({}, {}), next({}, {})", prev_position.0, prev_position.1, next_pos_x, next_pos_y);
}
println!("rotation : {:?}", transform.rotation.to_scaled_axis()[2]);
if flag {
println!("衝突!");
}else {
transform.rotate_z(PI/2.0);
}
}
});
}
}
・
・
・
雑記
今回の回転の実装は思ったよりすんなりできました…もっと手間取ると思ってた
まぁ予想外だったことは回転したオブジェクトの子オブジェクトのTransformが変化しないってことでしょうか…あれのせいでコードがめちゃ長くなってしまった…多分ある程度まとめてかけるんですけどね
次回はいよいよ列の消去を実装していきます
これができればテトリスが完成したといってもいいのではないかな??
ホントはこれに加えて、スコアの計算とかメニューの表示とか次のテトリミノの表示とかを実装したいんですけどねぇ
あとは、落下中に回転や移動ができないこととか回転と移動を同時にできないこととか一度のキー入力で2回分の移動や回転をしたりとか生成されるテトリミノが完全ランダムなところとかいろいろ改善するべきところがあるんですよね
まぁここら辺はやる気があれば実装すると思います…今のところ一部はやる気ある
次回更新もなるべく早くできるように頑張ります………