Visual StudioとC#を初めて使ってブロック崩しゲームを作ってみた③

![4nft4087NOI5COAcVP9c1586411565-1586411590.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/384409/87d241b3-c576-29c8-7656-8a0d07ed4479.gif)
![ダウンロード.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/384409/9280ee1a-be2a-5517-9f5c-aa488c2c3ae7.gif)


前回：[Visual StudioとC#を初めて使ってブロック崩しゲームを作ってみた②](https://qiita.com/oh_yeah_sayryo/items/1f57ff462e9327e86d5b)

前回の続きです。
リザルト画面を作っていきます。

***リザルト画面のアウトライン***
・リプレイボタン
・終了ボタン
・成績表示

考えるところは成績くらいかな

***成績のディティール***
・ブロック数とモードに応じてSCORE表示
・経過時間を表示
・選択モード表示

こんな感じで進めていこうと思います。

(補足)
ホーム画面：Form2
プレイ画面：Form1
リザルト画面：Form3

##画面追加
Form2にプレイ画面とリザルト画面を表示したかったのですが、どうやらC#ではできないようです。。
ボタンやテキストボックスなどのコントロールを切替る方法はありました。
　[ウィンドウ内遷移](http://punyo-er-met.hateblo.jp/entry/2016/04/10/103632)
　[パネルとグループボックス](https://tech.bbtune.com/csharp/items/5-8.html)
ブロックはプログラムで作成しているので、大人しく別ウィンドウで作成します。

前回同様画面とボタンを作っていきます。
追加の配下にWindowsフォームがあったので、今回はここから作成します。
すると自動でWindowsフォームが選択され、ファイル名もデフォルトの「Form3.cs」で追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/384409/047ebe1e-67d8-d11f-2e2c-93401abedd3d.png)

ここも前回同様にボタンを追加
テキストは以下の通りにしてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/384409/53492789-be4f-af45-44d4-c27349a1e663.png)

リプレイのクリックイベントは
「replay_Click」、
終了のクリックイベントは
「backHome_Click」とします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/384409/36efafc7-6ffd-9a44-f5f3-54cd715c9fdc.png)

ボタン処理を記述したリザルト画面コードは以下です。

```Form3.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace Breakout
{
    public partial class Form3 : Form
    {
        public Form3()
        {
            InitializeComponent();
        }

        private void replay_Click(object sender, EventArgs e)
        {
            //画面を閉じ、プレイ画面を開く
            this.Close();
            this.Hide();
            Form1 form1 = new Form1();
            form1.ShowDialog();
        }

        private void backHome_Click(object sender, EventArgs e)
        {
            //画面を閉じる
            this.Close();
        }
    }
}
```
Close()で画面は消せますが、その後すぐにプレイ画面を呼んでいる為
画面は開いたままになってしまいます。Close処理が流れた瞬間に画面が閉じるわけではないからです。
そこでHide()で非表示にします。
(`Application.Exit();`でアプリ自体を終了できる)

プレイ画面に失敗時の処理を追加します。
まずはタイマーのインスタンス作成部分をクラス直下へ移動します。下記1行。
わかりにくかったら「Ctrl + F」でtimer等検索してください。
これでタイマーインスタンスのスコープを広げ
他のメソッドでも呼び出せるようにします。

```From1.cs
Timer timer = new Timer();
```
Updateメソッドに以下を追加。

```Form1.cs
            //失敗時
            if (ballPos.Y > paddlePos.Y)
            {
                //画面閉じてリザルト表示
                timer.Stop();
                this.Close();
                this.Hide();
                Form3 form3 = new Form3();
                form3.ShowDialog();
            }
```
リザルト画面のClose処理と違うのは`timer.Stop`があることですね。
さっきのタイマーインスタンスの移動はこの為です。
タイマーを終了させないと無限にリザルト画面が呼び出されてしまいます(すぐ止まりますが)。
これでボールがパドルより下に来ると終了してリザルト画面が呼ばれるはずです。
実行してみてください。

