Unityでリバーシを作る

Unityでリバーシを作る5(ひっくり返す編)

どうも、りくらぼです。

前回は駒を置けるようにしました。

Unityでリバーシを作る4(駒を置く編)どうも、りくらぼです。 前回は盤面を表示しました。 https://riku-lab.com/reversi3 今回は...

今回はひっくり返す処理を実装していきます。

ひっくり返す手順を考えるところからやっていきましょう。

ひっくり返す手順を考える

リバーシのルールでは、自分の色で相手の色を挟むと相手の駒をひっくり返して自分の色にすることができます。

このルールを実装するために、まずはひっくり返すまでの手順を考えてみましょう。

手順は大きく3段階あります。

  1. 駒を置く
  2. ひっくり返せる駒を探す
  3. ひっくり返す

①は前回の記事で作成しました。

駒が置かれた場所がわかれば、そこから上下左右斜めの8方向に盤面をみて、相手の駒を挟んでいるか確認します。この動作が②になります。

もし相手の駒を挟んでいれば③ひっくり返します。

これが駒を置いてからひっくり返すまでの手順です。

ここから詳しく見ていきます。

ひっくり返せる駒を探す手順

ひっくり返せる駒を探すにはどうすればいいでしょうか。

以下の画像の例で考えてみます。

reverse1

Aが置かれた駒です。Aが白の駒を挟んでいる場合の条件は以下です。

相手の駒の先に自分の駒がある

Aがこの条件を満たしているか確認します。確認する方向は8方向あるので、まずは右方向に確認してみます。

右方向(挟んでいる場合)

  • 右方向に見た時、相手の駒がある(画像の①)
  • 相手の駒の先に、自分の駒がある(画像の②)

これは相手の駒を挟んでいると言えますね。なので後述のひっくり返す処理を行います。

また、2つ以上の駒を挟んでいる可能性があるので自分の駒が見つかるまで①の確認を繰り返す必要があります。言い換えると以下のようになります。

  • ①を繰り返した後、自分の駒があれば「挟んでいる」
  • ①を繰り返した後、空欄または盤面のフチにきたら「挟んでいない」

上方向(挟んでいない場合1)

では、上方向はどうでしょうか。

人間は画像をみればすぐに上方向が空欄だとわかりますが、プログラムでは置かれた駒の座標しかわかりません。

上方向を確認するまで一つ上の盤面がどうなっているのか不明です。確認してみましょう。

上方向には駒がありませんね。上方向が相手の駒ではないので相手の駒を挟む条件を満たしません。

よって上方向にはひっくり返しません。

下方向(挟んでいない場合2)

次に、下方向はどうでしょうか。

下方向を確認したいのですが、下方向に盤面はありません。プログラムで言うと、長さが8×8の二次元配列に対して配列の外を確認しようとしている状態です。

配列の外を確認しようとすると配列外参照というエラーが発生してしまいます。

なので置かれた座標が盤面のフチなら確認しないという条件が必要とわかります。

省略しますが、他の方向についてもこれらのいずれかになりますね。

ひっくり返す手順

ひっくり返せる駒を探す手順がわかったので次はひっくり返す手順を考えます。

以下の画像を例に考えてみましょう。

reverse2

まず、置かれた駒Aの座標が[4, 7]です。これを変数h,vに置き換えて考えると白の駒、黒の駒の座標はそれぞれ次のように表せます。

白の駒……[h+1, v]、黒の駒……[h+2, v]

今、ひっくり返せる駒を探す手順で右方向に進め、[h+2, v]の盤を確認したところ、黒の駒だったとします。

「挟んでいる」ので[h, v]の次から[h+2, v]の前までをひっくり返します。

言い換えると[h+1, v]から[h+1, v]までをひっくり返す、つまり1つだけひっくり返すことになります。

これがひっくり返す手順です。

もう少し発展させて考える

これまでの例では右方向に進めた場合を考えましたが、実際には8方向に対して考える必要があります。

座標[h, v]から右方向に進むときにはhの値に+して[h+1, v],[h+2, v]のようにしましたが、進む方向を縦と横それぞれ数字で表してみます。

方向 横(directionH) 縦(directionV)
1 0
-1 0
0 1
0 -1
右上 1 1
左上 -1 1
右下 1 -1
左下 -1 -1

これを使って考えると駒の座標[h,v]から任意の方向に進むには

[h+directionH, v+directionV]と表すことができます。

ここまでをまとめて、駒を置いてからひっくり返す手順でした。

次は、この手順をプログラムで書いてみましょう。

ひっくり返す処理を実装する

順番にひっくり返す処理を実装していきます。

まずは関数を作りましょう。

    //1方向にひっくり返す
    void Reverse(int h, int v, int directionH, int directionV)
    {

    }

任意の方向にひっくり返す関数Reverseを作成しました。

引数h,vが駒が置かれた座標、directionH,directionVがひっくり返す方向です。

では挟んでいるか確認していく部分を追加します。

挟んでいるか確認していく部分を実装

    //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)
        {
            //確認座標を次に進める
            x += directionH;
            y += directionV;
        }
    }

まず、確認する座標x,yを宣言しました。

h,vが駒が置かれた座標なので、その次の座標となるh+directionH, v+directionVがx,yの初期値です。

次に、while文でループさせて確認する座標を1つずつ進めていきます。

ループが継続する条件は次の4つがすべて満たされていることです。

  1. x < WIDTH … x座標が盤面の右端から出ていない
  2. x >= 0 … x座標が盤面の左端から出ていない
  3. y < HEIGHT … y座標が盤面の下端から出ていない
  4. y >= 0 … y座標が盤面の上端から出ていない

