Unityでリバーシを作る

Unityでリバーシを作る6(仕上げ編)

どうも、りくらぼです。

前回はひっくり返すを実装しました。

リバーシの要となる部分はすでに完成しています。

今回で細かな部分を仕上げて完成させます。

置ける場所を制限する

前回の状態では、駒を挟んでいない場所にも駒を置けるようになっていました。

修正して、駒を挟んでいる場合のみ置けるようにしましょう。

駒を挟んでいるかどうかはReverse関数を実行するまでわかりません。

Reverse関数の中に、1つ以上ひっくり返した場合の処理を追加します。

      //自分の駒だった場合
      if (board[x, y] == player)
      {
          //ひっくり返す
          int x2 = h + directionH, y2 = v + directionV;
          int count = 0; //カウント用の変数を追加
          while (!(x2 == x && y2 == y))
          {
              board[x2, y2] = player;
              x2 += directionH;
              y2 += directionV;
              count++;
          }
          //1つ以上ひっくり返した場合
          if (count > 0)
          {
              //駒を置く
              board[h, v] = player;
          }
          break;
      }

ひっくり返すwhile文の中で、count変数を1ずつ足していきます。

ひっくり返し終わった後、count > 0 つまり1つ以上ひっくり返していれば、駒を置く処理を行っています。

ここで駒を置くようになったので、PutStone関数の駒を置く部分は削除してください。

同時に、駒を置いた場合にのみ相手の番になるので、駒の色を変更する処理にも条件を付け加えます。

        //ひっくり返していれば相手の番、駒の色を変更する
        if (board[h, v] == player)
        {
            //駒の色を変更
            player = player == COLOR.BLACK ? COLOR.WHITE : COLOR.BLACK;
        }

ReverseAll関数の後、駒が置かれていれば1つ以上ひっくり返したということなので、駒の色を変更して相手の番にします。

これで、最低限の機能が完成しました!!!

やったーーー!(まだパスの機能はありません…)

リバーシとしては、最低限これで遊べるようになりました。

以降は、追加機能を充実させていきます。

プレイヤーの番を表示する

今は盤面しか表示がないので白黒どちらの番かわかりにくいです。

次がどちらの番なのかを表示してみましょう。

まずは以下の手順でテキストを作成します。

手順
  1. Canvasに新しくテキストを追加する
  2. テキストに「次の番:黒」と入力する
  3. フォントサイズと位置を調整する

私は以下の画像のようにしてみました。表示する文字や表示位置などは好みで。

create_text

このテキストの文字列をスクリプトで変更していきます。

Scriptsファイル内に新しくC#スクリプトを作成しましょう。

私は「NextPlayerText」という名前にしました。

ここで、なぜスクリプトを分けるのかについて簡単に説明しておきます。

今まで編集してきたGameControllerはあくまでゲームの根幹の部分について制御するスクリプトとして作成しました。もちろん今から作成するNextPlayerTextの処理もGameControllerに追加していくことはできますが、コードが長くなればなるほど、何をしているファイルなのかわからなくなってしまいます。

また、後で編集したい箇所がでてきても変数が他で使われていないか、他のコードに影響が出ないかなどコード全体を見直す必要が出てきます。

なので、機能ごとにそれ専用のスクリプトファイルを作成して切り分けて実装していくことで他の機能に影響を与えにくく、編集もしやすいといったメリットがあります。(もちろんちゃんとデバッグをする必要はあります)

このあたりは自分でルールを決めて作っていくことをおすすめします。

処理内容としては次のように考えました。

手順
  1. GameControllerのplayer変数を取得する
  2. textコンポーネントのtextの値をplayer変数によって書き換える

GameControllerのplayer変数を取得する

まずはGameControllerのplayer変数を取得する部分を作成しましょう。

    GameController gameController;
    Text text;
    // Start is called before the first frame update
    void Start()
    {
        gameController = GameObject.Find("GameManager").GetComponent<GameController>();
        text = this.GetComponent<Text>();
    }

まずGameController型の変数gameControllerとText型の変数textを宣言します。