##成績表示
まずブロックの数を数えます。
blockNumで終了時のブロックの数をカウントします。
追加する箇所は、変数blockNumの定義とブロック作成時に値＋１、ブロック削除時に－１。
blockNumと検索してみてください。５か所見えるはずです。
blockNumMaxで最大値を求めます。2か所見えます。
変更のないメソッドは省略。

```Form1.cs
namespace Breakout
{
    public partial class Form1 : Form
    {
        Vector ballPos; //位置(Vector：2D空間における変位を表す)
        Vector ballSpeed;
        int ballRadius; //半径
        Rectangle paddlePos; //パドル位置(Rectangle:四角形を作成)
        List<Rectangle> blockPos; //ブロックの位置(リスト化)
        Timer timer = new Timer();

        public static int blockNum { get; set; } // ブロック数
        public static int blockNumMax { get; set; } // ブロック数最大値



        public Form1()
        {
            InitializeComponent(); //設定したハンドラ等の初期設定

            this.ballSpeed = new Vector(Form2.x, Form2.y); //Form2で設定した値を代入

            this.ballPos = new Vector(200, 200);
            this.ballRadius = 10;
            this.paddlePos = new Rectangle(100, this.Height - 50, 100, 5); //(位置横縦,サイズ横縦)
            this.blockPos = new List<Rectangle>();
            for (int x = 0; x <= this.Height; x += 100)
            {
                for (int y = 0; y <= 150; y += 40)
                {
                    this.blockPos.Add(new Rectangle(25 + x, y, 80, 25));

                    blockNum++;
                }
            }
            blockNumMax = blockNum;

            
            //タイマー
            timer.Interval = 33;
            timer.Tick += new EventHandler(Update); //timer.Trik：Timer有効時に呼ばれる
            timer.Start();
        }

        private void Update(object sender, EventArgs e)
        {
            //ボールの移動
            ballPos += ballSpeed;

            //左右の壁でのバウンド
            if (ballPos.X + ballRadius * 2 > this.Bounds.Width || ballPos.X - ballRadius < 0)
            {
                ballSpeed.X *= -1;
            }

            //上の壁でバウンド
            if (ballPos.Y - ballRadius < 0)
            {
                ballSpeed.Y *= -1;
            }

            //パドルの当たり判定
            if (LineVsCircle(new Vector(this.paddlePos.Left, this.paddlePos.Top),
                             new Vector(this.paddlePos.Right, this.paddlePos.Top),
                             ballPos, ballRadius)
                )
            {
                ballSpeed.Y *= -1;
            }

            // ブロックとのあたり判定
            for (int i = 0; i < this.blockPos.Count; i++)
            {
                int collision = BlockVsCircle(blockPos[i], ballPos);
                if (collision == 1 || collision == 2)
                {
                    ballSpeed.Y *= -1;
                    this.blockPos.Remove(blockPos[i]);
                    blockNum--;
                }
                else if (collision == 3 || collision == 4)
                {
                    ballSpeed.X *= -1;
                    this.blockPos.Remove(blockPos[i]);
                    blockNum--;
                }
            }

            //失敗時
            if (ballPos.Y > paddlePos.Y)
            {
                //画面閉じてリザルト表示
                timer.Stop();
                this.Close();
                this.Hide();
                Form3 form3 = new Form3();
                form3.ShowDialog();
            }

            //画面再描画
            Invalidate();
        }
    }
}
```

　
スコア表示する為のラベルを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/384409/bd546a29-e65e-ec70-aa6c-41d83c344935.png)

文字サイズはプロパティのFontをクリックすれば「・・・」が表示されるので、
そこから変更できる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/384409/a5c8edb7-97b4-2965-76e5-59a03d9d6659.png)

フォントサイズを両方とも20に設定しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/384409/5e1653e9-3008-efe5-ce1f-80cfb1f66078.png)

モードの値を受け渡す為にホーム画面に変数modeを追加。
2行追加。変更のないメソッドは省略。

