おもちゃラボ

Unityで遊びを作ってます

【Visual C#でゲームを作る】ブロック崩し編その3

前回はキー入力によるパドルの移動、パドルとボールの当たり判定を作成しました。

【Visual C#でゲームを作る】ブロック崩し編 その1 - おもちゃラボ
【Visual C#でゲームを作る】ブロック崩し編 その2 - おもちゃラボ
【Visual C#でゲームを作る】ブロック崩し編その3 - おもちゃラボ ◀今回

今回は、最後の仕上げとしてブロックを作成し、ボールとブロックの当たり判定をVisual C#を使って作りましょう。最終的な完成イメージは次のようになります。

f:id:nn_hokuson:20170816120653g:plain:w350

ブロックを作成する

まずはフォームにひとつだけブロックを表示するところから始めましょう。ブロックはパドルを描画したときと同じ手順で表示できます。だんだんC#のプログラムが長くなってきたので、必要な部分のみ載せます。C#プログラムの全文は下記のページで見ることができます。

DrawBlock · GitHub

public partial class Form1 : Form
{
    Vector ballPos;
    Vector ballSpeed;
    int ballRadius;
    Rectangle paddlePos;
    Rectangle blockPos;

    public Form1()
    {
        InitializeComponent();

        this.ballPos = new Vector( 200, 200 );
        this.ballSpeed =  new Vector(-2, 4);
        this.ballRadius = 10;
        this.paddlePos = new Rectangle(100, this.Height - 50, 100, 5);
        this.blockPos =  new Rectangle(100, 50, 80, 25);

        Timer timer = new Timer();
        timer.Interval = 33;
        timer.Tick += new EventHandler(Update);
        timer.Start();
    }

    double DotProduct(Vector a, Vector b)
    {
        //変更なし
    }

    bool LineVsCircle(Vector p1, Vector p2, Vector center, float radius)
    {
        //変更なし
    }

    private void Update(object sender, EventArgs e)
    {
        //変更なし
    }

    private void Draw(object sender, PaintEventArgs e)
    {
        SolidBrush pinkbrush = new SolidBrush(Color.HotPink);
        SolidBrush grayBrush = new SolidBrush(Color.DimGray);
        SolidBrush blueBrush = new SolidBrush(Color.LightBlue);

        float px = (float)this.ballPos.X - ballRadius;
        float py = (float)this.ballPos.Y - ballRadius;

        e.Graphics.FillEllipse(pinkbrush, px, py, this.ballRadius * 2, this.ballRadius * 2);
        e.Graphics.FillRectangle(grayBrush, paddlePos);
        e.Graphics.FillRectangle(blueBrush, blockPos);
    }

    private void KeyPressed(object sender, KeyPressEventArgs e)
    {
        //変更なし
    }
}

メンバ変数にブロックの位置と大きさを定義するblockPos変数を追加しています。また、このblockPos変数を使ってDrawメソッド内でブロックの描画をしています。

実行すると次のように画面上部にブロックが表示されます。

f:id:nn_hokuson:20170816101547p:plain:w350

ブロックとボールの当たり判定をする

続けてブロックとボールの当たり判定を作ります。ボールとブロックの当たり判定は「円と長方形の当たり判定」とみなせますね。

f:id:nn_hokuson:20170816132541p:plain:w500

そして円と長方形の当たり判定は、円と直線の当たり判定を4回繰り返すことで実現できます。単純ですが、実装は簡単なのでこの方法でプログラムを作りましょう。

f:id:nn_hokuson:20170816133106p:plain

次のプログラムを入力して下さい。こちらもプログラム全文は次のページを参照して下さい。

BlockVsCircle · GitHub

public partial class Form1 : Form
{
    Vector ballPos;
    Vector ballSpeed;
    int ballRadius;
    Rectangle paddlePos;
    Rectangle blockPos;

    public Form1()
    {
        //変更なし
    }

    double DotProduct(Vector a, Vector b)
    {
        //変更なし        }

        bool LineVsCircle(Vector p1, Vector p2, Vector center, float radius)
        {
            //変更なし
        }

        int BlockVsCircle(Rectangle block, Vector ball)
        {
            if (LineVsCircle(new Vector(block.Left, block.Top),
                new Vector(block.Right, block.Top), ball, ballRadius))
                return 1;

            if (LineVsCircle(new Vector(block.Left, block.Bottom),
                new Vector(block.Right, block.Bottom), ball, ballRadius))
                return 2;

            if (LineVsCircle(new Vector(block.Right, block.Top),
                new Vector(block.Right, block.Bottom), ball, ballRadius))
                return 3;

            if (LineVsCircle(new Vector(block.Left, block.Top), 
                new Vector(block.Left, block.Bottom), ball, ballRadius))
                return 4;

            return -1;
        }

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

            // 左右の壁でのバウンド
            if (ballPos.X + ballRadius > 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;
            }

            // ブロックとのあたり判定
            int collision = BlockVsCircle(blockPos, ballPos);
            if( collision == 1 || collision == 2)
            {
                ballSpeed.Y *= -1;
            }
            else if(collision == 3 || collision == 4)
            {
                ballSpeed.X *= -1;
            }

            // 再描画
            Invalidate();
        }