つまり確認座標が盤面の中に納まっている間、ループが継続します。そして確認座標を次に進めていき、確認座標が盤面の外に出たら条件を満たさなくなってループが終了します。

挟んでいるか確認する処理を追加

このWhile文の中に挟んでいる場合の処理を追加します。

相手の駒を挟んでいる条件はこれでした。

相手の駒の先に自分の駒がある

この部分を実装しましょう。長くなるのでWhile文だけ抜き出しています。

        //挟んでいるか確認してひっくり返す
        while (x < WIDTH && x >= 0 && y < HEIGHT && y >= 0)
        {
            //自分の駒だった場合
            if (board[x, y] == player)
            {
                //ここにひっくり返す処理を書く
            }
            //空欄だった場合
            else if (board[x, y] == COLOR.EMPTY)
            {
                //挟んでいないので処理を終える
                break;
            }

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

if文でboard[x, y]を確認しています。

board[x, y]の値によって以下のように処理が分かれます。

  • 自分の駒だった場合…ひっくり返す
  • 空欄だった場合…挟んでいる条件を満たさないのでループを抜ける
  • 相手の駒だった場合…そのままループを継続する

自分の駒があればひっくり返す処理を行うのですが、間に相手の駒がなくてもひっくり返す処理を行います。

え?挟んでないじゃんと思われるかもしれませんが、この場合は0個挟んでいると考えることができます。

ひっくり返す処理を0回行えばいいのです。結局は1つもひっくり返さないのですが、「1つ以上相手の駒があるか?」という余計な処理を省けます。

余計な処理は省いてできるだけ簡単に書くことでコードの可読性が上がります。

ひっくり返す処理を追加

最後に、ひっくり返す処理を追加しましょう。

      //自分の駒だった場合
      if (board[x, y] == player)
      {
          //ひっくり返す
          int x2 = h + directionH, y2 = v + directionV;
          while (!(x2 == x && y2 == y))
          {
              board[x2, y2] = player;
              x2 += directionH;
              y2 += directionV;
          }
          break;
      }

自分の駒が見つかったら、駒を置いた座標から見つかった座標までを自分の駒にします。

これでひっくり返すことができます。

コード内では、安直ですが確認開始地点の座標をx2,y2としています。

x2,y2が確認座標x,yと同じになるまでwhile文のループを繰り返します。

ループ内ではboard[x2,y2]を自分の駒にし、x2,y2を次の座標に進めています。

そしてひっくり返す処理が終わったら、挟んでいない場合の処理と同様にbreakで処理を終えます。

これで、任意の方向に向かってひっくり返す処理が完成しました。

全方向にひっくり返していく

今作ったReverse関数は任意の1方向にのみひっくり返すことができる関数でした。

次は、Reverse関数を呼び出して全方向にひっくり返す関数を作ります。

    //全方向にひっくり返す
    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); //左下方向
    }

ReverseAll関数を作成しました。

引数には駒が置かれた座標h,vをとっています。

関数の中身は単純で、Reverse関数を方向を変えて8回呼び出しているだけです。

方向については「もう少し発展させて考える」で記載しました。

これで、全方向に対してひっくり返す処理の完成です。

ひっくり返す処理を呼び出す

最後に、作成したReverseAll関数を駒が置かれた時に呼び出しましょう。

前回作成したPutStone関数内で呼び出せばOKです。

    //駒を置く
    public void PutStone(string position)
    {
        //positionをカンマで分ける
        int h = int.Parse(position.Split(',')[0]);
        int v = int.Parse(position.Split(','));
        //クリックされた座標に駒を置く
        board[h, v] = player;
        //ひっくり返す
        ReverseAll(h, v);
        ShowBoard();
        //駒の色を変更
        player = player == COLOR.BLACK ? COLOR.WHITE : COLOR.BLACK;
    }

ReverseAll(h, v)が追加した箇所です。

これで駒を置いたときにひっくり返すことができるようになりました。

最後に

ここまで完成したら保存して実行してみましょう。

駒を挟むとひっくり返すことができると思います。

ひっくり返すといっても、コードでは「ひっくり返す」のではなく「盤面の変数を書き換える」という処理をしています。これでひっくり返したように見せています。実際に駒を裏返しているわけではありません。

リバーシ以外のゲームを作る際にも、同じような考え方ができます。例えばシューティングゲームで敵に弾を当てて破壊する処理があったとします。

しかし「敵を破壊する」というような命令文はないので破壊するように見せる処理を作成することを考えます。例えば発射した弾を削除する。敵のグラフィックを破壊時のものに変更する。爆発のエフェクトを表示する。効果音を鳴らす。これらを1つずつ実装して初めて「敵を破壊する」処理となります。このような考え方ができると検索するときに、必要なことが見つかりやすくなります。

例えば敵のグラフィックを変更する処理の作り方がわからない場合があったとします。そして敵のグラフィックはImageコンポーネントで表示していたとします。その場合には「Unity Image 変更」などで検索をかけると、具体的な方法やコードなど知りたい情報が手に入ります。安直に「敵グラフィック 変更方法」などと検索するよりも効果的です。

これができるようになることが、「初心者がゲームを作る方法」の絶対条件だと思います。

リバーシに話を戻します。

今のままでは駒をどこにでも置くことができてしまいますので次回はひっくり返せる場所にしか置けないようにする、勝ち負けの判定などルール部分を仕上げます。

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

Unityでリバーシを作る6(仕上げ編)どうも、りくらぼです。 前回はひっくり返すを実装しました。 リバーシの要となる部分はすでに完成しています。 今回で細か...
ABOUT ME
りくらぼ
ブログ歴2ヵ月です。「りくらぼ」では雑記のほか、趣味のUnity関連のなど書きたいことを書いていきます