おもちゃラボ

Unityで遊びを作っていきます

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

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

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

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

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

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

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

nn-hokuson.hatenablog.com

プロジェクトを作る

まずはブロック崩し用のプロジェクトを作ります。ここでは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#「超」入門」で詳しく説明しているので、そちらを参照下さい。

確かな力が身につくC#「超」入門

確かな力が身につく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メソッドを呼び出すことにします。先ほどのプログラムを次のように修正して下さい(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

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

参考

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

確かな力が身につくC#「超」入門

確かな力が身につくC#「超」入門

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

ゲームを動かす技術と発想

ゲームを動かす技術と発想

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

Unity5の教科書 2D&3Dスマートフォンゲーム入門講座 (Entertainment&IDEA)

Unity5の教科書 2D&3Dスマートフォンゲーム入門講座 (Entertainment&IDEA)

【Unity】ユニティちゃんの走った経路を足跡で表示する

ゲームで「地に足の着いた感」を出すには影を使うのが一般的ですが、面白い表現として足跡を使うことがあります。この記事ではUnityで「それっぽい」足跡を表示する方法を紹介します。

f:id:nn_hokuson:20170810201546p:plain

記事の内容は次のとおりです。

ステージをセッティングする

ここでは次のようなステージを作成し、NavMeshを使ってUnityちゃんを走らせるようにします。

f:id:nn_hokuson:20170810201614p:plain:w550

NavMeshを使ってUnityちゃんを走らせる方法は次の記事を参照ください。

nn-hokuson.hatenablog.com

足跡のテクスチャを用意する

片足づつのテクスチャを使うと配置するのが大変になるので、今回は両足ぶんのテクスチャを用意します。足跡の中点がテクスチャの中心に来るようにします。

f:id:nn_hokuson:20170810201642p:plain:w100

続いてこのテクスチャをシーンビューに配置します。ヒエラルキーウインドウでCreate -> 3D Object -> Planeを選択して、画面上に地面と平行になるように配置してください。

f:id:nn_hokuson:20170810201656p:plain:w500

足跡テクスチャをPlaneにドラッグ&ドロップし、ShaderのRendering Modeを「Fade」に設定します。また、Smoothnessは「0」に設定しておきます。

f:id:nn_hokuson:20170810201708p:plain:w300

Standard ShaderのFadeとTransparentの違いは次の通りです。

オプション 意味
Transparent ガラスのような透明感(光の具合によっては不透明)
Fade どこから見ても透明


ヒエラルキーウインドウからプロジェクトウィンドウに足跡のテクスチャをドラッグ&ドロップして足跡のPrefabをつくります。名前はfootPrintPrefabにしておきます。

f:id:nn_hokuson:20170810135633j:plain

Unityちゃんの通った経路に足跡を生成する

Unityちゃんの通った経路に足跡を生成していくスクリプトを作りましょう。プロジェクトウィンドウで右クリックし、Create -> Script -> C# Scriptを選択してください。

スクリプトが作れたら名前を「UnitychanController」にして、次のスクリプトを入力してください。

public class UnitychanController : MonoBehaviour 
{
    public GameObject footPrintPrefab;
    float time = 0;

    void Update()
    {
        this.time += Time.deltaTime;
        if (this.time > 0.35f)
        {
            this.time = 0;
            Instantiate (footPrintPrefab, transform.position, transform.rotation);
        }
    }

Updateメソッドの中で一定時間ごとに、ユニティちゃんの向いている方向に合わせて足跡Prefabのインスタンスを生成しています。

UnitychanControllerをユニティちゃんにドラッグ&ドロップして、アタッチしてください。続けて、アタッチしたスクリプトのfootPrintPrefabの欄に、足跡Prefabの実態をセットしてください。

f:id:nn_hokuson:20170810135814j:plain

これで、ユニティちゃんの歩いた経路に従って足跡が表示されるようになりました。実行してみてください。

f:id:nn_hokuson:20170810201813g:plain

足跡がだんだん薄れていくようにする

今のままでも良いですが、徐々に足跡が薄れていくように変更してみます。

プロジェクトウィンドウで右クリックし、Create -> Script -> C# Scriptを選択して「FootPrintController」を作り、次のスクリプトを入力してください。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class FootPrintController : MonoBehaviour 
{
	void Start () {
        StartCoroutine (Disappearing ());
	}
	
    IEnumerator Disappearing()
    {
        int step = 90;
        for (int i = 0; i < step; i++)
        {
            GetComponent<MeshRenderer> ().material.color = new Color (1, 1, 1, 1 - 1.0f * i / step);
            yield return null;
        }
        Destroy (gameObject);
    }
}

このスクリプトではDisapearingコルーチンの中で、マテリアルの透明度を徐々に下げることで足跡を薄れさせています。最後にDestroyメソッドでインスタンスを破棄しています。

作成したFootPrintControllerをプロジェクトウィンドウのfootPrintPrefabにアタッチして再度実行してみてください。今度は足跡が徐々に薄れていくはずです。

f:id:nn_hokuson:20170810201828g:plain

まとめ

Unityちゃんに足跡を付ける方法を紹介しました。雪の上に足跡をつけるなど、ちゃんと地面を凹ませたい場合は、こちらの記事が参考になります。

kikikiroku.session.jp

確かな力が身につくC#「超」入門とUnity5の教科書もよろしくお願いします〜

確かな力が身につくC#「超」入門

確かな力が身につくC#「超」入門

Unity5の教科書 2D&3Dスマートフォンゲーム入門講座

Unity5の教科書 2D&3Dスマートフォンゲーム入門講座

【Unity】Webカメラの画像を加工して表示する

Unityを使えばUSBカメラやスマートフォンのカメラからの画像を簡単に取得したり、加工したりすることが出来ます。ここではUnityでWebカメラを使う方法と、取得した画像のピクセルにアクセスして画像処理する方法を紹介します。

f:id:nn_hokuson:20170809192551j:plain

今回の記事の内容は次のとおりです。

カメラ画像を画面に表示する

まずは画像を表示するためのPlaneを作成します。ヒエラルキーウインドウからCreate -> 3D Object -> Planeを選択してください。カメラに対して垂直になるように回転し、サイズを調整してください。

f:id:nn_hokuson:20170809191014j:plain:w500

続いてWebカメラの画像をいま作成したPlaneに表示するためのスクリプトを作りましょう。プロジェクトウィンドウで右クリックし、Create -> Script -> C# Scriptを選択して「WebCamController」を作成してください。

スクリプトが作成できたら、次のスクリプトを入力してください。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class WebCamController : MonoBehaviour
 {
    int width = 1920;
    int height = 1080;
    int fps = 30;
    WebCamTexture webcamTexture;

    void Start () {
        WebCamDevice[] devices = WebCamTexture.devices;
        webcamTexture = new WebCamTexture(devices[0].name, this.width, this.height, this.fps);
        GetComponent<Renderer> ().material.mainTexture = webcamTexture;
        webcamTexture.Play();
    }
}

このスクリプトでは、カメラデバイスのリストを取得し、先頭のデバイス(スマートフォンの内側カメラならdevices[1]とかになります)から得られるWebCamTextureをPlaneにセットしています。

f:id:nn_hokuson:20170809191548p:plain:w400

WebCamTextureをnewするときに画像の幅と高さを渡していますが、指定どおりの大きさのテクスチャが得られるわけではなく、カメラがサポートしている解像度から最も近いものが選択されます。

WebCamControllerをPlaneにアタッチします。プロジェクトウィンドウのWebCamControllerをヒエラルキーウインドウのPlaneにドラッグ&ドロップしてください。

f:id:nn_hokuson:20170808155425j:plain

実行すると、カメラで撮影された動画がPlaneに表示されると思います。

映像が上下逆さまになっている場合

PlaneのScaleのy値をマイナスにしてみてください。左右方向も逆さまになっている場合はxの値もマイナスにしてください。

f:id:nn_hokuson:20170809191219p:plain:w300

映像が淡い色合いになっている場合

マテリアルを新規で作成してPlaneにアタッチし、ShaderをUnlit/Textureに設定してください。

f:id:nn_hokuson:20170809191228p:plain:w300

Webカメラからの画像に対して画像処理をする

続いて、カメラから得られた画像をグレースケールに変換する画像処理をしてみましょう。画像処理する場合はテクスチャをもう一枚用意する必要があります。

先程はWebCamTextureを直接Planeのマテリアルに設定していましたが、今回はWebCamTextureで得られたテクスチャに対して画像処理したものをTexture2D型の変数に格納します。この画像をPlaneに設定することで、画像処理後の画像が表示されます。

f:id:nn_hokuson:20170809191914p:plain:w550

次のプログラムを入力して実行してください。

using System.Collections;
using System.Collections.Generic;
using UnityEngiusing System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class WebCamController : MonoBehaviour {

    int width = 1920;
    int height = 1080;
    int fps = 30;
    Texture2D texture;
    WebCamTexture webcamTexture;
    Color32[] colors = null;

    IEnumerator Init()
    {
        while (true)
        {
            if (webcamTexture.width > 16 && webcamTexture.height > 16)
            {
                colors = new Color32[webcamTexture.width * webcamTexture.height];
                texture = new Texture2D (webcamTexture.width, webcamTexture.height, TextureFormat.RGBA32, false);
                GetComponent<Renderer> ().material.mainTexture = texture;
                break;
            }
            yield return null;
        }
    }

    // Use this for initialization
    void Start () {
        WebCamDevice[] devices = WebCamTexture.devices;
        webcamTexture = new WebCamTexture(devices[0].name, this.width, this.height, this.fps);
        webcamTexture.Play();

        StartCoroutine (Init ());
    }

    // Update is called once per frame
    void Update () {
        if (colors != null)
        {
            webcamTexture.GetPixels32 (colors);

            int width = webcamTexture.width;
            int height = webcamTexture.height;
            Color32 rc = new Color32();

            for (int x = 0; x < width; x++)
            {
                for (int y = 0; y < height; y++)
                {
                    Color c = colors [x + y * webcamTexture.width];
                    colors [x + y * webcamTexture.width] = new Color (c.grayscale, c.grayscale, c.grayscale);
                }
            }

            texture.SetPixels32 (colors);
            texture.Apply ();
        }
    }
}

このプログラムでは画像処理後の画像を格納するためのピクセルデータ配列(colors)とバッファになるテクスチャ(texture)を用意しています。Initメソッドの中で、ピクセル配列とテクスチャ用にカメラから得られる画像の大きさぶんのメモリを確保しています。

WebCamTextureをPlayしてから、画像が表示されるまで少しタイムラグがあります。カメラからの映像が得られてから、メモリを確保するようにInitメソッド内では待ちのプログラムを挟んでいます。

画像処理はUpdateメソッドの中で行っています。GetPixels32メソッドを使ってWebCamTextureからピクセルデータを取得し、得られた画像の全ピクセルを走査して、1ピクセルごとにグレースケール化する処理をしています。最後にSetPixels32メソッドでtextureに画像処理したピクセルをセットしています。

プログラムができたら実行してみてください。グレースケールに変換された映像が表示されたと思います。ただ・・・重たいですね・・・。Statusで確認すると3FPSぐらいしか出ていませんでした。

高速化する

ということで、何処の処理が重たいのかprofilerで確認してみましょう。Window -> Profilerをクリックしてプロファイラウインドウを開いてください。「Deep Profile」をクリックしてからゲームを実行してください。

f:id:nn_hokuson:20170808155721j:plain:w550

プロファイル結果を見ると、Color32.op_implictとTexture.get_width、Texture.get_heightでCPUを65%ぐらい食ってしまっていますね。WebCamTextureの幅と高さを取得する処理とColor->Color32の型変換が遅いようです。

Updateメソッドを次のように書き換えてみてください。

void Update () 
{
    if (colors != null)
    {
        webcamTexture.GetPixels32 (colors);

        int width = webcamTexture.width;
        int height = webcamTexture.height;
        Color32 rc = new Color32();

        for (int x = 0; x < width; x++)
        {
            for (int y = 0; y < height; y++)
            {
                Color32 c = colors [x + y * width];
                byte gray = (byte)(0.1f * c.r + 0.7f * c.g + 0.2f * c.b);
                rc.r = rc.g = rc.b = gray;
                colors [x + y * width] = rc;
            }
        }

        texture.SetPixels32 (colors);
        texture.Apply ();
    }
}

ここでは、テクスチャの幅と高さを取得する部分を画像処理ループの外側に移動しています。また、Color型ではなくColor32型を使うように変更し、newするのをループの外側に持っていっています。

もう一度実行してみてください。今度は20FPS程度は出るはずです。

f:id:nn_hokuson:20170809192551j:plain

iOSで実行するとクラッシュする場合

iOSで実行しようとすると、iOSのバージョンによってはアプリがクラッシュする場合があります。この場合次のようなエラーが表示されます。

This app has crashed because it attempted to access privacy-sensitive data without a usage description. The app's Info.plist must contain an NSCameraUsageDescription key with a string value explaining to the user how the app uses this data.

iOS10からはカメラへアクセスする場合のセキュリティが厳しくなり、info.plistにDescriptionを記述しなければいけなくなりました。

なにもクラッシュすることはないと思うのですが・・・(笑)

Xcodeのinfo.plistを開いて、info.plistに「Privacy - Camera Usage Description」を追加してから、再度Xcodeでビルドしてください。

f:id:nn_hokuson:20170809191304p:plain:w300

実行するとiPhone上でカメラアクセスを許可する画面がでて、OKするとカメラからの映像が表示されます。