Unityでリバーシを作る

Unityでリバーシを作る3(盤面作成後編)

どうも、りくらぼです。

今回はUnityでリバーシを作るの3回目、盤面作成の後編です。

前編はこちら

reversi2
Unityでリバーシを作る2(盤面作成前編)どうも、りくらぼです。 前回はリバーシに必要な素材を準備しました。 https://riku-lab.com/reversi...

この記事ではこのような盤面を表示するまでを解説します。

board
見た目ができると嬉しいですね!頑張りましょう

盤面を作る(中編)

prefabを8×8マスに並べるプログラムを作る途中でした。

手順
  1. 2次元配列をつくる
  2. 表示先のGameObjectを読み込む
  3. 表示先のGameObjectにprefabを並べる

前回は「①2次元配列をつくる」まで解説しました!

2次元配列は次のコードで宣言しましたね。

    int[,] board = new int[8,8]; // 長さが8*8の2次元配列boardを宣言

この2次元配列boardで盤面の状態を管理することになりました。

詳しくは前回の記事をご覧ください。

reversi2
Unityでリバーシを作る2(盤面作成前編)どうも、りくらぼです。 前回はリバーシに必要な素材を準備しました。 https://riku-lab.com/reversi...

今回は「②表示先のGameObjectを読み込む」を解説していきます。

表示先のGameObjectを読み込む

手順
  1. 表示先のGameObjectを作る
  2. GameObjectにコンポーネントを追加する
  3. GameObjectを読み込む

表示先のGameObjectを作る

表示先のGameObjectを作りましょう。

ヒエラルキーのCanvas内に空のGameObjectを作成し、名前を「BoardDisplay」にします。

create_board_display
詳しい方法は前回の記事で解説しています。

このBoardDisplayにプログラムでPrefabを追加していきます。

次のように書くとわかりやすいでしょうか。

プログラムでPrefabを複製→BoardDisplayの子オブジェクトとして追加していく

GameObjectにコンポーネントを追加する

プログラムで複製したPrefabをBoardDisplayに追加していくのですが、ただ追加するだけでは綺麗に並んでくれません。

タイル状に並べるにはいくつか方法がありますが、今回は「Grid Layout Group」コンポーネントを使って並べることにします。

Grid Layout GroupコンポーネントはGameObjectに追加すると子オブジェクトを自動でタイル状に並べてくれるという便利なものです!

BoardDisplayのインスペクターをみてみましょう。手順と画像を参考にグリッドレイアウトグループを追加してみてください。

手順
  1. 「コンポーネントを追加」をクリック
  2. 「lay」と入力
  3. 「グリッドレイアウトグループ」を選択
add_grid_layout_group

グリッドレイアウトグループが追加出来たら、次は値を変更していきましょう。

  • 幅と高さを512にします。(セルサイズ64×8マス)
  • セルサイズを64にします。
  • 子を整列をMiddle Centerにします。
set_grid_layout_group

これで8×8マスにきれいに並べることができます。

セルサイズを64にしたのは用意した素材のサイズが64だったからです。

また、子を整列をMiddleCenterにすることで上下中央ぞろえにできます。

GameObjectを読み込む

それでは作成したBoardDisplayをプログラムで読み込みましょう。

読み込む手順は前回解説した通りです。

  //盤面のGameObject
    [SerializeField]
    GameObject boardDisplay = null;

ここで、前回からの変更点が1つだけあります。前回は何も代入しなかったのですが、今回から「null」を代入するように変更します。

nullについてざっくり解説

nullとは、プログラムでは何もないという意味になります。つまり、「変数boardDisplayの初期値はカラです」と宣言しています。

こうしないとコンソールに以下のエラーが出てしまっていました。

(前回の時点で気が付かず申し訳ありません)

warning

前回のblackObject、whiteObject、emptyObjectにもそれぞれ「null」を代入してあげてください。

保存出来たらインスペクターからboardDisplayを指定してくださいね。

gameManager

盤面の初期値を設定する

ここが今回の山場です!!!

手順
  1. enumで数字に名前を付ける
  2. 盤面の初期値を設定する関数を作る
  3. 関数を呼び出す

どうしてもenumや関数といった言葉が出てきてしまいます。本来は一気にやるような内容ではないので、無茶振りをしています。

順番に解説していきますので、なるべく落ち着いていきましょう!

enumで数字に名前を付ける

まずenumって何??
enumを使うと数字にわかりやすい名前を付けることができます!

どうして数字に名前を付ける必要があるのでしょうか。今回のリバーシを例にしてみましょう。

数字に名前を付けるメリット

前回、盤面の空欄を0、黒い駒を1、白い駒を2にして2次元配列に代入するということをお話ししました。

このとき、二次元配列boardの(0,0)を空欄にすると

board[0,0] = 0;