次にStartメソッドでGameManagerにアタッチされているGameControllerを取得、

NextPlayerTextがアタッチされているGameObjectからTextコンポーネントを取得、

それぞれ宣言した変数に代入しています。

また、Textを扱うために次の1行を追加する必要があります。

using UnityEngine.UI;

textコンポーネントのtext値を書き換える

gameControllerからplayer変数の値を取得したいのですが、GameControllerではplayerのアクセス修飾子がpublicになっていないのでNextPlayerTextからは見ることができません。

GameController内を2か所修正します。

    public COLOR player = COLOR.BLACK;
    public enum COLOR
    {
        EMPTY,  //空欄 = 0
        BLACK,  //黒色 = 1
        WHITE   //白色 = 2
    }

先頭部分にpublicをつけました。これでplayer変数に外からアクセスできるようになります。

次にNextPlayerTextのUpdateメソッドにテキストを書き換える処理を追加します。

    void Update()
    {
        string colorText = "";
        switch (gameController.player)
        {
            case GameController.COLOR.BLACK:
                colorText = "黒";
                break;
            case GameController.COLOR.WHITE:
                colorText = "白";
                break;
            default:
                break;
        }
        text.text = "次の番:" + colorText;
    }

gameControllerのplayerの値によってtextコンポーネントのtextの値を書き換えます。

GameController.COLOR.○○の部分はGameControllerで宣言したenumのCOLORの値です。playerの型はCOLORなのでそれに合わせています。

これでプレイヤーの番を表示することができました。

今回はアクセス修飾子をpublicにすることでplayer変数の値を参照しましたが、publicを付けると外からも値を書き換えることができてしまい、想定外のバグの原因となり得ます。他のプログラムから変数の値を書き換えられたくない場合には次の関数を作成する方法があります。

    public COLOR GetPlayer()
    {
        return player;
    }

playerのアクセス修飾子はpublicにせず、playerの値を返す関数を作り、そのアクセス修飾子をpublicにすることでplayerの値だけを他のプログラムに渡すことができます。

詳しくは「getter setter」で検索するといいかもしれません。

リセットボタンを作る

今の状態では最後まで終わったあと再起動しないと盤面が初期配置に戻りません。

盤面を初期配置に戻すリセットボタンを作成しましょう。

以下の手順でボタンを追加します。

手順
  1. Canvasに新しくボタンを追加する
  2. ボタンのテキストに「リセット」と入力する
  3. サイズと位置を調整する
②補足:ボタンのテキストは追加したButtonの子オブジェクトにあります。

私は以下の画像のようにしてみました。

create_button

盤面を初期化する手順は以下のように考えています。

手順
  1. 初期化する関数を作成する
  2. ボタンのクリックイベントに関数を設定する

初期化する関数を作成する

実はすでに作ってありますね。

GameControllerのInitialize関数です。

ただ、今のままでは不十分なので少し追加します。

    //盤面の初期値を設定
    public void Initialize() //publicを追加
    {
        board = new COLOR[WIDTH, HEIGHT]; //追加
        board[3, 3] = COLOR.WHITE;
        board[3, 4] = COLOR.BLACK;
        board[4, 3] = COLOR.BLACK;
        board[4, 4] = COLOR.WHITE;
        player = COLOR.BLACK; //追加
        ShowBoard(); //追加
    }

以下4つを追加しました。

  • ボタンのクリックイベントとして参照できるようにpublicを付けます。
  • 盤面に置かれている駒をすべて空欄にするためにboard変数を初期化します。
  • playerをCOLOR.BLACK(初期値)にします。
  • ShowBoard関数を呼び出して画面に反映します。

Initialize関数の中でShowBoard関数を呼ぶようになったので、StartメソッドのShowBoard()は削除してください。

これで関数が完成しました。

ボタンのクリックイベントに関数を設定する

今回、ボタンのクリックイベントを設定するのは「盤面に駒を置く」の方法とは異なります。

以前はスクリプトからイベントを指定しましたが、今回はインスペクターから指定します。

