こんにちは、あんみんどうふです。
前回に続き、テトリス作っていきます。今回はNEXTとテトリミノの移動の実装です。キーボード入力で縦横無尽に操作できるようにします。
前回の記事はこちら↓
全編見たい方はタグからどうぞ。
3.NEXT・移動の実装
現状では何を移動させればいいのかを書いていない状態なので、まずはそれを決めてくれるNEXT(ネクスト)を実装します。
3-1.NEXT
NEXTとは、ざっくり言うと次に出てくるミノがわかる機能です。ぷよぷよにもありますね。
このNEXTの先頭(next[0])のミノを現在操作しているミノとして使います。厳密に言うと操作するミノはNEXTではないですが、細かいことは気にしない。
まずグローバル変数にNEXT関連の変数を用意します。
1 2 3 | /* NEXT関連 */ int[] next = new int[14]; // NEXT int tonextCount = 0; // nextToNext()実行回数 |
次にNEXTを決定してくれるnextDecideメソッドを作ります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | void nextDecide() { int[] val = { 1, 2, 3, 4, 5, 6, 7 }; int loop = next.Length / 7; // ループする回数 for (int i = 0; i < loop; i++) { // 各巡最初のNEXTが存在しない時に処理を行う if (next[i * 7] == 0) { // 配列valをシャッフルして配列resに格納 int[] res = val.OrderBy(i => Guid.NewGuid()).ToArray(); // NEXTに適用 for(int j = 0; j < 7; j++) { next[i * 7 + j] = res[j]; } } } } |
テトリスのガイドラインによると全種類のミノを偏りなく出す必要があるので、予め配列に1~7の数値を順番に入れてからシャッフルしています。
また、一巡分無くなったらまたシャッフルして後ろから補充するようにしています。
ちゃんと動いているか確認してみます。
Loadイベントの一番下に以下のコードを追記します。nextDecideでNEXTを設定し、minoDrawingで最初のミノを描画しています。
1 2 | nextDecide(); minoDrawing(next[0], 0); |
そして実行、終了を繰り返します。
このように毎回ランダムでミノが出てきていたらオッケーですね。テトリスに一歩近づいた気がします。
次に、NEXTを進めてくれるnextToNextメソッドを作ります。・・・もっと良い名前あったかも。
このメソッドは呼び出される度にNEXTを1つ次に進め、一巡分空っぽになったら補充する役割を持ちます。
NEXTを実装したついで一緒に書いときます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | /* NEXT進行 */ void nextToNext() { // NEXTを次に進める for (int i = 0; i < next.Length - 1; i++) { next[i] = next[i + 1]; } // ループ処理が終わったらNEXTの最後尾を0にする next[next.Length - 1] = 0; tonextCount++; if(tonextCount == 7) { // 7回実行する度にnextDecide()を実行 nextDecide(); tonextCount = 0; } } |
とりあえず書きましたがテストしようにもする手段がないので、一旦保留して次に操作の部分を作ります。次回やります。
3-2.方向キーで操作
ここに来て現在のミノの向きを表す変数を作り忘れていたので、現在の向きを表すグローバル変数を用意します。あぶねー。
1 | int dir = 0; // ミノの向き |
操作にはKeyDownイベントを使います。上を除く矢印キーで移動、Zキーで左回転、Xキーで右回転の処理を行います。
今は移動処理を作るので左右キーと下キーの処理のみ書きます。
ちなみに後の実装ですが上キーはハードドロップ、Cキーはホールドに使います。
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 | private void mainForm_KeyDown(object sender, KeyEventArgs e) { switch (e.KeyCode) { case Keys.Up: // 後でハードドロップの処理を入れる break; case Keys.Left: minoMoving(3, 1); break; case Keys.Right: minoMoving(1, 1); break; case Keys.Down: minoMoving(2, 1); break; case Keys.Z: // 左回転 break; case Keys.X: // 右回転 break; case Keys.C: // 後でホールドの処理を入れる break; } } |
移動処理はminoMovingというメソッドに分けることにしました。配列field上のミノを削除し、座標を移動してから再描画という処理になっています。
引数moveで行く先の方向、引数pwrで移動量を指定しています。
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 | void minoMoving(int move, int pwr) { for(int i = 0; i < 4; i++) { for(int j = 0; j < 4; j++) { // 現在位置に描画しているミノを一旦削除 int target = field[i + y + FIELD_SPACE - 1, j + x + FIELD_WALL]; if (target == mino[next[0], dir, i, j]) { field[i + y + FIELD_SPACE - 1, j + x + FIELD_WALL] = 0; } } } switch (move) { case 0: // 上 y += -pwr; break; case 1: // 右 x += pwr; break; case 2: // 下 y += pwr; break; case 3: // 左 x += -pwr; break; } // 新しく描画する minoDrawing(next[0], dir); } |
さて、さっそく実行して方向キーを押してみます。
ウロウロ・・・ スポッ!!
ちゃんと動いてくれていますが、衝突判定の処理を書いていないので壁抜けしてしまいます。というわけで衝突判定を作ります。
3-3.衝突判定
minoMoveCheckメソッドです。directionは移動方向、powerは移動距離。
srsx、srsyはスーパーローテーションをする時の座標のずれですが今は必要ないので、初期値0だけ入れておきます。これはまた後ほど。
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 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | /** * ミノ移動可能チェック * ------------------------------ * direction 移動方向 * power 移動距離 * srsx スーパーローテーション用X座標オフセット * srsy スーパーローテーション用Y座標オフセット */ bool minoMoveCheck(int direction, int power, int srsx = 0, int srsy = 0) { int diry = 0; int dirx = 0; bool[] chked = { false, false, false, false }; // チェック済みの列 for (int i = 0; i < 4; i++) { for (int j = 0; j < 4; j++) { // ミノの向きによってチェックする順番を決める switch (direction) { case 0: diry = i; dirx = j; break; case 1: diry = j; dirx = 3 - i; break; case 2: diry = 3 - i; dirx = j; break; case 3: diry = j; dirx = i; break; } // 現在ミノが空白の場合スキップ if (mino[next[0], dir, diry, dirx] != 0) { // powerの値分進んで障害があればFalseを返す、最後のループまで何事もなければTrue switch (direction) { case 0: if (field[diry + y - power + FIELD_SPACE - 1 + srsy, dirx + x + FIELD_WALL + srsx] != 0 && !chked[j]) { return false; } break; case 1: if (field[diry + y + FIELD_SPACE - 1 + srsy, dirx + x + power + FIELD_WALL + srsx] != 0 && !chked[j]) { return false; } break; case 2: if (field[diry + y + power + FIELD_SPACE - 1 + srsy, dirx + x + FIELD_WALL + srsx] != 0 && !chked[j]) { return false; } break; case 3: if (field[diry + y + FIELD_SPACE - 1 + srsy, dirx + x - power + FIELD_WALL + srsx] != 0 && !chked[j]) { return false; } break; } // 一度チェックした列は以降スキップする chked[j] = true; } } } return true; } |
現在ミノを1マスずつチェックし、今いる場所から各方向へ移動できるかを確認して大丈夫そうならtrueを返す処理です。
画像にしてわかりやすくしました。まず、赤いマスから緑のマスにかけて順に現在操作しているミノの場所をチェックします。移動方向によってチェックする順番を変えています。
次に、各方向に壁があるかをチェックします。例として、右向きのZミノの画像を用意しました。これを1枚目の画像に照らし合わせると、上方向は6・10・11・15になるので、その位置にミノが存在することがわかりました。長くなるので他の方向は割愛します。
画像の緑の部分に壁があればfalseを返し、何もなければ移動できるのでtrueを返します。
また、条件式にchkedという配列を挟んでいますが、これは既に壁のチェックをした列はスキップするためです。
スキップしないと画像のように、1の上が空白だったとしても2の上に空白でない物、即ち壁が存在するのでfalseを返してしまいます。これを防ぐため、1をチェックしたらその下の2はスキップするようにしています。3も同様、チェックしたら4はスキップされます。
これをさっきのminoMoving後半のswitch文に条件式で囲んであげます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | if (minoMoveCheck(move, pwr)) { switch (move) { case 0: // 上 y += -pwr; break; case 1: // 右 x += pwr; break; case 2: // 下 y += pwr; break; case 3: // 左 x += -pwr; break; } } |
あとは実行・・・の前に、これを書いて・・・。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | y = 18; x = 0; minoDrawing(6, 0); // L x = 6; minoDrawing(4, 0); // Z y = 17; x = 8; minoDrawing(7, 3); // T x = 2; minoDrawing(1, 3); // I x = -1; minoDrawing(2, 0); // O y = 16; x = 7; minoDrawing(3, 0); // S y = 15; x = 0; minoDrawing(5, 2); // J |
実行!
壁やミノを避けてるわけじゃないですよ!ちゃんとぶつかって進めないようになっています。うまくいきました~。
ちなみにこの形、テトリスでは「開幕パフェ積み」と呼ばれています。パーフェクトの略称でパフェです。食べたい。
7種すべてのミノを1回ずつ使って作れる形で、空いたスペースにミノを入れてすべて消せるとパーフェクトクリアです。
この形を組んだ後のミノでパーフェクトが取れるか取れないかが決まります。上の画像だとOミノを操作しているので、この後にJミノとIミノが来れば取れますね。
パーフェクトが取れなかったとしても比較的形が綺麗なので、Tスピン等に派生しやすくデメリットが少ない技となっています。
ちなみにパーフェクトクリアで相手に攻撃すると、前回のDT砲と同じぐらいの攻撃になります。(ぷよテトの場合。作品によって上下します)
・・・余談でした。
次回はミノの設置をできるようにし、更に今回作ったNEXTを可視化できるようにします。
#4 ミノ設置・NEXT表示編に続く。