ひとりでのアプリ開発 - fineの備忘録 -

ひとりでアプリ開発をするなかで起こったことや学んだことを書き溜めていきます

Unity - オセロの作り方

初めに

 この記事は、Unityで簡単なオセロゲームの作る方法を紹介します。

今回作るもの



 今回、作成するオセロは次のようなものにします。

  • シーンがスタートしたら盤面を初期化し、盤を表示
  • 黒石のオブジェクトと白石のオブジェクトと空欄のオブジェクトを用意し、盤面に配置する
  • 石を置くことができるマスをクリックしたら、石を配置
  • 石を配置したら、攻守のチェック
  • 石が置けなくなったら、勝敗のチェック
  • 現在の黒石、白石の個数と番手を表示する
  • 勝敗がついたら、リトライボタンを表示する
  • リトライボタンが押されたら、盤面を初期化

スクリプトの作成

 オセロを作るためのスクリプトを作成します。

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

public class GameController : MonoBehaviour
{

    public enum COLOR
    {
      EMPTY, //EMPTY = 0
      BLACK, //BLACK = 1
      WHITE  //WHITE = 2
    };

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

    [SerializeField] GameObject boardDisplay = null;
    [SerializeField] GameObject blackObject = null;
    [SerializeField] GameObject whiteObject = null;
    [SerializeField] GameObject emptyObject = null;

    COLOR[,] board = new COLOR[WIDTH,HEIGHT]; //盤面
    public COLOR player = COLOR.BLACK;

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

    //Retryボタン
    [SerializeField] GameObject retryButton = null;

    //StageSelectボタン
    [SerializeField] GameObject stageSelectButton = null;

    //SEObject
    [SerializeField] GameObject SEObject = null;

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

    //初期化の関数, 盤面の初期状態を設定
    public void Initialize()
    {
      resultText.text = "";
      retryButton.SetActive(false);
      stageSelectButton.SetActive(false);
      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();
    }

    void ShowBoard()
    {
      foreach (Transform child in boardDisplay.transform)
      {
        Destroy(child.gameObject); //削除
      }

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

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

      CurrentCheck();
    }

    //色によって適切な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;
    }

    //駒を置く関数
    public void PutStone(string position)
    {
      //positionをカンマで分ける
        int h = int.Parse(position.Split(',')[0]);
        int v = int.Parse(position.Split(',')[1]);

        SEObject.GetComponent<SEManager>().RingPutSE();
        
        ReverseAll(h, v);
        ShowBoard();
        //ひっくり返していれば相手の番, 駒の色を変更
        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();
            }
          }
        }
    }

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

    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); //左下方向
    }

    // パスになるか否かの判定
    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 win";
        }
        else if (black < white)
        {
            resultText.text = "White win";
        }
        else
        {
            resultText.text = "Draw";
        }

        retryButton.SetActive(true);  // リトライ用ボタンを表示
        stageSelectButton.SetActive(true);  //セレクト画面シーンへ移るためのボタンを表示
    }

    // 盤面の駒数を数えて, text に表示
    void CurrentCheck()
    {
      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;
              }
            }
        }

        currentText.text = "Black : " + black + "\nWhite : " + white;
    }

    // リトライボタン用, 盤面を初期化する
    public void onClickRetry()
    {
      Initialize();
    }

    // 選択画面シーンのロード, 別のシーンを作らない場合は不要
    public void onClickSelect()
    {
      SceneManager.LoadScene("Select");
    }
}

オブジェクトの準備

石、空欄用オブジェクト
空欄

 Image と Button で作成します。関数はスクリプトから Addlisner で割り当てるので、Inspector から Button に割り当てることはしません。作成したオブジェクトは Prefab 化しておきます。

 オブジェクトのサイズは画面サイズや次に作成する盤の大きさを踏まえて調整します。


 石、空欄用オブジェクトを64マスに配置するための盤を作成します。Grid Layout Group を使って整列するようにします。Cell Size は画面サイズを踏まえて調整します。

他のオブジェクトの作成

 背景や勝敗を表示するテキスト、石の個数を表示するテキスト、リトライボタンなど必要なオブジェクトを作成します。

 先ほど作成したスクリプトを GameManager と名前を付けた空のゲームオブジェクトにアタッチし、オブジェクトを指定していきます。

 リトライボタンには、GameManager から onCliclRetry 関数を割り当てておきましょう。

テスト



 テストをし、正常に動けば ok です。

  • 正常に石の色が変わるか
  • 黒が勝った場合
  • 白が勝った場合
  • 石の個数が正常に表示されているか
  • パスが正常に動作するか
  • 石が置けなくなったら勝敗を判定されるか

youtu.be