以下の画像を参考にボタンコンポーネントのクリック時()を追加しましょう。

add_event1

次にGameManagerをドラッグ&ドロップします。

add_event2

次に、呼び出す関数を指定します。

No Functionとなっている部分をGameControllerのInitialize()に変更します。

add_event3

これでボタンのクリックイベントが設定できました。

普通はこの方法でクリックイベントを設定しますが、スクリプトから設定する方法が先になってしまいました。

パス・勝敗を判定する

最後に、パスと勝敗の判定をできるようにします。

勝敗の判定は両方のプレイヤーがパスの時に行いたいので、先にパスの判定から作ります。

パスを判定する

パスになるのは、ひっくり返せる場所が1つもない場合です。

GameControllerにパスを判定する関数CheckPassを作りました。

    //パスを判定する
    bool CheckPass()
    {
        for (int v = 0; v < HEIGHT; v++)
        {
            for (int h = 0; h < WIDTH; h++)
            {
                //board[h, v]が空欄の場合
                if (board[h, v] == COLOR.EMPTY)
                {
                    COLOR[,] boardTemp = new COLOR[WIDTH, HEIGHT]; //盤面保存用の変数を宣言
                    Array.Copy(board, boardTemp, board.Length); //盤面の状態を保存用変数に保存しておく
                    ReverseAll(h, v); //座標h,vに駒を置いたとしてひっくり返してみる

                    //ひっくり返せればboard[h, v]に駒が置かれている
                    if (board[h, v] == player)
                    {
                        //ひっくり返したのでパスではない
                        board = boardTemp; //盤面をもとに戻す
                        return false;
                    }
                }
            }
        }
        //1つもひっくり返せなかった場合パス
        return true;
    }

メモリの関係で、配列のコピーには工夫が必要です。

今回はArray.Copyを使用しました。Array.Copyを使うには以下の1行を追加する必要があります。

using System;

これでパスの場合はtrue、パスでない場合はfalseを返す関数CheckPassが完成しました。

PutStone関数にCheckPass関数の処理を追加します。

        //ひっくり返していれば相手の番、駒の色を変更する
        if (board[h, v] == player)
        {
            //駒の色を相手の色に変更
            player = player == COLOR.BLACK ? COLOR.WHITE : COLOR.BLACK;
            //相手がパスか判定
            if (CheckPass())
            {
                //相手がパスの場合、駒の色を自分の色に変更
                player = player == COLOR.BLACK ? COLOR.WHITE : COLOR.BLACK;
            }
        }

駒の色を相手の色に変更した後、CheckPass関数を呼び出して相手がパスかどうかを判定します。

相手がパスの場合は駒の色を自分の色に戻して、再び自分の番になります。

これでパスが実装できました。

勝敗を判定する

パスの処理が完成したので、勝敗の判定も作りましょう。

まずは勝敗を表示するためのテキストを作成します。

CanvasにResultTextを作成しましょう。私は以下のようにしました。

create_result_text

文字列は位置確認のために入力しただけなので空欄にしておいてくださいね。

GameControllerにresultText変数を作ってインスペクターからResultTextを指定します。

    //勝敗を表示するテキスト
    [SerializeField]
    Text resultText = null;
set_result_text

それでは勝敗を判定する関数を作りましょう。

    //勝敗を判定する
    void CheckGame()
    {
        int black = 0;
        int white = 0;

        //駒の数を数える
        for (int v = 0; v < HEIGHT; v++)
        {
            for (int h = 0; h < WIDTH; h++)
            {
                switch (board[h, v])
                {
                    case COLOR.BLACK:
                        black++; //黒をカウント
                        break;
                    case COLOR.WHITE:
                        white++; //白をカウント
                        break;
                    default:
                        break;
                }
            }
        }

        if (black > white)
        {
            resultText.text = "黒"+black+":白"+white+"で黒の勝ち";
        }
        else if (black < white)
        {
            resultText.text = "黒" + black + ":白" + white + "で白の勝ち";
        }
        else
        {
            resultText.text = "黒" + black + ":白" + white + "で引き分け";
        }
    }

