C#
game
Unity
Unity入門

ミニゲームを作ってUnityを学ぶ! [ 3Dマインスイーパー編 - 2. ブロックを作る ]

ミニゲームを作ってUnityを学ぶ![3Dマインスイーパー編]

第2回目: ブロックを作る

マインスイーパーのフィールドを作るためにはそれを構成するマスが必要です。
今回はせっかくの3Dですので平面的な「マス」ではなく、立体的な「ブロック」を並べることでフィールドを構築していきます。

*この変更とあわせて、以降は地雷マスを「爆弾ブロック」と呼称します。

ブロックオブジェクトの準備

まずはCubeを利用してブロックのオブジェクトを作り、それをプレハブに書き出します。

unity_sweeper_ss_2_1.jpg

  1. ゼロポジションにCubeを配置
  2. Cubeの名前を「Block」に変更
  3. 配置されたBlockに「Sci-Fi Texture Pack 2/Materials/Floor_1_Diffuse」をアタッチ
  4. MeshRendererのCastShadowsをOFFにして影を非表示に設定
  5. Blockをプレハブに書き出す

ブロックに必要な機能

3Dマインスイーパーでは通常のそれと同じように、プレイヤーがブロックに対して「開ける・マーカーを付ける」のアクションを実行することができます。

開けられたブロックに爆弾が設置されていた場合はそこでゲームオーバーとなり、爆弾の設置されていない通常ブロックを開けた場合はそのブロックの周囲に設置された爆弾の数を知ることができます。
この仕様に沿った場合、ブロックに必要な機能は以下の2つになります。

  • 閉じている状態から開けられた状態へ変化する
  • マーカー・数字・爆弾のいずれかを表示する

マテリアルの差し替えでブロックを開く

まずは1つ目の機能「閉じている状態から開けられた状態への変化」をBlockにアタッチされたマテリアルを差し替えることで実装していきます。

unity_sweeper_ss_2_3.jpg

  • スクリプト「BlockModel」を作成してBlockにアタッチ
BlockModel.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

    public class BlockModel : MonoBehaviour
    {
        [SerializeField]
1:      private Material mOpenedMaterial;

        //----------
        // フラグ //
        //---------------------------------------------------------------------------------

        // 開かれたブロックの場合はtrue
        public bool IsOpen { get; private set; }

        //-------------
        // アクション //
        //---------------------------------------------------------------------------------

        /// <summary>
        /// ブロックを開く
        /// </summary>
        public void Open()
        {
            IsOpen = true;
            GetComponent<Renderer>().material = mOpenedMaterial;
        }
    }

1: 「Sci-Fi Texture Pack 2/Materials/Wall_2_Diffuse」をインスペクタから設定

Open()では自身(Blockオブジェクト)にアタッチされたマテリアルを差し替えることで見た目の状態を変化させています。
また同時に自身が開いているかどうかのフラグを立てていますので、Open()を呼び出す側でフラグの判定を行えばすでに開いているブロックを再度開けないように制御することができます。

板状のオブジェクトで表示機能を実装する

2つ目のブロックにマーカーや爆弾を表示する機能はブロックの上の面に表示用のテクスチャをアタッチした板状のオブジェクトを貼り付けることで実装します。

unity_sweeper_ss_2_2.jpg

◆ マテリアルの準備

まずは板状のオブジェクトにアタッチするマテリアルを用意しますが、今回は表示するそれぞれに対応した数のテクスチャではなく、それらを1つにまとめたテクスチャアトラスを使ってマテリアルを作成します。

  1. 下の画像を「atlas_number」という名前でインポート
  2. Texture TypeをDefaultに設定
  3. マテリアル「NumberPlane」を作成し、Albedoにatlas_numberを設定

atlas_plane.png

テクスチャアトラス

上の画像のように使用する複数のテクスチャを1つにまとめたモノを「テクスチャアトラス」と呼びます。
テクスチャアトラスを利用することで画面を描画する際に呼び出されるテクスチャの数を減らし、描画
の処理を軽くできる場合があります。

◆ 板オブジェクトの作成

続いて、板オブジェクトを作ってBlockの上に乗せていきます。

unity_sweeper_ss_2_4.jpg

  1. 3D ObjectのQuadを「NumberPlane」という名前でシーンに配置
  2. NumberPlaneのCastShadowsをOFFにして影を非表示に設定
  3. NumberPlaneに先ほど作成したマテリアル「NumberPlane」をアタッチ
  4. NumberPlaneをBlockの子にしてTransformを以下のように設定
