C++でつくったテトリスの完成品
よく動画でテトリスを○分でつくってみたというのがありますが、鳩でもわかるC#管理人はそんなスキルはないので別の領域で勝負するしかありません。「テトリスを○分でつくってみた」はテトリスを完全再現したものではないものがほとんどです。なかでも今回の肝になるスーパーローテーションシステム(SRS)に関しては完全にスルーしているものが多いです。だから私としてはこの部分をキッチリ実装することで他との差別化をしたいのです。
Contents
スーパーローテーションシステム(SRS)とは
スーパーローテーションシステム(SRS)とは簡単に説明すると普通に回転させようとすると他のブロックにぶつかって回転できない場合、「回転できない」で終わらせず回転後の位置を平行移動させることで回転させることができるというものです。
またテトリスにはTスピントリプルという大技も存在します。これはSRSを利用して本来なら絶対に入らない場所にTミノをねじ込もうとするテクニックです。SRSがあるとゲームをより面白くすることができるのです。
SRSはテトリスのガイドラインにも組み込まれています。テトリミノがどのような状態のとき、どのように回転軸がずれるのかはガイドラインで決められているのです。やっぱりやるからにはこれに沿ったものを作りたいです。
このあたりをことを解説してくれているのがTetrisちゃんねるです。これまでC#,JavaScriptでテトリスをつくってきましたが、今回もこのサイトを参考にすることにします。
通常の回転処理
まずは通常の回転処理を考えますが、これなら前述のGetInitBlocksRotate33関数やGetInitBlocksRotate44関数を使えばできそうです。そこで最初に普通の方法で回転した場合のテトリミノの各ブロックがどうなるかを考えてみます。これがCanRotate関数です。第一引数は右回転であればTRUE、左回転ならFALSEです。
現在落下中のテトロミノをGetInitBlocksRotate33関数やGetInitBlocksRotate44関数で回転させればよいのですが、回転処理を加えたときの各ブロックのPositionを求める前にm_TetriminoPositionX == 0 m_TetriminoPositionY == 0の状態に戻さなければなりません。そして回転処理を加えたときのブロックのPositionを求めてm_TetriminoPositionX、m_TetriminoPositionY分平行移動されたときのPositionが回転処理後の位置ということになります。
回転処理後のPositionがわかったらあとは当たり判定をするだけです。回転することができるならTRUE、できないならFALSEを返します。最初はこの関数を第二引数、第三引数はともに0を渡して処理をします。これでSRSを適用しないでテトリミノを回転させることができるかの結果が得られます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
BOOL Tetris::CanRotate(BOOL isRight, int x, int y) { TetriminoAngle nextAngle; if(isRight) nextAngle = TetriminoAngle::Angle90; else nextAngle = TetriminoAngle::Angle270; TetriminoPosition tetri[4]; // m_TetriminoPositionX == 0 m_TetriminoPositionY == 0の状態に戻して考える GetTetriminosPosition(tetri, 4); for (int i = 0; i < 4; i++) { tetri[i].m_column -= m_TetriminoPositionX; tetri[i].m_row -= m_TetriminoPositionY; } if(m_CurTetriminoType != TetriminoTypes::I && m_CurTetriminoType != TetriminoTypes::O) GetInitBlocksRotate33(tetri, 4, nextAngle); else if (m_CurTetriminoType == TetriminoTypes::I) GetInitBlocksRotate44(tetri, 4, nextAngle); // SRSを適用した場合の回転軸のズレを加える for (int i = 0; i < 4; i++) { tetri[i].m_column += x; tetri[i].m_row += y; } // 現在のm_TetriminoPositionXとm_TetriminoPositionYの分だけ平行移動させる for (int i = 0; i < 4; i++) { tetri[i].m_column += m_TetriminoPositionX; tetri[i].m_row += m_TetriminoPositionY; } // これで回転後のブロックの座標が取得できた // 後は当たり判定をおこなう // フィールドの外に出たり、すでに固定されているブロックがある場合は移動・回転できない for (int i = 0; i < 4; i++) { if (tetri[i].m_column > COLUMN_MAX - 1) return FALSE; if (tetri[i].m_column < 0) return FALSE; if (tetri[i].m_row > ROW_MAX - 1) return FALSE; if (m_FixedBlocks[tetri[i].m_row][tetri[i].m_column] != NULL) return FALSE; } return TRUE; } |
テトリミノの回転は可能かを検証する
では実際に回転処理をおこなわせます。Oミノは回転処理をおこないません。回転させても形状が同じだからです。Iミノ以外のテトリミノの処理をRotateRight3関数でおこないます。IミノであればRotateRightI関数でおこないます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
BOOL Tetris::RotateRight() { BOOL ret = FALSE; if(m_CurTetriminoType != TetriminoTypes::I && m_CurTetriminoType != TetriminoTypes::O) { ret = RotateRight3(); if(ret) ResetTimer(); } else if (m_CurTetriminoType == TetriminoTypes::I) { ret = RotateRightI(); if (ret) ResetTimer(); } return ret; } |
Iミノとそれ以外ではSRS適用下の処理も異なっている
OミノとIミノ以外のテトリミノの回転処理をおこなう関数を示します。最初はCanRotate関数の第二第三引数には0を渡し、SRSは適用されない普通の回転ができないかを調べます。そして回転させることができるのであれば引数付きのRotateRight関数を呼び出して、実際に回転処理をおこないます。
CanRotate関数がFALSEを返した場合はSRS適用下での回転ができないかを調べます。どのようなルールが適用されるかは以下の画像をみてくださし。
これを見ると通常回転できないのであれば、
回転前の回転角が0度のときは
(1) 左に1マスずらす
(2) 左に1マス、上に1マスずらす
(3) 下に2マスずらす
(4) 左に1マス、下に2マスずらす
この順で調べてみてどれもダメな場合は回転処理はできないということになります。
回転前の回転角が0度以外のときもTetrisちゃんねるに掲載されている図にある順番で回転できるかどうかを調べていきます。
もし回転させたあとテトリミノ全体を平行移動させることで回転できるのであれば、平行移動させるべき値をRotateRightに渡して回転処理をおこないます。
するとOミノとIミノ以外のテトリミノの回転処理をおこなう関数は以下のようなものになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
BOOL Tetris::RotateRight3() { if (CanRotate(TRUE, 0, 0)) return RotateRight(0, 0); if (m_TetriminoAngle == TetriminoAngle::Angle0) { if (CanRotate(TRUE, -1, 0)) return RotateRight(-1, 0); if (CanRotate(TRUE, -1, -1)) return RotateRight(-1, -1); if (CanRotate(TRUE, 0, 2)) return RotateRight(0, 2); if (CanRotate(TRUE, -1, 2)) return RotateRight(-1, 2); } else if (m_TetriminoAngle == TetriminoAngle::Angle90) { if (CanRotate(TRUE, 1, 0)) return RotateRight(1, 0); if (CanRotate(TRUE, 1, 1)) return RotateRight(1, 1); if (CanRotate(TRUE, 0, -2)) return RotateRight(0, -2); if (CanRotate(TRUE, 1, -2)) return RotateRight(1, -2); } else if (m_TetriminoAngle == TetriminoAngle::Angle180) { if (CanRotate(TRUE, 1, 0)) return RotateRight(1, 0); if (CanRotate(TRUE, 1, -1)) return RotateRight(1, -1); if (CanRotate(TRUE, 0, 2)) return RotateRight(0, 2); if (CanRotate(TRUE, 1, 2)) return RotateRight(1, 2); } else if (m_TetriminoAngle == TetriminoAngle::Angle270) { if (CanRotate(TRUE, -2, 0)) return RotateRight(-2, 0); if (CanRotate(TRUE, -2, 1)) return RotateRight(-2, 1); if (CanRotate(TRUE, 0, -2)) return RotateRight(0, -2); if (CanRotate(TRUE, -1, -2)) return RotateRight(-1, -2); } return FALSE; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
BOOL Tetris::RotateRightI() { if (CanRotate(TRUE, 0, 0)) return RotateRight(0, 0); if (m_TetriminoAngle == TetriminoAngle::Angle0) { if (CanRotate(TRUE, -2, 0)) return RotateRight(-2, 0); if (CanRotate(TRUE, 1, 0)) return RotateRight(1, 0); if (CanRotate(TRUE, -2, 1)) return RotateRight(-2, 1); if (CanRotate(TRUE, 1, -2)) return RotateRight(1, -2); } else if (m_TetriminoAngle == TetriminoAngle::Angle90) { if (CanRotate(TRUE, -1, 0)) return RotateRight(-1, 0); if (CanRotate(TRUE, 2, 0)) return RotateRight(2, 0); if (CanRotate(TRUE, -1, -2)) return RotateRight(-1, -2); if (CanRotate(TRUE, 2, 1)) return RotateRight(2, 1); } else if (m_TetriminoAngle == TetriminoAngle::Angle180) { if (CanRotate(TRUE, 2, 0)) return RotateRight(2, 0); if (CanRotate(TRUE, -1, 0)) return RotateRight(-1, 0); if (CanRotate(TRUE, 2, -1)) return RotateRight(2, -1); if (CanRotate(TRUE, -1, 2)) return RotateRight(-1, 2); } else if (m_TetriminoAngle == TetriminoAngle::Angle270) { if (CanRotate(TRUE, -2, 0)) return RotateRight(-2, 0); if (CanRotate(TRUE, 1, 0)) return RotateRight(1, 0); if (CanRotate(TRUE, 1, 2)) return RotateRight(1, 2); if (CanRotate(TRUE, -2, -1)) return RotateRight(-2, -1); } return FALSE; } |
回転処理ができる場合のみ回転処理をおこなう
回転処理ができることがわかったら回転処理をおこないます。RotateRight関数の引数はSRS適用下での回転させる場合、どれだけテトリミノを平行移動させるかです。平行移動させるということはメンバ変数であるm_TetriminoPositionXとm_TetriminoPositionYも適切に変更させなければならないことを意味しています。
m_TetriminoAngleとm_TetriminoPositionX、m_TetriminoPositionYを書き換えればあとは描画処理の関数がうまくやってくれます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
BOOL Tetris::RotateRight(int x, int y) { m_TetriminoPositionX += x; m_TetriminoPositionY += y; if (m_TetriminoAngle == TetriminoAngle::Angle0) m_TetriminoAngle = TetriminoAngle::Angle90; else if (m_TetriminoAngle == TetriminoAngle::Angle90) m_TetriminoAngle = TetriminoAngle::Angle180; else if (m_TetriminoAngle == TetriminoAngle::Angle180) m_TetriminoAngle = TetriminoAngle::Angle270; else if (m_TetriminoAngle == TetriminoAngle::Angle270) m_TetriminoAngle = TetriminoAngle::Angle0; return TRUE; } |
左回りも同様に処理する
同様に左回りに対しても行ないます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
BOOL Tetris::RotateLeft() { BOOL ret = FALSE; if (m_CurTetriminoType != TetriminoTypes::I && m_CurTetriminoType != TetriminoTypes::O) { ret = RotateLeft3(); if(ret) ResetTimer(); } else if (m_CurTetriminoType == TetriminoTypes::I) { ret = RotateLeftI(); if (ret) ResetTimer(); } return ret; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
BOOL Tetris::RotateLeft3() { if (CanRotate(FALSE, 0, 0)) return RotateLeft(0, 0); if (m_TetriminoAngle == TetriminoAngle::Angle0) { if (CanRotate(FALSE, 1, 0)) return RotateLeft(1, 0); if (CanRotate(FALSE, 1, -1)) return RotateLeft(1, -1); if (CanRotate(FALSE, 0, 2)) return RotateLeft(0, 2); if (CanRotate(FALSE, 1, 2)) return RotateLeft(1, 2); } else if (m_TetriminoAngle == TetriminoAngle::Angle90) { if (CanRotate(FALSE, 1, 0)) return RotateLeft(1, 0); if (CanRotate(FALSE, 1, 1)) return RotateLeft(1, 1); if (CanRotate(FALSE, 0, -2)) return RotateLeft(0, -2); if (CanRotate(FALSE, 1, -2)) return RotateLeft(1, -2); } else if (m_TetriminoAngle == TetriminoAngle::Angle180) { if (CanRotate(FALSE, -1, 0)) return RotateLeft(-1, 0); if (CanRotate(FALSE, -1, -1)) return RotateLeft(-1, -1); if (CanRotate(FALSE, 0, 2)) return RotateLeft(0, 2); if (CanRotate(FALSE, -1, 2)) return RotateLeft(-1, 2); } else if (m_TetriminoAngle == TetriminoAngle::Angle270) { if (CanRotate(FALSE, -1, 0)) return RotateLeft(-1, 0); if (CanRotate(FALSE, -1, 1)) return RotateLeft(-1, 1); if (CanRotate(FALSE, 0, -2)) return RotateLeft(0, -2); if (CanRotate(FALSE, -1, -2)) return RotateLeft(-1, -2); } return FALSE; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
BOOL Tetris::RotateLeftI() { if (CanRotate(FALSE, 0, 0)) return RotateLeft(0, 0); if (m_TetriminoAngle == TetriminoAngle::Angle0) { if (CanRotate(FALSE, -1, 0)) return RotateLeft(-1, 0); if (CanRotate(FALSE, 2, 0)) return RotateLeft(2, 0); if (CanRotate(FALSE, -2, -2)) return RotateLeft(-2, -2); if (CanRotate(FALSE, 2, 1)) return RotateLeft(2, 1); } else if (m_TetriminoAngle == TetriminoAngle::Angle90) { if (CanRotate(FALSE, 2, 0)) return RotateLeft(2, 0); if (CanRotate(FALSE, -1, 0)) return RotateLeft(-1, 0); if (CanRotate(FALSE, 2, -1)) return RotateLeft(2, -1); if (CanRotate(FALSE, -1, 2)) return RotateLeft(-1, 2); } else if (m_TetriminoAngle == TetriminoAngle::Angle180) { if (CanRotate(FALSE, 1, 0)) return RotateLeft(1, 0); if (CanRotate(FALSE, -2, 0)) return RotateLeft(-2, 0); if (CanRotate(FALSE, 1, 2)) return RotateLeft(1, 2); if (CanRotate(FALSE, -2, -1)) return RotateLeft(-2, -1); } else if (m_TetriminoAngle == TetriminoAngle::Angle270) { if (CanRotate(FALSE, 1, 0)) return RotateLeft(1, 0); if (CanRotate(FALSE, -2, 0)) return RotateLeft(-2, 0); if (CanRotate(FALSE, -2, 1)) return RotateLeft(-2, 1); if (CanRotate(FALSE, 1, -2)) return RotateLeft(1, -2); } return FALSE; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
BOOL Tetris::RotateLeft(int x, int y) { m_TetriminoPositionX += x; m_TetriminoPositionY += y; if (m_TetriminoAngle == TetriminoAngle::Angle0) m_TetriminoAngle = TetriminoAngle::Angle270; else if (m_TetriminoAngle == TetriminoAngle::Angle90) m_TetriminoAngle = TetriminoAngle::Angle0; else if (m_TetriminoAngle == TetriminoAngle::Angle180) m_TetriminoAngle = TetriminoAngle::Angle90; else if (m_TetriminoAngle == TetriminoAngle::Angle270) m_TetriminoAngle = TetriminoAngle::Angle180; return TRUE; } |
これまでテトリミノの生成、描画、移動、回転、消去の処理をおこないました。これだけではあまりにも不十分ですが一応、ゲームとして成り立たせるための最低限のものは実装できたといえます。次回、以降はもう少しゲームらしくすることを追求します。現状、スコアの表示すらできていません。