【Unity】五目並べの作り方

ゲーム開発
UnsplashJESHOOTS.COMが撮影した写真

あのゲームのあの機能ってどうやって作るんだろう?を考えていきます。
効率のいい作り方などはわからないので、そういったものをお求めの方は回れ右。

今回は五目並べ

こういうやつ

まずは白黒の石とボード用に2枚の四角の画像を用意します。ただ塗りつぶしているだけなので詳細は割愛。

ちなみに五目並べが作りたかったわけではなく、パズルゲームを作るために二次元配列の処理の仕方などの基本を学べそうだったので調べてみた次第です。ここ数年だとBaba Is Youとかめちゃすごいなと思いました。

Steam:Baba Is You
「Baba Is You」はゲーム上で従われるルールがステージ内にある接触可能のブロックとして存在するゲームです。全てのステージんいおいて、ルール自体が接触可能なブロックとして配置されています。そのブロックを操作することでステージの原則も変...

以下のサイトを参考にして作成していましたが、どうも斜めの判定を行う部分がうまく機能しないようだったので他のサイトも参考にしました。(後述)

【Unity 入門】2時間で作る五目並べゲーム! – XR-Hub
「プログラミングを学びたいけど何から始めれば良いかわからない…」そんな方にオススメな方法がUnityでゲームを作りながらプログラミングを学ぶ方法です。 今回はUnityを使って「五目並べ」を...

コードはこちら

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

public class GameController : MonoBehaviour
{

    private int boardSize;
    private int[,] boards;

    private const int EMPTY = 0;
    private const int WHITE = 1;
    private const int BLACK = -1;

    [SerializeField] GameObject whiteStone;
    [SerializeField] GameObject blackStone;

    //初期値は白
    private int currentPlayer = WHITE;
    private const string WHITETURN = "白のターン";
    private const string BLACKTURN = "黒のターン";

    [SerializeField] GameObject button;
    [SerializeField] TextMeshProUGUI turnText;
    [SerializeField] TextMeshProUGUI winner;

    //float maxDistance = 10;

    public Camera cameraObject;

    void Start()
    {
        boardSize = 10;
        boards = new int[boardSize, boardSize];
        turnText.text = WHITETURN;
        button.SetActive(false);
    }

    void Update()
    {
        if (checkVertical(WHITE) || checkVertical(BLACK)){ return; }
        if (checkHorizontal(WHITE) || checkHorizontal(BLACK)) { return; }
        if (checkLowerLeftToUpperRight(WHITE) || checkLowerLeftToUpperRight(BLACK)) { return; }
        if (checkUpperLeftToLowerRight(WHITE) || checkUpperLeftToLowerRight(BLACK)) { return; }
        if (Input.GetMouseButtonDown(0))
        {
            Ray ray = cameraObject.ScreenPointToRay(Input.mousePosition);
            RaycastHit2D hit = Physics2D.Raycast((Vector2)ray.origin, (Vector2)ray.direction);

            if (!hit.collider){ return; }
            int x = (int)hit.collider.gameObject.transform.position.x;
            int y = (int)hit.collider.gameObject.transform.position.y;

            if (x < 0) { return; }
            if (y < 0) { return; }

            if (boards[x, y] != EMPTY) {return;}

            if (currentPlayer == WHITE)
            {
                boards[x,y] = WHITE;
                GameObject stone = Instantiate(whiteStone);
                stone.transform.position = new Vector3(x,y,0);
                currentPlayer = BLACK;
                turnText.text = BLACKTURN;
            }
            else if (currentPlayer == BLACK)
            {
                boards[x, y] = BLACK;
                GameObject stone = Instantiate(blackStone);
                stone.transform.position = new Vector3(x, y, 0);
                currentPlayer = WHITE;
                turnText.text = WHITETURN;
            }           
        }
    }
    private bool checkVertical(int color)
    {
        int consecutiveTimes = 0;
        for (int i = 0; i < 10; i++)
        {
            for (int j = 0; j < 10; j++)
            {
                if (boards[i, j] == EMPTY || boards[i, j] != color) { consecutiveTimes = 0; continue; }
                consecutiveTimes++;

                if (consecutiveTimes < 5) { continue; }

                declarationWinner(color);
                return true;
            }
        }
        return false;
    }

    private bool checkHorizontal(int color)
    {
        //横方向
        int consecutiveTimes = 0;
        for (int i = 0; i < 10; i++)
        {
            for (int j = 0; j < 10; j++)
            {
                if (boards[j, i] == EMPTY || boards[j, i] != color) { consecutiveTimes = 0; continue; }
                consecutiveTimes++;

                if (consecutiveTimes < 5) { continue; }

                declarationWinner(color);
                return true;
            }
        }
        return false;
    }