Position: (x=0, y=0.52, z=0)
Rotation: (x=90, y=0, z=0)
   Scale: (x=1, y=1, z=1)
PlaneとQuad

どちらもUnityで平面を扱う際に使用するオブジェクトですが
Planeが121個の頂点と200個の三角メッシュから構成されているのに対して
Quadは4つの頂点と2つの三角メッシュで構成されています。

今回のようにただ単純に画像を表示したい場合はQuadを使うと考えておけば問題ありません。

◆ マテリアルにテクスチャの透過を反映させる

これでBlockオブジェクトの上面に新しいテクスチャを表示することができましたが、いまの段階ではテクスチャに本来設定されている透明部分が白く塗りつぶされていますので、これを修正します。

  1. Projectウインドウにあるマテリアル「NumberPlane」の「Shader/Standard」をクリック
  2. ドロップボックスから「Legasy Shaders/Transparent/Diffuse」を選択

unity_sweeper_ss_2_5.jpg

◆ チェックマークを表示する

マテリアルに透明部分が正しく反映されましたので、次は板オブジェクト「NmberPlane」のUVマップを変更してマーカーとして使用するチェックマークだけが表示されるようにしてみます。

  • スクリプト「NumberChanger」を作成してNumberPlaneにアタッチ
NumberChanger.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

    public class NumberChanger : MonoBehaviour {

        //-----------------
        // UVマップの定義 //
        //---------------------------------------------------------------------------------

        private static Vector2[] UV_CHECK =
        {
            new Vector2(0.5f, 0.25f),
            new Vector2(0.75f, 0.5f),
            new Vector2(0.75f, 0.25f),
            new Vector2(0.5f, 0.5f)
        };

        //----------
        // 初期化 //
        //---------------------------------------------------------------------------------

        private void Awake()
        {
            // チェックマークを表示
            ChangeUvToCheck();
        }

        //---------------------
        // UVマップの切り替え //
        //---------------------------------------------------------------------------------

        /// <summary>
        /// UVマップをチェックに変更
        /// </summary>
        public void ChangeUvToCheck()
        {
            Mesh mesh = GetComponent<MeshFilter>().mesh;
            mesh.uv = UV_CHECK;
        }

    }

プロジェクトを実行することでNumberPlaneのテクスチャがチェックマークに切り替わることが確認できます。

unity_sweeper_ss_2_6.jpg

テクスチャの表示範囲を切り替えるためにChangeUvToCheck()ではオブジェクトのMeshを取得して、プロパティのuvに予め定義しておいたUV_CHECKを代入していますが、この部分について3Dオブジェクトの構造とあわせてもう少し詳しく見ていきます。

◇ 3Dオブジェクトの構造

Unityの3Dオブジェクトはそれが立方体でも円でも、複雑な形状のキャラクターでさえも、全ては三角形の板だけで形成されています。

unity_sweeper_2_7.jpg

この三角形がそれぞれどんな大きさで、どのように組み合わせれば良いかを表した、いわば3Dの組み立て説明書がMesh = 形状データのことです。

Meshの中には様々な種類のデータが格納されていますが、NumberChangerで値を代入したuvとはUVマップと呼ばれるデータ群で、3Dのオブジェクトに対して2Dのテクスチャをどのように貼り付ければ良いかを示したモノとなります。

つまりChangeUvToCheck()ではテクスチャの全体が表示されている状態からチェックマークのみが表示されるように、このテクスチャの貼り付け方を変更したということになります。

Meshの構造についてもっと知りたい場合はぜひ下記の紹介記事などをご参考ください。

参考: UnityEngine.Meshの内部構造解説

◇ UVマップ

Meshの簡単な説明に続いて、次はUV_CHECKの値について見ていきます。

new Vector2(0.5f, 0.25f)
new Vector2(0.75f, 0.5f)
new Vector2(0.75f, 0.25f)
new Vector2(0.5f, 0.5f)

UVマップは別名UV座標とも呼ばれますが、上記のVector2はそのままテクスチャの座標を表しています。
元画像の左下を原点(0, 0)として右上を(1.0f, 1.0f)としたときに、UV_CHECKが示すのは以下の範囲になります。

unity_sweeper_ss_2_8.jpg