このようになりますね。

しかし、この1行を見ただけでは数字の0を代入しているだけで、空欄にしているという風には読み取れません。

では以下のような書き方はどうでしょうか。

int EMPTY = 0;
board[0,0] = EMPTY;

これはEMPTYという変数に0を代入して、board[0,0]にEMPTYを代入しています。

この書き方のほうが、board[0,0]は空欄だと直感的にわかりやすいと思います。

数字に名前を付けるメリットはここにあります。

数字に名前を付けることで直感的に何をしているかわかりやすくなる

enumを使うメリット

数字に名前を付けるとわかりやすくなると説明しました。

では1(黒い駒)、2(白い駒)にも名前を付けてみましょう。

次のように考えると思います。

int EMPTY = 0;
int BLACK = 1;
int WHITE = 2;

この方法でもできなくはないですが、enum使って書くと次のようになります。

    // enumを使って数字に名前をつける
    enum COLOR
    {
        EMPTY,  //空欄 = 0
        BLACK,  //黒色 = 1
        WHITE   //白色 = 2
    }

COLORという名前のenum(連番)です。

,(カンマ)で区切るだけで0から順番に数字を割り当てることができます。

また、enumを使うときには以下のようになります。

COLOR[,] board = new COLOR[8, 8]; // 8x8の長さのCOLOR型の2次元配列
board[0,0] = COLOR.EMPTY

注意したいのが、enumはintとは型が違うという点です。

なのでintではなく、COLOR型として宣言する必要があります。

これで、数字に名前が付いて読みやすいコードを書くことができるようになりました。

盤面の初期値を設定する関数を作る

enumの次は関数というワードが出てきました。

まずは関数について簡単に説明します。

なぜ関数を使うのか

盤面を初期状態にする処理を作るのですが、その前に盤面を初期化する時はどんな場面かを考えてみましょう。

  1. ゲーム開始時
  2. リセット時
  3. 終わった後にもう一回始める時

このように盤面を初期化する場面は複数あります。

3か所すべてに同じ処理をしますが、同じプログラムを3回も書くのは非効率ですし、1か所で不具合が見つかった場合、他の2か所も修正していく必要があります。

このように何度も同じ処理を使う場合に、関数を使うのが有効です。

この例では、初期状態にする処理を関数化しておくと、必要な場面で毎回処理を書かなくても関数を呼び出すだけでよくなります。

何度も使う処理は関数化しておくようにしましょう。

初期値を設定する関数を作ってみる

それでは関数を作ってみましょう。

GameControllerの中に以下のコードを書きます。どこに書いてもいいですが今回はStartメソッドの下にしましょう。ついでにUpdateメソッドは削除してOKです。

    //盤面の初期値を設定
    void Initialize()
    {
        board[3, 3] = COLOR.WHITE;
        board[3, 4] = COLOR.BLACK;
        board[4, 3] = COLOR.BLACK;
        board[4, 4] = COLOR.WHITE;
    }

Initializeという名前の関数です。

処理の内容は盤面の中央4か所にそれぞれ黒と白の駒を指定していますね。

Initializeの前の「void」についてですが、これは関数の「返り値」が「なし」という意味です。

詳しくは、もう一つ関数を作って説明します。

COLORの値によって正しいPrefabを返す関数を作ってみよう

もう一つ関数を作って、返り値と引数(ひきすう)について説明します。

次の話になりますが、画面に盤面を表示させるとき、boardの配列を順番に見ていって、その値によって画面にPrefabを追加する処理を作ります。

例えばboard[0,0]=COLOR.EMPTYだった場合、画面に表示したいPrefabはemptyObjectですね。

また、COLOR.BLACKだった場合はblackObject、COLOR.WHITEの場合にはwhiteObjectを表示します。

そこでもう一つ作る関数は、boardの値(COLOR)によって正しいPrefabを取得する関数です。

以下がその関数です。

    //色によって適切なprefabを取得して返す
    GameObject GetPrefab(COLOR color)
    {
        GameObject prefab;
        switch (color)
        {
            case COLOR.EMPTY:   //空欄の時
                prefab = Instantiate(emptyObject);
                break;
            case COLOR.BLACK:   //黒の時
                prefab = Instantiate(blackObject);
                break;
            case COLOR.WHITE:   //白の時
                prefab = Instantiate(whiteObject);
                break;
            default:            //それ以外の時(ここに入ることは想定していない)
                prefab = null;
                break;
        }
        return prefab; //取得したPrefabを返す
    }

色々大変なことになっていますが、順番にみていきましょう。

まずは1行目の部分からです。

    GameObject GetPrefab(COLOR color)

関数の名前部分は前回と同じです。今回はGetPrefabという名前にしました。

次に、voidだった返り値の型がGameObjectに変わっていますね。