黒と白の数をカウントして、結果を表示する関数です。

PutStone関数の中にこの関数を呼び出す処理を追加しましょう。

        //ひっくり返していれば相手の番
        if (board[h, v] == player)
        {
            //駒の色を相手の色に変更
            player = player == COLOR.BLACK ? COLOR.WHITE : COLOR.BLACK;
            //相手がパスか判定
            if (CheckPass())
            {
                //相手がパスの場合、駒の色を自分の色に変更
                player = player == COLOR.BLACK ? COLOR.WHITE : COLOR.BLACK;

                //自分もパスか判定
                if (CheckPass())
                {
                    //自分もパスだった場合、勝敗を判定
                    CheckGame();
                }
            }
        }

ちょっとややこしいですが、相手と自分が両方パスになった場合にCheckGame関数を呼び出す処理です。

これで、勝敗が決まったタイミングで結果が表示されます。

もし、リセットボタンを作成している場合はリセット時の処理も追加する必要がありますね。

Initialize関数の中に次の1行を追加してresultTextの文字列を空欄にしましょう。

        resultText.text = "";

これで、勝敗の判定処理が完成しました!!

おわりに

これで、「Unityでリバーシを作る」が完成しました!

説明しきれていない箇所はたくさんありますが、自分で調べながら作れるようになるというテーマで始めたので、せめて「何を調べればいいか」のヒントになればと思います。

今回はUnityのUI機能を使って作りましたが、3Dで作ることもできますし、2Dで作ることもできます。スクリプトの書き方や作成する処理もたくさん方法があります。

同じゲームでもいろいろな作り方があるので、1つの方法にこだわらずに試行錯誤して一番いい方法を見つけながら進めていくと詰まりにくくなると思います。

また、Unityに慣れるという部分では、今回作成したリバーシを色々改造してみるのも有効だと思います。その途中でわからない部分を調べて身につけていくと、徐々にわかってきて力が付きます。

ここまで読んで頂いた方、ありがとうございました!

最後にGameControllerの全文を載せておきますので参考になればと思います。

長いので注意してください。

