LoginSignup
0
5

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-04-03

※追記
自分なりに拡張してみました。
オリジナルですので参考サイトのように解説をしています!
シンプルかつ簡単に初学者向けに解説しています。
②ホーム画面からスタートボタンで開始してみた
③リザルト画面を作ってスコアを出す
④設定画面・カウントダウン・経過中タイム・CLEAR画面追加してみた


20170816120653.gif
少しばかり自由時間を与えられたので勉強がてらにゲームアプリを探していたところ丁度良いものを発見!

参考にしたサイト:【Visual C#でゲームを作る】ブロック崩し編

文法の基礎さえ知っていれば作れるレベル。
これならC#を初めて触る人でも大丈夫そう。
でも、ベクトルが登場するので、高校数学苦手なら少し難しいかも・・・

環境
無償のVisual Studioバージョン2017を使っています。
インストールしてすぐ使えるので初心者に優しいですね。
ここからダウンロード→https://visualstudio.microsoft.com/ja/vs/express/

ボール作成

【Visual C#でゲームを作る】ブロック崩し編①を解説
その1.ではボールが移動し跳ね返るところまで。
基本は参考サイトを見ていただいて
初学者にわかりやすくする為、こちらでさらにコメント追記や軽い解説をしていく。

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;
using System.Windows.Forms;

namespace Breakout
{
    public partial class Form1 : Form
    {
        Vector ballPos; //位置(Vector:2D空間における変位を表す)
        Vector ballSpeed;
        int ballRadius; //半径

        public Form1()
        {
            InitializeComponent(); //設定したDraw等のイベントハンドラを呼ぶ

            this.ballPos = new Vector(200, 200);
            this.ballSpeed = new Vector(-3, -4);
            this.ballRadius = 10;

            Timer timer = new Timer();
            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 > this.Bounds.Width || ballPos.X - ballRadius < 0)
            {
                ballSpeed.X *= -1;
            }

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

        private void Draw(object sender, PaintEventArgs e) //Draw意味:描画する
        {
            SolidBrush pinkBrush = new SolidBrush(Color.HotPink); //SolidBrush(ブラシ)は.Netのクラスで図形を塗り潰す

            //左上からの位置を設定
            float px = (float)this.ballPos.X - ballRadius; //マイナス半径とすることで円の中心になる
            float py = (float)this.ballPos.Y - ballRadius;

            //e.描画.円(色, 横, 縦, 物質幅, 物質高さ)
            e.Graphics.FillEllipse(pinkBrush, px, py, this.ballRadius * 2, this.ballRadius * 2);
        }
    }
}

解説

上のメソッド毎に渡される「object sender」「e」は
senterでどの部品か判定し、eにその情報を含んでいる。
このようにイベントで動くメソッドをイベントハンドラという。

↓メリットの例としては呼ばれた時点で部品を判断でき、
まとめて管理できる。

Button.cs
private void Button_Click(object Sender, EventArgs e)
{
    switch((Sender as Button).Name){
    case "Button1":
        // Button1 のクリック処理
        break;
    case "Button2":
        // Button2 のクリック処理
        break;
    case "Button3":
        // Button3 のクリック処理
        break;
    ...
    }
}

修正箇所

左右壁に触れるときボールが半分壁にめり込むのが気になったので、
判定条件を少し変更している。
//左右の壁でのバウンド
if (ballPos.X + ballRadius * 2 > this.Bounds.Width || ballPos.X - ballRadius < 0)

パドル作成

【Visual C#でゲームを作る】ブロック崩し編②
ここに来て高校数学で習ったベクトルが登場する...
全く覚えていない。。
鋭角三角形のcosθがマイナスになることはggって思い出し、
ベクトルの定理は呪文として理解した(理解したとは言っていない)。

当たり判定はパドルとボールのX軸Y軸座標から求めることもできるが、
後で作成するブロックにも同じ処理を使えるよう
汎用性のある法線の長さから求めるべきだろう。

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;
using System.Windows.Forms;

namespace Breakout
{
    public partial class Form1 : Form
    {
        Vector ballPos; //位置(Vector:2D空間における変位を表す)
        Vector ballSpeed;
        int ballRadius; //半径
        Rectangle paddlePos; //パドル位置(Rectangle:四角形を作成)

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

            this.ballPos = new Vector(200, 200);
            this.ballSpeed = new Vector(-3, -4);
            this.ballRadius = 10;
            this.paddlePos = new Rectangle(100, this.Height - 50, 100, 5); //(位置横縦,サイズ横縦)

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

        /// <summary>
        /// 内積計算
        /// </summary>
        /// <param name="a"></param>
        /// <param name="b"></param>
        /// <returns></returns>
        double DotProduct(Vector a, Vector b)
        {
            return a.X * b.X + a.Y * b.Y;
        }

        /// <summary>
        /// 当たり判定
        /// </summary>
        /// <param name="p1">パドル左端座標</param>
        /// <param name="p2">パドル右端座標</param>
        /// <param name="center">ボール中心</param>
        /// <param name="radius">ボール半径</param>
        /// <returns></returns>
        bool LineVsCircle(Vector p1, Vector p2, Vector center, float radius)
        {
            Vector lineDir = (p2 - p1); //パドルのベクトル(パドルの長さ)
            Vector n = new Vector(lineDir.Y, -lineDir.X); //パドルの法線
            n.Normalize();

            Vector dir1 = center - p1;
            Vector dir2 = center - p2;

            double dist = Math.Abs(DotProduct(dir1, n));
            double a1 = DotProduct(dir1, lineDir);
            double a2 = DotProduct(dir2, lineDir);

            return (a1 * a2 < 0 && dist < radius) ? true : false;
        }

        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;
            }

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