今回は適切なPrefabを取得する処理なので、処理の結果をGameObject型の変数を関数の呼び出し元に返す必要があります。

返り値の型は、返す変数の型をしていします。

今回はGameObject型を返すのでGameObjectとなっています。

次に、名前の右の()の中に「COLOR color」が追加されています。

これは呼び出し元から関数に渡す引数(ひきすう)というものです。

この例ではCOLOR型のcolorという名前の変数が渡されます。

このcolorの値によって、返すprefabを決めています。

次に関数の中身を見ていきましょう。中身は以下の構成になっています。

  1. GameObject prefabを宣言
  2. prefabの中身をswitch文で分岐
  3. return prefabで呼び出し元にprefabを返す

1つ目のGameObject prefabはわかると思います。GameObject型の変数prefabを宣言しています。

2つ目はswitch文を使った条件分岐です。長くなるので以下のアコーディオンで簡単に解説しています。

分岐の中身について詳しく

引数colorの値によって分岐しています。

以下の部分はcolorの値がCOLOR.EMPTYだった場合の処理です

 case COLOR.EMPTY:   //空欄の時
                prefab = Instantiate(emptyObject);
                break;

 

2行目の「prefab = Instantiate(emptyObject);」

でemptyObjectを生成してprefabに代入しています。

3行目の「break;」でswitch文を抜けます。

処理内容については他の条件も同様です。

(すみませんがswitch文の文法については各自検索をお願いします。)

3つ目はprefabを関数の呼び出し元に返す処理です。

「return 返り値」で返すことができます。

関数を呼び出す

ここまでくれば関数を呼び出すことは簡単です。

Startメソッド内でInitialize関数を呼び出してみましょう。

    void Start()
    {
        Initialize(); //盤面の初期値を設定
    }

これだけでInitialize関数を呼び出すことができます。関数名の横の()には関数に渡す引数が入りますが、Initialize関数は引数をとらないので空欄です。

もう一つのGetPrefab関数は次の見出しで使用します。

かなり詰め込んだのでポイントをまとめておきます。よくわからない部分はすみませんが検索するようにお願いします。

  • enum
  • 関数、返り値と引数、return
  • switch文

盤面を作る(後編)

手順
  1. 2次元配列をつくる
  2. 表示先のGameObjectを読み込む
  3. 表示先のGameObjectにprefabを並べる

長かったですが、これが最後です。

③表示先のGameObjectにprefabを並べるを解説していきます。

表示する処理を関数で作る

盤面を画面に表示する処理は何度も使うので、関数として作成します。

    //盤面を表示する
    void ShowBoard()
    {

    }

このShowBoard関数に処理を追加していきましょう。

まずはboardの中身を順番に見ていく処理です。

    //盤面を表示する
    void ShowBoard()
    {
        for (int v = 0; v < 8; v++) // vertical(垂直方向)のv
        {
            for (int h = 0; h < 8; h++) // horizontal(水平方向)のh
            {
                // boardの色に合わせて適切なPrefabを取得
                GameObject piece = GetPrefab(board[h, v]);
            }
        }
    }

「for(int v = 0; v < 8; v++)」という文が出てきましたが、これはfor文といいます。

for文は繰り返しをするときに使います。

(すみませんがfor文の文法については各自検索をお願いします。)

簡単に説明すると、int vの値が0~7の間繰り返す命令です。

1回繰り返すたびにvの値が1ずつ増えていきます。なので0~7の合計8回繰り返します。

その中でまたfor文がありますね。外側のfor文が1回ループするごとに内側のfor文が実行されます。

内側のfor文の中では、GetPrefab関数が呼び出されています。

GetPrefabはGameObject型を返すのでGetPrefabの結果をGameObject型変数pieceに代入しています。

GetPrefabの引数に「board[h, v]」とあります。for文のhとvの値を追っていくとわかりますが、board[0,0]からboard[7,7]まですべての値が順番に入るようになっています。

v=0の間にhが0~7まで繰り返し、次にv=1の間にhが0~7まで繰り返し・・・

この2重のfor文で二次元配列boardの値を順番に全て参照しています。

取得したprefabをBoardDisplayの子オブジェクトにする

これで、boardの値によって適切なprefabを取得することができました。

最後にこのprefabをBoardDisplayの子オブジェクトにしましょう。

    //盤面を表示する
    void ShowBoard()
    {
        for (int v = 0; v < 8; v++)
        {
            for (int h = 0; h < 8; h++)
            {
                // boardの色に合わせて適切なPrefabを取得
                GameObject piece = GetPrefab(board[h, v]);

                //取得したPrefabをboardDisplayの子オブジェクトにする
                piece.transform.SetParent(boardDisplay.transform);
            }
        }
    }

変数pieceが、取得したprefabですね。

