こんにちは、あんみんどうふです。
今回はTスピンを行ったかどうかの判定を作って、いろいろ応用したいと思います。
現状、回転入れができるだけなのでTスピンをしても特に何もありません。
高度なテクニックをきめたのに何も見返りがないのは寂しいので、いい感じに作ります。
先に警告しますが今回の投稿めちゃめちゃ長いです。流し読み推奨です(笑)
前回の記事はこちら。
全編見たい方はタグからどうぞ。
10-1.準備
まずはTスピンを行ったかどうかのフラグを表す変数を作ります。
判定には「回転した」「Tスピンした」「Tスピンミニした」の3種と、「スーパーローテーションが○パターン目だった」ことを表す数値が必要です。
1 2 3 4 5 | /* Tスピン関連 */ bool useSpin = false; // 回転使用フラグ bool useTSpin = false; // Tスピン使用フラグ bool useTSpinMini = false; // Tスピンミニ使用フラグ int lastSRS = 0; // 最後に行ったSRSパターン(0-4) |
ここで“Tスピンミニ”という単語が出てきました。Tスピンには3+1種類あり、1列で発生する「シングル」、2列の「ダブル」、3列の「トリプル」、そして「ミニ」があります。
Tスピンミニは通常のTスピンとは異なり、壁や床を蹴って行うTスピンを指します。これのシングル(1列消し)は通常のTスピンシングルよりずっと簡単で、画像のように壁や床にくっついて回転するだけでTスピンミニになります。
厳密に言うとミニはTスピンの種類というよりはTスピンの”ボーナス加点”みたいなものです。
壁を蹴りさえすればその時点でミニなので、その状態で1列消せば「Tスピンシングルミニ」、2列消せば「Tスピンダブルミニ」・・・ということになります。
対戦テトリスでは1列消しのミニを総じて「Tスピンミニ」と言っているので、画像右側にある消し方がTスピンミニっていうんだな~ぐらいに覚えておくといいと思います。
ダブルやトリプルはミニしても特に変わらないのでそのままの名前で呼ばれます。
余談ですが、本家テトリスの火力で並べると「ミニ(1列)<シングル<ダブル<トリプル」という順で、最も簡単に作れる分ミニが最弱です。実はミニ自体の火力は”0″なので、攻撃手段ですらありません。
しかしテトリスプレイヤーは時折ミニを使ってラインを消します。この理由は、連続でTスピンやテトリスを行うと発生するボーナス「Back To Back」を継続させるためです。
Back To Backが発生すると火力ボーナスとして1列分のダメージが追加されるので、これを目当てにミニで消すプレイヤーが多いわけです。
変数の準備はできたので判定の仕組みについて解説します。
以下の条件をすべて満たすことででTスピンを行ったことになります。
・現在操作しているミノが「Tミノ」である
・設置したタイミングで最後の操作が「ミノの回転」である
・Tミノの周囲4マスの空間のうち、3マス以上が壁やミノで埋まっている
この空間が3つ以上埋まっている必要があります。
これらの条件を満たした上で列を消すと、シングル・ダブル・トリプル・ミニ、といったように消した列数や消し方によってTスピンの種類が決まります。
10-2.フラグ管理
回転を行ったフラグを立たせるためにminoTurnメソッドに追記します。
書く場所はどこでもいいと思いますが、dirの値を変えた直後が不具合なく確実だと思うのでそこに書きます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | void minoTurn(int turn) { ... int dirTmp = dir; // 以前のdirを控えておく switch (turn) { case 0: // 左回転 dir--; break; case 1: // 右回転 dir++; break; } if (dir < 0) dir = 3; if (dir > 3) dir = 0; useSpin = true; |
(わかりづらいですが、17行目の「useSpin = true;」です)
移動したら回転フラグをfalseにします。そのままにしていると最後の操作が「移動」ではなく「回転」になってしまうので・・・。
移動を司るメソッドminoMovingに追記します。
1 2 3 4 5 6 7 8 9 10 11 12 | void minoMoving(int move, int pwr, bool hard = false) { ... if (minoMoveCheck(move, pwr)) { switch (move) { ... } useSpin = false; // 移動できたので回転フラグはfalse } |
minoMoveCheckのif文の中に書かれていればオッケーです。移動に失敗した場合フラグはそのままにします。
次にスーパーローテーションのパターン数を数えます。
minoSuperRotationメソッドの最初に追記し、変数を初期化させます。
1 2 3 4 5 | bool minoSuperRotation(int dirOld) { int movex = 0; // X座標移動量 int movey = 0; // Y座標移動量 lastSRS = 0; // SRSパターン初期化 |
そして同一メソッド内の「Iミノ以外」のif文にあるミノ重複チェック(minoDuplicateCheck)の手前に追記します。
全部で4回チェックしているので、4つすべてに追記です。
1 2 3 4 5 6 7 8 9 10 11 12 13 | // Iミノ以外 if (next[0] != 1) { // 1. 軸を左右に動かす // 0が90度(B)の場合は左,-90度(D)の場合は右へ移動 // 0が0度(A),180度(C)の場合は回転した方向の逆へ移動 switch (dir) { ... } lastSRS++; if (!minoDuplicateCheck(movey, movex)) { |
(「lastSRS++;」をif文の前に追記するだけなので他3回分のコードは省略します)
これで回転したかどうかの判定や、スーパーローテーションの到達パターン数を数えることができるようになりました。
到達パターン数は後のTスピンミニの実装で使います。
10-3.Tスピン判定
新規でminoTspinCheckメソッドを作り、以下のコードを書きます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | void minoTspinCheck() { // 対象カウント int tag = 0; // チェック箇所 int[,] point = { { 1, 0 }, { 1, 2 }, { 3, 0 }, { 3, 2 } }; for (int i = 0; i < 4; i++) { // 周囲の空間チェック if (field[y + point[i, 0] + FIELD_SPACE - 1, x + point[i, 1] + FIELD_WALL] != 0) { tag++; } } // Tスピンチェック if (tag >= 3 && useSpin) { useTSpin = true; } } |
これを実行するためにminoTurnメソッドの最後に追記します。
操作中のミノがTミノであれば呼び出します。
1 2 3 4 5 6 7 8 | // Tスピン判定 if(next[0] == 7) { minoTspinCheck(); } // 描画 minoDrawing(next[0], dir); } |
Tスピンの判定だけはこれで完成です。現状、テストする術がないのでモヤモヤが残りますが・・・。
10-4.Tスピンミニ
通常のTスピン判定はできたので、次にTスピンミニの判定です。
Tスピンミニとしてみなす条件は以下の通りです。
・Tスピンを成功させる
・埋まっている空間の数が「3つ」であり、その位置がTミノの土台側に2つ、凸型の方に1つである
・スーパーローテーションの到達パターン数が「4以外」である
2つ目の条件についてですが、例えば回転後のTミノが上向きだと下2つと上のどちらか1つの空間が埋まっている必要があります。
これが右向きだと左2つと右1つ、左向きだと右2つと左1つ、といった感じです(下向きは多分Tスピンミニ自体できないので無いです)。
また、先程スーパーローテーションの所に追記した変数も使います。「4パターン目以外」が条件なので、lastSRSの値が4未満であればオッケーですね。
これらの仕様をminoTspinCheckに書き起こして追記します。
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 | void minoTspinCheck() { // 対象カウント int tag = 0; // チェック箇所 int[,] point = { { 1, 0 }, { 1, 2 }, { 3, 0 }, { 3, 2 } }; string around = ""; // Tミノ周辺情報記録 bool chkMini = false; // ミニ用空間チェックを通ったフラグ for (int i = 0; i < 4; i++) { int sav; // 周囲の空間チェック if (field[y + point[i, 0] + FIELD_SPACE - 1, x + point[i, 1] + FIELD_WALL] != 0) { tag++; sav = 1; } else { sav = 0; } // 周囲の空間を文字列で記録する around += sav; } // Tスピンミニ用空間チェック if (tag == 3) { switch (dir) { case 0: // 上向き if (around == "1011" || around == "0111") chkMini = true; break; case 1: // 右向き if (around == "1110" || around == "1011") chkMini = true; break; case 2: // 下向き if (around == "1101" || around == "1110") chkMini = true; break; case 3: // 左向き if (around == "0111" || around == "1101") chkMini = true; break; } } // Tスピンチェック if (tag >= 3 && useSpin) { useTSpin = true; // Tスピンミニチェック if(chkMini && lastSRS != 4) useTSpinMini = true; } lastSRS = 0; // 初期化 } |
判定は空間が埋まっているかどうかを0と1で決めて、結合した文字列で比較する方法が一番楽だと思いました。
ミニとしてみなされるパターンを向き毎に用意して比較しています。
10-5.結果を表示させる
Tスピンしたことをわかりやすく表示させます。
本家でもTスピンしたら「T-Spin ○○」みたいに出てくるので、そんな感じにしてみます。
デザイナーを開き、pictureboxを適当な所に配置します。nameは「statusMessage」にでもしておきましょうか。
この領域に2秒間だけ表示させたいのでタイマーを使います。ツールボックスからTimerをフォームに持ってきて、nameを「statusViewTimer」にし、Intervalを「2000」にします。
このIntervalは“○秒経ったら××する“の○の部分です。2000ミリ秒=2秒です。
もう一つTimerを作成し、nameを「statusAnimation」にします。Intervalを「20」にします。
これは先程の××の部分で、20ミリ秒=0.02秒ごとに処理が行われます。
次に変数を用意します。いつものグローバル変数の部分に追記します。
1 2 3 4 5 | /* メッセージ関連 */ int statusAlpha = 255; // 透明度 string statusMsg; // メッセージ内容 int statusMessageInitialX; // メッセージ初期位置X int statusMessageInitialY; // メッセージ初期位置Y |
今回は前に作ったゲームオーバーの演出のように、領域を移動させたいと考えています。
変数で管理せずに移動し続けるとそのままどこかへ行ってしまうので、初期位置用の変数を用意しました。
次にその初期位置を決定するため、Loadイベントに追記します。
(Loadイベント内であれば書く場所はどこでもいいです)
1 2 3 4 5 6 | private async void mainForm_Load(object sender, EventArgs e) { ... statusMessageInitialX = statusMessage.Location.X; statusMessageInitialY = statusMessage.Location.Y; |
Load時に先程作成したpicturebox「statusMessage」の座標を取得しています。
これで変数の値から初期位置を呼び出せるようになりました。
表示の処理に移ります。
新規メソッド「statusView」を作成し、以下のコードを入力します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | /** * メッセージ表示 */ int decrementAlpha; bool BackToBackView; void statusView() { // 位置・透明度の初期化 statusMessage.Left = statusMessageInitialX; statusMessage.Top = statusMessageInitialY; statusAlpha = 255; // アルファ値減少量をTimerのIntervalから算出する decrementAlpha = 255 / (statusViewTimer.Interval / statusAnimation.Interval / 2); // 初期化してからスタート statusViewTimer.Stop(); statusAnimation.Stop(); statusViewTimer.Start(); statusAnimation.Start(); statusMessage.Visible = true; } |
メソッドが呼び出されたら透明度・位置を初期化してからタイマーを開始します。
各タイマーのTickイベントにコードを書きこみます。
まずは「statusViewTimer」から。2秒立つとタイマーがストップし、表示されていた文字が消えます。
1 2 3 4 5 6 | private void statusViewTimer_Tick(object sender, EventArgs e) { statusViewTimer.Stop(); statusAnimation.Stop(); statusMessage.Visible = false; } |
次に「statusAnimation」です。0.02秒ごとに実行され、ここで文字を描画する処理を行います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | private void statusAnimation_Tick(object sender, EventArgs e) { Bitmap bitmap = new Bitmap(statusMessage.Width, statusMessage.Height); Graphics g = Graphics.FromImage(bitmap); g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias; Font font = new Font("Meiryo UI", 12); StringFormat align = new StringFormat(); align.Alignment = StringAlignment.Center; SolidBrush brush; // 描画 brush = new SolidBrush(Color.FromArgb(statusAlpha, Color.DarkBlue)); g.DrawString(statusMsg, font, brush, statusMessage.Width / 2, 0, align); statusMessage.Image = bitmap; statusMessage.Top -= 1; statusAlpha -= decrementAlpha; // アルファ値減少(フェードさせる) if (statusAlpha < 0) statusAlpha = 0; } |
ざっくり要約すると、中央揃えで青い文字で描画し、処理を行うたびに文字を薄くしています。
ここまで非常に長いですが、あと少しで終わります。
minoDropメソッドに追記します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | void minoDrop() { if (gameStart) { // フィールドに描画 minoDrawing(next[0], dir); // NEXTを進める nextToNext(); if (useTSpin) statusMsg = "T-Spin"; if (useTSpinMini) statusMsg += "\nMini"; statusView(); // 特殊消しフラグを初期化 useTSpin = false; useTSpinMini = false; ... |
設置した時にTスピンのフラグが立っていれば「T-Spin」と表示させます。ミニの条件を満たしていれば更に「Mini」と表示。
デバッグモードで土台を作ってテストします。
フィールドの左隣に文字が出てますね!
Tスピンの判定もうまくできてるので今回の分はこれで完成です。
長かった・・・。
次回はスコアを実装します。ゲームといえばこういう要素がモチベーションになりますよね。
Tスピンをしたら高得点、パーフェクトクリアしたらもっと高得点!みたいに作りたいと思います。
#11 スコア実装編へ続く。