このとき座標を指定する順番が決められていて、Quadに適用するUVマップは下の画像で言う0番から順番に指定する必要があります。

unity_sweeper_ss_2_9.jpg

表示機能を完成させる

それではNumberPlaneがチェックマークだけでなく、爆弾や数字といった全ての表示をできるようにNumberChangerを修正していきます。
尚、NumberChangerについてはこれが完成形のコードとなります。

NumberChanger.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

    public class NumberChanger : MonoBehaviour
    {

        //----------
        // 初期化 //
        //---------------------------------------------------------------------------------

        private void Awake()
        {
            // 最初は何も表示しないテクスチャを設定
            ChangeUvToBlank();
        }

        //---------------------
        // UVマップの切り替え //
        //---------------------------------------------------------------------------------

        private static Vector2[] UV_ONE =
        {
            new Vector2(0.0f, 0.75f),
            new Vector2(0.25f, 1.0f),
            new Vector2(0.25f, 0.75f),
            new Vector2(0.0f, 1.0f)
        };

        private static Vector2[] UV_TWO =
        {
            new Vector2(0.25f, 0.75f),
            new Vector2(0.5f, 1.0f),
            new Vector2(0.5f, 0.75f),
            new Vector2(0.25f, 1.0f)
        };

        private static Vector2[] UV_THREE =
        {
            new Vector2(0.5f, 0.75f),
            new Vector2(0.75f, 1.0f),
            new Vector2(0.75f, 0.75f),
            new Vector2(0.5f, 1.0f)
        };

        private static Vector2[] UV_FOUR =
        {
            new Vector2(0.75f, 0.75f),
            new Vector2(1.0f, 1.0f),
            new Vector2(1.0f, 0.75f),
            new Vector2(0.75f, 1.0f)
        };

        private static Vector2[] UV_FIVE =
        {
            new Vector2(0.0f, 0.5f),
            new Vector2(0.25f, 0.75f),
            new Vector2(0.25f, 0.5f),
            new Vector2(0.0f, 0.75f)
        };

        private static Vector2[] UV_SIX =
        {
            new Vector2(0.25f, 0.5f),
            new Vector2(0.5f, 0.75f),
            new Vector2(0.5f, 0.5f),
            new Vector2(0.25f, 0.75f)
        };

        private static Vector2[] UV_SEVEN =
        {
            new Vector2(0.5f, 0.5f),
            new Vector2(0.75f, 0.75f),
            new Vector2(0.75f, 0.5f),
            new Vector2(0.5f, 0.75f)
        };

        private static Vector2[] UV_EIGHT =
        {
            new Vector2(0.75f, 0.5f),
            new Vector2(1.0f, 0.75f),
            new Vector2(1.0f, 0.5f),
            new Vector2(0.75f, 0.75f)
        };

        private static Vector2[] UV_BOMB_A =
        {
            new Vector2(0.0f, 0.25f),
            new Vector2(0.25f, 0.5f),
            new Vector2(0.25f, 0.25f),
            new Vector2(0.0f, 0.5f)
        };

        private static Vector2[] UV_BOMB_B =
        {
            new Vector2(0.25f, 0.25f),
            new Vector2(0.5f, 0.5f),
            new Vector2(0.5f, 0.25f),
            new Vector2(0.25f, 0.5f)
        };

        private static Vector2[] UV_CHECK =
        {
            new Vector2(0.5f, 0.25f),
            new Vector2(0.75f, 0.5f),
            new Vector2(0.75f, 0.25f),
            new Vector2(0.5f, 0.5f)
        };

        private static Vector2[] UV_BLANK =
        {
            new Vector2(0.75f, 0.25f),
            new Vector2(1.0f, 0.5f),
            new Vector2(1.0f, 0.25f),
            new Vector2(0.75f, 0.5f)
        };

        /// <summary>
        /// UVマップを爆弾に変更
        /// </summary>
        public void ChangeUvToBombA()
        {
            Mesh mesh = GetComponent<MeshFilter>().mesh;
            mesh.uv = UV_BOMB_A;
        }

        /// <summary>
        /// UVマップを特別な爆弾に変更
        /// </summary>
        public void ChangeUvToBombB()
        {
            Mesh mesh = GetComponent<MeshFilter>().mesh;
            mesh.uv = UV_BOMB_B;
        }

        /// <summary>
        /// UVマップをチェックに変更
        /// </summary>
        public void ChangeUvToCheck()
        {
            Mesh mesh = GetComponent<MeshFilter>().mesh;
            mesh.uv = UV_CHECK;
        }

        /// <summary>
        /// UVマップをブランクに変更
        /// </summary>
        public void ChangeUvToBlank()
        {
            Mesh mesh = GetComponent<MeshFilter>().mesh;
            mesh.uv = UV_BLANK;
        }

        /// <summary>
        /// 自分の周りにある爆弾の数によってUVマップを変更する
        /// </summary>
        /// <param name="aroundBombs">隣接1マス内にある爆弾の数</param>
        public void ChangeNumber(int aroundBombs)
        {
            Mesh mesh = GetComponent<MeshFilter>().mesh;
            switch (aroundBombs)
            {
                case 1:
                    mesh.uv = UV_ONE;
                    break;
                case 2:
                    mesh.uv = UV_TWO;
                    break;
                case 3:
                    mesh.uv = UV_THREE;
                    break;
                case 4:
                    mesh.uv = UV_FOUR;
                    break;
                case 5:
                    mesh.uv = UV_FIVE;
                    break;
                case 6:
                    mesh.uv = UV_SIX;
                    break;
                case 7:
                    mesh.uv = UV_SEVEN;
                    break;
                case 8:
                    mesh.uv = UV_EIGHT;
                    break;
                default:
                    mesh.uv = UV_BLANK;
                    break;
            }
        }

    }