このpieceをboardDisplayの子オブジェクトにするには次のように書きます。

 piece.transform.SetParent(boardDisplay.transform);

transformはオブジェクトの親子関係やオブジェクトの座標などの情報を持っています。

SetParentで親オブジェクトを指定できます。boardDisplayを親オブジェクトにしたいのでboardDisplayにします。ただし、SetParentはtarnsformを指定する必要があるのでboardDisplay.transformとしています。

あとは、このShowBoard関数をStartメソッドで呼び出しましょう。

    void Start()
    {
        Initialize(); //盤面の初期値を設定
        ShowBoard(); //盤面を表示
    }

ここまでできれば保存して実行すると画面に盤面が表示されるはずです。

ただ、今後のためにもう一つだけしておきたいことがありますので解説します。

定数を使う

定数はenumと似ていますが連番ではないところが違います。

今回は盤面の縦幅と横幅を定数で管理することにします。

    const int WIDTH = 8;
    const int HEIGHT = 8;

int のまえにconstを付けると定数にすることができます。定数にすると途中で値を変更することができません。今回の盤面の縦幅横幅のように、値が決まっている場合は定数にしてしまいましょう。

それに合わせて、GameController内で使われている8をそれぞれWIDTHとHEIGHTに置き換えてみましょう。ただ8と書くよりも何をしているのかわかりやすくなります。

最後に、GameControllerの全文を載せておきますので参考にしてください。

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

public class GameController : MonoBehaviour
{
    // enumを使って数字に名前をつける
    enum COLOR
    {
        EMPTY,  //空欄 = 0
        BLACK,  //黒色 = 1
        WHITE   //白色 = 2
    }

    const int WIDTH = 8;
    const int HEIGHT = 8;

    //黒い駒
    [SerializeField]         //変数をインスペクターに表示
    GameObject blackObject = null;  //blackObjectという名前のGameObjectを宣言
    //白い駒
    [SerializeField]
    GameObject whiteObject = null;  //whiteObjectという名前のGameObjectを宣言
    //盤
    [SerializeField]
    GameObject emptyObject = null;  //emptyObjectという名前のGameObjectを宣言

    //盤面のGameObject
    [SerializeField]
    GameObject boardDisplay = null;

    //盤面
    COLOR[,] board = new COLOR[WIDTH, HEIGHT]; // 8x8の2次元配列

    // Start is called before the first frame update
    void Start()
    {
        Initialize(); //盤面の初期値を設定
        ShowBoard(); //盤面を表示
    }

    //盤面の初期値を設定
    void Initialize()
    {
        board[3, 3] = COLOR.WHITE;
        board[3, 4] = COLOR.BLACK;
        board[4, 3] = COLOR.BLACK;
        board[4, 4] = COLOR.WHITE;
    }

    //盤面を表示する
    void ShowBoard()
    {
        for (int v = 0; v < HEIGHT; v++)
        {
            for (int h = 0; h < WIDTH; h++)
            {
                // boardの色に合わせて適切なPrefabを取得
                GameObject piece = GetPrefab(board[h, v]);

                //取得したPrefabをboardDisplayの子オブジェクトにする
                piece.transform.SetParent(boardDisplay.transform);
            }
        }
    }

    //色によって適切なprefabを取得して返す
    GameObject GetPrefab(COLOR color)
    {
        GameObject prefab;
        switch (color)
        {
            case COLOR.EMPTY:   //空欄の時
                prefab = Instantiate(emptyObject);
                break;
            case COLOR.BLACK:   //黒の時
                prefab = Instantiate(blackObject);
                break;
            case COLOR.WHITE:   //白の時
                prefab = Instantiate(whiteObject);
                break;
            default:            //それ以外の時(ここに入ることは想定していない)
                prefab = null;
                break;
        }
        return prefab; //取得したPrefabを返す
    }
}

まとめ

かなり長くなってしまいましたが、これで盤面作成編が終わりました。

お疲れさまでした。

「初心者がゲームを完成させる方法」というコンセプトに対し、初めての人が理解するのにはかなり難しい内容だったと思います。実際かなり詰め込むことになりました。

私は自分で調べながら作れるようになることが「ゲームを完成できるようになる」ために必要なことだと思います。また、一人で作るならプログラミングは避けられません。

なのでUnityでリバーシを作るでは、記事を参考に必要なことを調べながら作ることを想定しています。

この先もこのような進め方になると思いますのでご理解ください。

次は駒を置く部分を作っていきます。

ここまで付いてきてくださった方、ありがとうございました。
Unityでリバーシを作る4(駒を置く編)どうも、りくらぼです。 前回は盤面を表示しました。 https://riku-lab.com/reversi3 今回は...
ABOUT ME
りくらぼ
ブログ歴2ヵ月です。「りくらぼ」では雑記のほか、趣味のUnity関連のなど書きたいことを書いていきます