コードの全文を表示

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

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

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

    public COLOR player = COLOR.BLACK;

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

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

    //勝敗を表示するテキスト
    [SerializeField]
    Text resultText = null;

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

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

    //盤面の初期値を設定
    public void Initialize() //publicを追加
    {
        player = COLOR.BLACK;
        resultText.text = "";
        board = new COLOR[WIDTH, HEIGHT]; //追加
        board[3, 3] = COLOR.WHITE;
        board[3, 4] = COLOR.BLACK;
        board[4, 3] = COLOR.BLACK;
        board[4, 4] = COLOR.WHITE;
        ShowBoard(); //盤面を表示
    }


    //盤面を表示する
    void ShowBoard()
    {
        //boardDisplayの全ての子オブジェクトを削除
        foreach (Transform child in boardDisplay.transform)
        {
            Destroy(child.gameObject); //削除
        }

        //boardDisplayにGameObjectを追加
        for (int v = 0; v < HEIGHT; v++)
        {
            for (int h = 0; h < WIDTH; h++)
            {
                // boardの色に合わせて適切なPrefabを取得
                GameObject piece = GetPrefab(board[h, v]);

                //値がEMPTYならpieceに押下時のイベントを設定
                if (board[h, v] == COLOR.EMPTY)
                {
                    //座標を一時的に保持
                    int x = h;
                    int y = v;
                    //pieceにイベントを設定
                    piece.GetComponent<Button>().onClick.AddListener(() => { PutStone(x + "," + y); });
                }

                //取得した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を返す
    }

    //駒を置く
    public void PutStone(string position)
    {
        //positionをカンマで分ける
        int h = int.Parse(position.Split(',')[0]);
        int v = int.Parse(position.Split(','));
        //ひっくり返す
        ReverseAll(h, v);
        //ひっくり返していれば相手の番
        if (board[h, v] == player)
        {
            //駒の色を相手の色に変更
            player = player == COLOR.BLACK ? COLOR.WHITE : COLOR.BLACK;
            //相手がパスか判定
            if (CheckPass())
            {
                //相手がパスの場合、駒の色を自分の色に変更
                player = player == COLOR.BLACK ? COLOR.WHITE : COLOR.BLACK;

                //自分もパスか判定
                if (CheckPass())
                {
                    //自分もパスだった場合、勝敗を判定
                    CheckGame();
                }
            }
        }
        ShowBoard();
    }

    //全方向にひっくり返す
    void ReverseAll(int h, int v)
    {
        Reverse(h, v, 1, 0);  //右方向
        Reverse(h, v, -1, 0); //左方向
        Reverse(h, v, 0, -1); //上方向
        Reverse(h, v, 0, 1);  //下方向
        Reverse(h, v, 1, -1); //右上方向
        Reverse(h, v, -1, -1);//左上方向
        Reverse(h, v, 1, 1);  //右下方向
        Reverse(h, v, -1, 1); //左下方向
    }

    //1方向にひっくり返す
    void Reverse(int h, int v, int directionH, int directionV)
    {
        //確認する座標x, yを宣言
        int x = h + directionH, y = v + directionV;

        //挟んでいるか確認してひっくり返す
        while (x < WIDTH && x >= 0 && y < HEIGHT && y >= 0)
        {
            //自分の駒だった場合
            if (board[x, y] == player)
            {
                //ひっくり返す
                int x2 = h + directionH, y2 = v + directionV;
                int count = 0; //カウント用の変数を追加
                while (!(x2 == x && y2 == y))
                {
                    board[x2, y2] = player;
                    x2 += directionH;
                    y2 += directionV;
                    count++;
                }
                //1つ以上ひっくり返した場合
                if (count > 0)
                {
                    //駒を置く
                    board[h, v] = player;
                }
                break;
            }
            //空欄だった場合
            else if (board[x, y] == COLOR.EMPTY)
            {
                //挟んでいないので処理を終える
                break;
            }

            //確認座標を次に進める
            x += directionH;
            y += directionV;
        }
    }

    //パスを判定する
    bool CheckPass()
    {
        for (int v = 0; v < HEIGHT; v++)
        {
            for (int h = 0; h < WIDTH; h++)
            {
                //board[h, v]が空欄の場合
                if (board[h, v] == COLOR.EMPTY)
                {
                    COLOR[,] boardTemp = new COLOR[WIDTH, HEIGHT]; //盤面保存用の変数を宣言
                    Array.Copy(board, boardTemp, board.Length);    //盤面の状態を保存用変数に保存しておく
                    ReverseAll(h, v);                              //座標h,vに駒を置いたとしてひっくり返してみる

                    //ひっくり返せればboard[h, v]に駒が置かれている
                    if (board[h, v] == player)
                    {
                        //ひっくり返したのでパスではない
                        board = boardTemp; //盤面をもとに戻す
                        return false;
                    }
                }
            }
        }
        //1つもひっくり返せなかった場合パス
        return true;
    }

    //勝敗を判定する
    void CheckGame()
    {
        int black = 0;
        int white = 0;

        //駒の数を数える
        for (int v = 0; v < HEIGHT; v++)
        {
            for (int h = 0; h < WIDTH; h++)
            {
                switch (board[h, v])
                {
                    case COLOR.BLACK:
                        black++; //黒をカウント
                        break;
                    case COLOR.WHITE:
                        white++; //白をカウント
                        break;
                    default:
                        break;
                }
            }
        }

        if (black > white)
        {
            resultText.text = "黒"+black+":白"+white+"で黒の勝ち";
        }
        else if (black < white)
        {
            resultText.text = "黒" + black + ":白" + white + "で白の勝ち";
        }
        else
        {
            resultText.text = "黒" + black + ":白" + white + "で引き分け";
        }
    }

}

ABOUT ME
りくらぼ
ブログ歴2ヵ月です。「りくらぼ」では雑記のほか、趣味のUnity関連のなど書きたいことを書いていきます