```Form2.cs
namespace Breakout
{
    public partial class Form2 : Form
    {
        //ボールの速度(x, y)
        public static int x { get; set; }
        public static int y { get; set; }
        //モード
        public static int mode { get; set; }

        private void mode_Select(object sender, EventArgs e) //モードセレクト
        {
            int selectedMode = comboBox1.SelectedIndex;
            mode = selectedMode;

            switch (selectedMode)
            {
                case 0:
                    //Easy
                    x = -2;
                    y = -4;
                    break;
                case 1:
                    //Normal
                    x = -3;
                    y = -6;
                    break;
                case 2:
                    //Hard
                    x = -5;
                    y = -10;
                    break;
                case 3:
                    //Expert
                    x = -8;
                    y = -16;
                    break;
                default:
                    //未選択時はNormal
                    x = -3;
                    y = -6;
                    break;
            }
        }
    }
}

```
　

スコアを表示していきます。
Form3のウィンドウを選択してプロパティを開き、
from3_Loadメソッドを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/384409/f860fcb6-a2ae-64b6-2e7e-8f5a370ecd42.png)

Form3(リザルト画面)は以下になります。

```Form3.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace Breakout
{
    public partial class Form3 : Form
    {
        public Form3()
        {
            InitializeComponent();
        }

        private double score()
        {
            //倍率設定
            double bairitsu = ((Form2.mode + 1) /10 * 2) + 1;

            //基準点300*ブロック数*モード倍率
            return 300 * (Form1.blockNumMax - Form1.blockNum) * bairitsu;
        }

        private void replay_Click(object sender, EventArgs e)
        {
            //画面を閉じ、プレイ画面を開く
            this.Close();
            this.Hide();
            Form1 form1 = new Form1();
            form1.ShowDialog();
        }

        private void backHome_Click(object sender, EventArgs e)
        {
            //画面を閉じる
            this.Close();
        }

        private void label2_Click(object sender, EventArgs e)
        {

        }

        private void form3_Load(object sender, EventArgs e)
        {
            label2.Text = score().ToString();
        }
    }
}

```
倍率を`bairitsu = ((Form2.mode + 1) /10 * 2) + 1`としていますが
ややこしかったら`bairitsu = Form2.mode + 1`だけでもいいです。
これだとモード：Easy(要素0)で倍率1です。
ラベルは`label2.Text`で変更でき、
受け取ったスコアを文字に変換しています`score().ToString()`。

###修正
実行してみてください。
きちんと表示されましたか？
スコア数が桁数によって左右にずれるのが気になったので、
常時センター表示に変更します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/384409/7faa666c-e685-662c-ed99-ed016210683f.png)
桁数少ないときに左端に寄ってしまうので、AutoSizeもfalseへ変更。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/384409/0c9e91f2-c1fe-e733-23cc-a873a1280cf0.png)

EasyからExpertまで試してみたのですが、Expertのときだけボールがパネルに触れる前に終了してしまいます。
遷移時の条件はコレです。`ballPos.Y > paddlePos.Y`
コンソールに出力します。

```Form1.cs
Console.WriteLine(ballPos.Y + ":" + paddlePos.Y);
            //失敗時
            if (ballPos.Y > paddlePos.Y)
            {
                //画面閉じてリザルト表示
                timer.Stop();
                this.Close();
                this.Hide();
                Form3 form3 = new Form3();
                form3.ShowDialog();
            }
```
　
もう一度Expertで同じ動作をします。
その後、アプリを終了させると出力結果が見れます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/384409/d0ef91f4-23eb-7056-e38b-e8dcc971a220.png)
一瞬423を超えていますね。
ボールの位置は→`ballPos += ballSpeed;`
Expert時のY軸移動数が16
出力を見ると408の次に16足された424が来てしまってますね。
なので、余裕を持って遷移条件をボールが画面から消えた時にします。
遷移時の条件を以下へ変更
`ballPos.Y > paddlePos.Y　→　ballPos.Y > this.Height`
プロパティのSizeを見ると画面のY軸473と余裕ありますね！(Size数はコレに合わせる必要はない)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/384409/ed6422ed-19b3-5dfd-59bf-b8a2d43b6f21.png)

