おもちゃラボ

Unityで遊びを作ってます

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

UnityやUnreal Engineなどゲームエンジン全盛期の時代ですが、ゲームの仕組みや当たり判定のアルゴリズムを学ぶには、やはり1から自分で作ってみるのが一番です。

ここではC#を使って、次のような(超地味な)ブロック崩しを作りながら、ゲームの仕組みを学んでいきましょう。

f:id:nn_hokuson:20170813182842g:plain:w400

全3回なので簡単に1時間あれば終わると思います。その1ではVisual C#のプロジェクト作成とボールの移動部分を作ります。その2ではキーに応じたパドルの移動、パドルとボールの当たり判定、その3ではブロックの作成、ボールとブロックの当たり判定を作ります。お楽しみに〜!

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


またVisual C#ではなく、ゲームエンジン(Unity)を使ったシューティングゲームの作り方が知りたい方は、こちらの記事を参考にしてみて下さい。

nn-hokuson.hatenablog.com

Visual C#のプロジェクトを作る

まずはブロック崩し用のVisual C#プロジェクトを作ります。ここではVisual Studio 2017を使って説明していきます。Visual Studioを起動したら、メニューバーからファイル→新規作成→プロジェクト作成を選択して下さい。プロジェクト作成画面が表示されるので、プロジェクト名を「Breakout」にしてOKボタンを押して下さい。

f:id:nn_hokuson:20170813191637j:plain:w500

フォームの右下をドラッグして画面サイズを横長にして下さい。大きさは大体図のような感じになればOKです。

f:id:nn_hokuson:20170813192854j:plain:w550

ボールを表示する

まずは画面にボールを表示するところから始めましょう。この記事では初心者の方でも理解しやすいように、追加でクラスは作らずに説明をしていきます。

ボールをフォーム上に描画するにはPaintイベントハンドラにボールの描画プログラムを記述します。Paintイベントハンドラについては拙書「確かな力が身につくC#「超」入門」で詳しく説明しているので、そちらを参照下さい。

まずForm1.cs[デザイン]タブを選択して下さい。フォームを選択した状態でイベントタブを選択し、Paintイベントハンドラを探して下さい。Paintイベントハンドラが見つかったら、その欄にDrawと入力しエンターキーを押して下さい。

f:id:nn_hokuson:20170813192729j:plain:w300

Form1.csが表示されます。ゲームプログラムではベクトルと行列演算は非常によく使います。Visual C#でVector型が使えるようにするにはWindowBaseフレームワークの参照を追加する必要があります。プロジェクト⇛参照の追加を選択し、ウインドウの下の方にある「WindowBase」を追加して下さい。

f:id:nn_hokuson:20170813193312j:plain:w450

参照が追加できたら、次のプログラムを入力して下さい。Vector型を使うため、using System.Window;の行を追加していることに注意して下さい。

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

namespace Breakout
{
    public partial class Form1 : Form
    {
        Vector ballPos;
        int ballRadius;

        public Form1()
        {
            InitializeComponent();

            this.ballPos = new Vector(200,200);
            this.ballRadius = 10;
        }