        private void Draw(object sender, PaintEventArgs e) //Draw意味:描画する
        {
            SolidBrush pinkBrush = new SolidBrush(Color.HotPink); //SolidBrush(ブラシ)は.Netのクラスで図形を塗り潰す
            SolidBrush grayBrush = new SolidBrush(Color.DimGray);

            //左上からの位置を設定
            float px = (float)this.ballPos.X - ballRadius; //マイナス半径とすることで円の中心になる
            float py = (float)this.ballPos.Y - ballRadius;

            //e.描画.円(色, 横, 縦, 物質幅, 物質高さ)
            e.Graphics.FillEllipse(pinkBrush, px, py, this.ballRadius * 2, this.ballRadius * 2);
            //e.描画.長方形(色, 長方形)
            e.Graphics.FillRectangle(grayBrush, paddlePos);
        }

        private void KeyPressed(object sender, KeyPressEventArgs e) //押下毎
        {
            if (e.KeyChar == 'a') //A押下時
            {
                this.paddlePos.X -= 20;
            }
            else if (e.KeyChar == 's') //S押下時
            {
                this.paddlePos.X += 20;
            }
        }
    }
}

複雑なのはベクトル計算ロジックだけ。。

「///」:スラッシュ3つ並んでいる箇所はドキュメントコメントといい、パラメーターやメソッドの説明を記述している。

修正箇所

パドルが画面の外にも移動できるのが気になったので
画面から消えないようにする。

 private void KeyPressed(object sender, KeyPressEventArgs e) //押下毎
        {
            if (e.KeyChar == 'a' && paddlePos.Left > 0) //A押下時
            {
                this.paddlePos.X -= 20;
            }
            else if (e.KeyChar == 's' && paddlePos.Right < this.Width) //S押下時
            {
                this.paddlePos.X += 20;
            }
        }

ブロック作成

【Visual C#でゲームを作る】ブロック崩し編③
ブロックを作成後、リスト化してボールが当たれば消えるようにする。
当たり判定は参照その2を流用するので難しいことは特に記述しない。

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;
using System.Windows.Forms;

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

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

            this.ballPos = new Vector(200, 200);
            this.ballSpeed = new Vector(-4, -8);
            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.Trik:Timer有効時に呼ばれる
            timer.Start();
        }

        /// <summary>
        /// 内積計算
        /// </summary>
        /// <param name="a"></param>
        /// <param name="b"></param>
        /// <returns></returns>
        double DotProduct(Vector a, Vector b)
        {
            return a.X * b.X + a.Y * b.Y;
        }

        /// <summary>
        /// 当たり判定
        /// </summary>
        /// <param name="p1">パドル左端座標</param>
        /// <param name="p2">パドル右端座標</param>
        /// <param name="center">ボール中心</param>
        /// <param name="radius">ボール半径</param>
        /// <returns></returns>
        bool LineVsCircle(Vector p1, Vector p2, Vector center, float radius)
        {
            Vector lineDir = (p2 - p1); //パドルのベクトル(パドルの長さ)
            Vector n = new Vector(lineDir.Y, -lineDir.X); //パドルの法線
            n.Normalize();

            Vector dir1 = center - p1;
            Vector dir2 = center - p2;

            double dist = Math.Abs(DotProduct(dir1, n));
            double a1 = DotProduct(dir1, lineDir);
            double a2 = DotProduct(dir2, lineDir);

            return (a1 * a2 < 0 && dist < radius) ? true : false;
        }

        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 * 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]);
                }
                else if (collision == 3 || collision == 4)
                {
                    ballSpeed.X *= -1;
                    this.blockPos.Remove(blockPos[i]);
                }
            }

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

        private void Draw(object sender, PaintEventArgs e) //Draw意味:描画する
        {
            SolidBrush pinkBrush = new SolidBrush(Color.HotPink); //SolidBrush(ブラシ)は.Netのクラスで図形を塗り潰す
            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.描画.円(色, 横, 縦, 物質幅, 物質高さ)
            e.Graphics.FillEllipse(pinkBrush, px, py, this.ballRadius * 2, this.ballRadius * 2);
            //e.描画.長方形(色, 長方形)
            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) //押下毎
        {
            if (e.KeyChar == 'a' && paddlePos.Left > 0) //A押下時
            {
                this.paddlePos.X -= 20;
            }
            else if (e.KeyChar == 's' && paddlePos.Right < this.Width) //S押下時
            {
                this.paddlePos.X += 20;
            }
        }
    }
}

↓BlockVsCircleのfor文で回しているブロックの生成順
縦(Y軸)に全て並べてから右(X軸)に移る
image.png

以下のように横長になってしまった場合、Form1.csのデザインで簡単にフォームのサイズを変更できる。
また、横長にブロックを並べたいときはForm1コンストラクタの「this.Height」を「this.Width」に変更すればよい。
image.png

感想

冒頭に1時間あれば終わるって書いてたのに全く足りんかった・・まだまだだな。
Unityでゲーム作ったほうが色々できそうだし楽しそう!

画面遷移とダイアログ表示をする為に、
ホーム画面とリザルト画面を作成したので良かったら見て行ってください。
Visual StudioとC#を初めて使ってブロック崩しゲームを作ってみた②

0
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
5