Awake()で実行されるChangeUvToBlank()によって、プロジェクト実行時のBlockの上面には何も表示されていない板が置かれていることになります。
必要に合わせて適切なChangeUvTo~()を呼び出すことでその時に適したテクスチャを板オブジェクトに表示していく仕組みです。

また、ChangeNumber()は引数に「周囲に設置された爆弾の数」を与えることでテクスチャの表示を適切な数字に切り替えています。

ブロックと板の連携

最後にBlockModelを修正してNumberChangerの機能を利用できるようにします。
尚、BlockModelについてもこれが完成形のコードとなります。

BlockModel.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

    public class BlockModel : MonoBehaviour
    {
        [SerializeField]
        private Material mOpenedMaterial;
        [SerializeField]
1:      private NumberChanger mNumChanger;

        //-------------
        // ポジション //
        //---------------------------------------------------------------------------------

        public int X { get; private set; }
        public int Y { get; private set; }

        public void SetPosition(int x, int y)
        {
            X = x;
            Y = y;
        }

        //----------
        // フラグ //
        //---------------------------------------------------------------------------------

        // 爆弾ブロックの場合はtrue
        public bool HasBomb { get; set; }

        // 開かれたブロックの場合はtrue
        public bool IsOpen { get; private set; }

        // チェック済ブロックの場合はtrue
        public bool IsCheck { get; private set; }

        //-------------
        // アクション //
        //---------------------------------------------------------------------------------

        /// <summary>
        /// ブロックを開く
        /// </summary>
        public void Open(int aroundBombs)
        {
            IsOpen = true;
            mNumChanger.ChangeNumber(aroundBombs);
            GetComponent<Renderer>().material = mOpenedMaterial;
        }

        /// <summary>
        /// チェック済フラグを反転させる
        /// それによってチェックマークを表示・非表示にする
        /// </summary>
        public void ChangeCheckFlg()
        {
            if (IsOpen) return;
            IsCheck = !IsCheck;
            if (IsCheck)
            {
                mNumChanger.ChangeUvToCheck();
            }else
            {
                mNumChanger.ChangeUvToBlank();
            }
        }

        /// <summary>
        /// 爆弾を表示する
        /// フラグが立っている場合は特別な爆弾を表示する
        /// </summary>
        /// <param name="flg"></param>
        public void ShowBomb(bool flg)
        {
            if (flg)
            {
                mNumChanger.ChangeUvToBombB();
            }
            else
            {
                mNumChanger.ChangeUvToBombA();
            }
        }

    }

1: Block自身の子に設定されているNumberPlane(NumberChanger)をインスペクタから設定

新しいBlockModelではポジションやフラグのプロパティが増えたほか、自身のマテリアルを変更したり上面に乗せた板のテクスチャを切り替えたりといった機能を実装しています。

これでフィールドを構成するブロックが完成しましたのでBlockのプレハブに変更を反映させた後、シーンに配置されているBlockオブジェクトは削除しておきます。


次のページに進む
イントロダクションに戻る