        private void Draw(object sender, PaintEventArgs e)
        {
            //変更なし
        }

        private void KeyPressed(object sender, KeyPressEventArgs e)
        {
            //変更なし
        }
    }
}

ここでは長方形と円の当たり判定をするためのBlockVsCircleメソッドを定義しています。このメソッドはブロックの情報と円の中心座標を引数に取ります。

メソッドの中ではブロックの4辺について1辺ずつ「円と直線の当たり判定」を使って当たりを調べています。当たっていた場合は1〜4の値を返します。

Updateメソッドの中で作成したBlockVsCircleメソッドを使ってボールとブロックの当たり判定をしています。メソッドの返り値を使って当たった辺を調べ、辺によって跳ね返る方向を変化させています。

実行すると次のようにボールがブロックで跳ね返るようになります。

f:id:nn_hokuson:20170816111347g:plain:w350

複数のブロックを表示する

1つのブロックで当たり判定ができたので、次はフォームの上部に複数のブロックを表示しましょう。次のプログラムを入力して下さい。プログラム全文は次のページを参照下さい。

DrawBlocks · GitHub

public partial class Form1 : Form
{
    Vector ballPos;
    Vector ballSpeed;
    int ballRadius;
    Rectangle paddlePos;
    List<Rectangle> blockPos;

    public Form1()
    {
        InitializeComponent();

        this.ballPos = new Vector( 200, 200 );
        this.ballSpeed = new Vector(0, 0);
        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));
            }
        }


        Timer timer = new Timer();
        timer.Interval = 33;
        timer.Tick += new EventHandler(Update);
        timer.Start();
    }

    double DotProduct(Vector a, Vector b)
    {
            //変更なし    }

    bool LineVsCircle(Vector p1, Vector p2, Vector center, float radius)
    {
            //変更なし
    }

    int BlockVsCircle(Rectangle block, Vector ball)
    {
            //変更なし
    }

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

        // 左右の壁でのバウンド
        if (ballPos.X + ballRadius > 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;
            }
            else if (collision == 3 || collision == 4)
            {
                ballSpeed.X *= -1;
            }
        }

        // 再描画
        Invalidate();
    }

    private void Draw(object sender, PaintEventArgs e)
    {
        SolidBrush pinkbrush = new SolidBrush(Color.HotPink);
        SolidBrush grayBrush = new SolidBrush(Color.DimGray);
        SolidBrush blueBrush = new SolidBrush(Color.LightBlue);

        float px = (float)this.ballPos.X - ballRadius;
        float py = (float)this.ballPos.Y - ballRadius;

        e.Graphics.FillEllipse(pinkbrush, px, py, this.ballRadius * 2, this.ballRadius * 2);
        e.Graphics.FillRectangle(grayBrush, paddlePos);

        for (int i = 0; i < this.blockPos.Count; i++)
        {
            e.Graphics.FillRectangle(blueBrush, blockPos[i]);
        }
    }

    private void KeyPressed(object sender, KeyPressEventArgs e)
    {
            //変更なし
    }
}

ここでは複数のブロックを表示するため、blockPos変数をList型に変更しています。コンストラクタの中で必要な個数分ブロックを生成し、当たり判定と描画部分はListの個数分繰り返しています。

プログラムを実行すると次のように複数個のブロックが表示されます。

f:id:nn_hokuson:20170816115256p:plain:w350

当たった場合はブロックを消す

さて、長かったブロック崩し作りも最後の工程です!最後はボールがブロックに当たった場合、ブロックを消します。ブロックを消すにはブロックのリストblockPosから、当たったブロックの変数を削除します。Updateメソッドの中のブロックとの当たり判定の部分を次のように書き換えて下さい。

            // ブロックとのあたり判定
            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]);  // 追加
                }
                else if (collision == 3 || collision == 4)
                {
                    ballSpeed.X *= -1;
                    this.blockPos.Remove(blockPos[i]); // 追加
                }
            }

ボールとブロックが当たった場合、ListのもつRemoveメソッドを使ってリストから当たったブロックの変数を削除しています。Removeメソッドに削除したい要素を渡すことで、リストの中から指定した要素を削除することができます。Listの持つメソッドについては「確かな力が身につくC#超入門」を参照下さい。

実行すると、次のようにボールが当たったブロックが消えるようになります。これでブロック崩しに必要な一通りの機能が作れました!

f:id:nn_hokuson:20170816120653g:plain:w350

まとめ

ようやくブロック崩しゲームは完成です!UnityやUnreal Engineといったゲームエンジンを使わなくてもVisual C#の機能だけでゲームを作ることができましたね。

ゲームづくりにはまだまだ学ばなければいけないことはたくさんありますが、基本的な考え方は押さえられたのではないでしょうか。もう少し踏み込んでゲームを作ってみたい方には次の書籍がおすすめです。