これで不具合はなくなりました。
にしても、パドルの移動距離が少ないからExpertが難しい・・・
機能アップデートは第4回で実施します。しばしお待ちを。

##経過時間表示
Stopwatchクラスを利用します。
Form1で４か所に1行ずつ追加します。

```Form1.cs
//追記②
using System.Diagnostics;

public partial class Form1 : Form
{
     //追記②
     public static Stopwatch keikaTime = new Stopwatch(); //経過時間

・・・

     public Form1()
     {
         //追記③
          keikaTime.Restart(); //経過時間スタート

・・・

            //失敗時
            if (ballPos.Y > this.Height)
            {
                //追記④
                keikaTime.Stop(); //停止
・・・
      
```
Startではなく、Restartを使っているのは
リプレイしたときにStartだとリセットされないからです。
Restartだとリセット＋スタートの機能を持ちます。

リザルト画面にラベルを2つ貼り付けます。
タイムと数字。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/384409/c049e996-5634-bcfb-f904-89826ca71103.png)

Form3で経過時間を受け取ります。
form3_Loadメソッドで以下1行を追加してください。

```Form3.cs
private void form3_Load(object sender, EventArgs e)
        {
           //経過時間例 00:01:03.1235785 → 03.123
            label4.Text = Form1.keikaTime.Elapsed.ToString().Substring(6, 6);
        }
```
Stopwatchの値を「Elapsed」で取得すると「00:01:03.1235785」のような表記になります。
スコアと同じように「ToString」で文字列にし、
「Substring(6,6)」で6桁目から6桁を切り取ります(要素と同じで最初を0桁目とし数えます)。

以下ように表記されればOK！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/384409/31ac26d3-4daf-c522-948d-832ea8d9c535.png)

##選択モード表示
もうここまで来れば説明しなくてもできそうですがｗ
選択モードのリザルト表示も解説します。
まずはラベルを用意します。
せっかくなのでタイムとモードは英文字で統一しました。
未選択時はNormal なのでテキストは「Normal 」。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/384409/65c684c7-0410-10e8-3ef6-546e53e3067d.png)

Form2.csで渡す変数を定義。

```Form2.cs
        //モードテキスト
        public static string modeText { get; set; }
```
mode_Selectメソッドに以下を追加。

```Form2.cs
            if (comboBox1.SelectedItem ==null)
            {
                modeText = "Normal";
            } else
            {
                modeText = comboBox1.SelectedItem.ToString();
            }
```
Form3.csのform3_Loadメソッドで受け取り代入。

```Form3.cs
         label6.Text = Form2.modeText;
```
実行してみてください。
未選択にNormal 、選択時に選択したモードが表示されていれば成功！

###修正

プレイ画面を×ボタンで閉じても、
裏でボールは動き続けリザルトへ遷移してしまう。
Form1のプロパティのイベントでFormClosingを設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/384409/e38f70bb-778b-5632-ebd9-2d07ca22ab7d.png)
これは×ボタン押下時に処理が流れます。
失敗時の処理をコピペして終わり。

```Form1.cs
 private void form1_Closing(object sender, FormClosingEventArgs e) //×ボタン押下時
        {
            keikaTime.Stop();
            timer.Stop();
            this.Close();
            this.Hide();
        }
```
お疲れ様でした。
##感想
今回は一時パドルの挙動がおかしくなって時間かかってしまいました。
そんなときは一度Visual Studioを再起動すると直ったり！・・・

全体ソースを載せていないのでGitHubに投稿しました。
以下から確認できます。気になるCSファイル等を見てください。
https://github.com/seyryo/sayryo/tree/start/Breakout

次回は
カウントダウンスタートとCLEAR時の処理を作っていく予定です。↓作成中