    private bool checkLowerLeftToUpperRight(int color)
    {
        int consecutiveTimes = 0;
        for (int i = -(boardSize - 1); i <= boardSize - 1; i++)
        { 
            for (int j = 0; j < boardSize; j++)
            {
                //範囲を超えてたらリセット
                if (0 > i + j || i + j >= boardSize){ consecutiveTimes = 0; continue; }
                //対象の石じゃないときはリセット
                if (boards[i+j, j] != color) { consecutiveTimes = 0; continue; }
                consecutiveTimes++;
                if (consecutiveTimes < 5) { continue; }
                declarationWinner(color);
                return true;
            }
        }
        return false;
    }

    private bool checkUpperLeftToLowerRight(int color)
    {
        int consecutiveTimes = 0;
        for (int i = 0; i <= 2 * (boardSize - 1); i++)
        { 
            for (int j = 0; j < boardSize; j++)
            {
                if (0 > i - j || i - j >= boardSize) { consecutiveTimes = 0; continue; }
                
                if (boards[i-j, j] != color) { consecutiveTimes = 0; continue; }
                consecutiveTimes++;
                if (consecutiveTimes < 5) { continue; }
                declarationWinner(color);
                return true;
            }
        }
        return false;
    }



    private void declarationWinner(int color)
    {
        if (color == WHITE)
        {
            winner.text = "白の勝ち";
        }
        //黒のとき
        else
        {
            winner.text = "黒の勝ち";
        }
        button.SetActive(true);
        return;
    }
}

主要な部分解説

48~51行目あたり

まず石を置く処理が必要になりますが、これにはRaycastという指定した場所(クリックした位置)から光線を出して、その光がコライダーにあたったらそのオブジェクトの情報を取得できる仕組みを使います。
ちなみに2Dと3Dで使う関数が異なりますので注意が必要です。

これを何に使うかというと、盤面や石をクリックしたかどうかを判別するためのものになります。RaycastHit2D hitの中にcolliderの情報が含まれているので、もしなければ早期returnしてそれ以上処理が進まないようにしてしまいます

Physics-Raycast - Unity スクリプトリファレンス
Casts a ray, from point origin, in direction direction, of length maxDistance, against all colliders in the Scene.

60~74行目あたり

石を置くためにクリックしたx,y座標に、1か-1(WHITEかBLACK)を最初に定義したboardsの中に入れて、石をInstantiate()で生成します。

78行目以降

縦・横・左下から右上・左上から右下の方向へ石が5つ並んでいるかをチェックする関数としてまとめました。
縦横はfor文を入れ子にすることで簡単にできますが、斜めの動きは少し考える必要があります。

以下のサイトを参考にしました。こういうアルゴリズムが自然と思い浮かぶようになりたいものです。

最後にそれをupdate()の中でチェックして、石が5つ並んだらゲームが止まるようになっています。その時にリセットするためのボタンを表示して、ボタンを押したらシーンがもう一度呼び出されます。

Unity側の設定

GameManagerオブジェクトを作って、先程作ったGameControllerスクリプトを追加します。そのあと石やボタン、カメラなどをそれぞれ設定します。

石を置くためのボードは色が交互になるように適当に並べています。重要なのは左下マスのPositionがX0,Y0になるように、そこから右に並べるたびにXが1増え、上に並べるたびにYが1増えるように調整することくらいですかね。一番右上が結果的にX9,Y9になっていればOK。
特に並べたあとは操作も何もしないので名前や順番も雑になっていますが、きれいに整理したほうが良いですね。見た目もそうですが、修正するときにわかりやすくなるので。

追記

やっぱりマスは自動で生成することにしました。空のオブジェクトを追加して名前をつけます(ここではGridGeneratorとしました)

ChatGPTにこのように問い合わせてみます。

using UnityEngine;

public class GridGenerator : MonoBehaviour
{
    public GameObject blackPrefab; // Black prefab.
    public GameObject whitePrefab; // White prefab.

    private void Start()
    {
        GenerateGrid(9, 9, 1, 1);
    }

    private void GenerateGrid(int width, int height, float xScale, float yScale)
    {
        for (int i = 0; i < height; i++)
        {
            for (int j = 0; j < width; j++)
            {
                // Determine the prefab to use based on the sum of i and j.
                GameObject prefabToUse = ((i + j) % 2 == 0) ? blackPrefab : whitePrefab;

                // Instantiate the prefab at the right position.
                GameObject instance = Instantiate(prefabToUse, new Vector3(j * xScale, i * yScale, 0), Quaternion.identity);
                instance.transform.localScale = new Vector3(xScale, yScale, 1);
            }
        }
    }
}

あとはprefabを追加するだけ

完成(gif)

タイトルとURLをコピーしました