        private void Draw(object sender, PaintEventArgs e)
        {
            SolidBrush pinkBrush = new SolidBrush(Color.HotPink);

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

Form1クラスのメンバ変数に、ボールの位置を決めるballPosと、ボールの半径ballRadiusを定義しています。Drawイベントハンドラの中ではまず、ボールを描画するためのブラシ(SolidBrush)を作っています。

作成したブラシを使い、FillEllipseメソッドで円を描画しています。円の描画には円を囲む矩形を指定するため、左上のXY座標と、幅と高さを計算しています。

f:id:nn_hokuson:20170813195215p:plain:w300

描画に関しても「確かな力が身につくC#超入門」で説明していますので、適宜参照下さい。実行すると、次のようにフォーム上に表示されます。

f:id:nn_hokuson:20170813194421p:plain:w350

ボールを動かす

ボールが表示できたので、次はボールを動かしましょう。壁での跳ね返りは後で考えるので、まずは直線でボールを移動させる方法だけを作ります。

今はフォームが表示されるタイミングで一度だけDrawメソッドが実行されます。ボールを移動するアニメーションを表示するためには、ボールの位置を移動させながら1秒間に何度もDrawメソッドを実行し、フォームを描画する必要があります。要はパラパラ漫画の原理です。

f:id:nn_hokuson:20170817200242p:plain

Visual C#に標準で用意されているタイマー機能を使って、一定時間ごとにフォームを再描画(Drawメソッドを実行)するInvalideメソッドを呼び出すことにします。先ほどのC#プログラムを次のように修正して下さい(Form1クラスの中だけ表示しています)。

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

        public Form1()
        {
            InitializeComponent();

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

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

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

        private void Draw(object sender, PaintEventArgs e)
        {
            SolidBrush pinkBrush = new SolidBrush(Color.HotPink);

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

ここでは、コンストラクタでタイマーを作り、Updateメソッドを33msごとに呼び出しています。Updateメソッドの中ではボールの位置に、ボールの移動ベクトル(ballSpeed)を足すことでボールの位置を移動しています。Updateメソッドの最後でInvalideメソッドを呼び出して、フォームを再描画しています。

このプログラムでは33msごとに画面を再描画するため、一秒間では約30回の画面書き換えが発生します。1秒間に何回画面を書き換えるかの数値をFPS(Frame Per Second)と呼びます。通常のゲームでは30FPSか60FPS、映画なら24FPSぐらいが目安となります。FPSについては次の記事も合わせて御覧ください。

nn-hokuson.hatenablog.com

再度、プログラムをビルドして実行してみて下さい。ボールが左上に移動するアニメーションが表示されます(画面がちらつく問題の対処は最後にします)。

f:id:nn_hokuson:20170813200710g:plain:w400

壁に当たったらボールを跳ね返す

ボールが動くようになったので、次は壁で跳ね返す処理を作りましょう。壁でボールを跳ね返すためには、壁に当たった瞬間にボールの移動ベクトルを反転します。左右の壁に当たった場合には移動ベクトルのX値、上側の壁に当たった場合は移動ベクトルのY値を反転させます。

f:id:nn_hokuson:20170813212110p:plain:w500

先ほど作成したForm1.csに次のプログラムを入力して下さい。

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

        public Form1()
        {
            InitializeComponent();

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

            Timer timer = new Timer();
            timer.Interval = 33;
            timer.Tick += new EventHandler(Update);
            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)
        {
            SolidBrush pinkBrush = new SolidBrush(Color.DimGray);

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

Updateメソッドの中にボールが跳ね返る処理を書いています。ボールが左右の壁にぶつかった場合には移動ベクトルのX値を反転させています。ballPposはボールの中心座標を表しているので、壁と当たり判定するときには、ボールの半径を考慮していることに注意して下さい。

f:id:nn_hokuson:20170813213425p:plain:w500

ゲームを実行すると、ボールが壁に当たったときに跳ね返るようになります。

f:id:nn_hokuson:20170813212359g:plain:w350

アニメーションのちらつきを低減する

ボールを移動させたときにアニメーションがちらつくのが気になった人がいるかもしれません。これはダブルバッファリングという仕組みを使うことで防ぐことができます。

ダブルバッファリングは、直接表示中の画像を書き換えることはせず、もう一枚バックバッファを用意し、バックバッファに対して次のフレームを描画する仕組みです。バックバッファの描画が終わったらフロントバッファとバックバッファを入れ替えます。

f:id:nn_hokuson:20170813203838p:plain

詳しくはこちらの記事でも解説しているので参照下さい。

nn-hokuson.hatenablog.com

Visual C#ではプロパティウインドウからダブルバッファリングを有効にできます。フォームを選択した状態で、プロパティウインドウの「DoubleBuffered」を「True」にして下さい。

f:id:nn_hokuson:20170813212642j:plain:w300

ゲームを実行するとちらつきがなくなっていることが分かります。今回はここまでです。次回はボールを跳ね返すパドルの作成と、パドルの移動、ボールとパドルの当たり判定を実装していきますよ〜お楽しみに!

nn-hokuson.hatenablog.com

参考

本文中で出てきた、イベントハンドラやグラフィックの描画については「確かな力が身につくC#超入門」で詳しく説明していますので、気になった方はぜひ読んでみて下さい。

また、ダブルバッファリングやFPSなどゲームを動かす仕組みについてはコチラの書籍が詳しいです。

Unityなどゲームエンジンを使ってみたい方はこちらをどうぞ