Unityで作るシンプルマインスイーパー

2024年6月29日

こんにちは、ゲーム制作系VTuberのジェイです!今回はUnityを使って、シンプルなマインスイーパーを作成する手順をご紹介します。スクリプトの説明やエフェクトの追加、スコア管理まで、一通りの流れを分かりやすく解説しますので、ぜひ最後までお付き合いください。

ちなみに完成品はこちらから遊ぶことができます!

ステップ1: プロジェクトの作成

まずはUnity Hubから新しいプロジェクトを作成しましょう。

  1. Unity Hubを開き、新しいプロジェクトを作成します。
  2. プロジェクト名を「Minesweeper」とし、テンプレートとして「2D」を選択します。
  3. 「Create」ボタンをクリックしてプロジェクトを作成します。

ステップ2: スプライトの準備

ゲームに使用するスプライトを用意します。今回は、空のタイル、地雷、フラグ、数字1、2、3のタイルを使用します。

  1. スプライト画像をダウンロードし、Assetsフォルダ内にSpritesフォルダを作成してそこにインポートします。
  2. スプライト画像を選択し、InspectorウィンドウでSprite ModeをMultipleに変更します。
  3. Sprite Editorを開き、スプライトシートをそれぞれのタイルに分割します。

ステップ3: プレハブの作成

次に、タイルのプレハブを作成します。

  1. Hierarchyウィンドウで右クリックし、「2D Object」 -> 「Sprite」を選択します。
  2. スプライトオブジェクトをTileと名付け、InspectorウィンドウでSprote RenderコンポーネントのSpriteフィールドに空のタイルのスプライトを設定します。
  3. このスプライトオブジェクトをPrefabsフォルダにドラッグ&ドロップしてプレハブにします。

ステップ4: スクリプトの作成

Gridスクリプト

Assets/Scriptsフォルダに新しいスクリプトGrid.csを作成し、以下のコードを追加します。

Gird.csは、マインスイーパーのゲームグリッドを管理するスクリプトです。このスクリプトは、ゲームのタイルを生成し、ゲームオーバーの処理やスコア管理を行います。

using UnityEngine;
using System.Collections;

public class Grid : MonoBehaviour
{
    public int width = 10; // グリッドの幅
    public int height = 10; // グリッドの高さ
    public GameObject tilePrefab; // タイルのプレハブ
    private Tile[,] tiles; // タイルの配列
    private GameManager gameManager; // GameManagerの参照

    void Start()
    {
        gameManager = FindObjectOfType<GameManager>(); // GameManagerを見つける
        GenerateGrid(); // グリッドを生成する
    }

    void GenerateGrid()
    {
        tiles = new Tile[width, height]; // タイルの配列を初期化
        for (int x = 0; x < width; x++)
        {
            for (int y = 0; y < height; y++)
            {
                GameObject tile = Instantiate(tilePrefab, new Vector3(x, y), Quaternion.identity); // タイルを生成
                tile.transform.parent = transform; // タイルをグリッドの子オブジェクトに設定
                Tile tileComponent = tile.GetComponent<Tile>(); // Tileスクリプトを取得
                bool isMine = Random.value < 0.15f; // 15%の確率で地雷を配置
                tileComponent.Init(this, x, y, isMine); // タイルを初期化
                tiles[x, y] = tileComponent; // 配列にタイルを保存
            }
        }
    }

    public Tile GetTile(int x, int y)
    {
        if (x >= 0 && y >= 0 && x < width && y < height)
        {
            return tiles[x, y]; // 指定位置のタイルを取得
        }
        return null; // 範囲外の場合はnullを返す
    }

    public void GameOver()
    {
        Debug.Log("Game Over. Reveal all mines.");

        // 全ての地雷を表示する
        for (int x = 0; x < width; x++)
        {
            for (int y = 0; y < height; y++)
            {
                Tile tile = tiles[x, y];
                if (tile.isMine)
                {
                    tile.GetComponent<SpriteRenderer>().sprite = tile.mineSprite;
                }
            }
        }

        // 5秒後にリセット
        StartCoroutine(RestartAfterDelay(5f));
    }

    private IEnumerator RestartAfterDelay(float delay)
    {
        yield return new WaitForSeconds(delay);
        gameManager.ResetScore(); // スコアをリセット
        Restart(); // ゲームをリスタート
    }

    public void Restart()
    {
        // グリッドを再生成
        foreach (Transform child in transform)
        {
            Destroy(child.gameObject); // 既存のタイルを削除
        }
        GenerateGrid(); // 新しいグリッドを生成
    }

    public void AddScore(int points)
    {
        gameManager.AddScore(points); // スコアを追加
    }
}

Tileスクリプト

同様にして、Tile.csを作成し、以下のコードを追加します。

Tile.csは、各タイルの動作を管理するスクリプトです。このスクリプトは、タイルの状態(地雷かどうか、フラグが立てられているか、開かれているか)を管理し、タイルをクリックしたときの処理を行います。

using UnityEngine;

public class Tile : MonoBehaviour
{
    private Grid grid; // グリッドの参照
    public int x; // タイルのX座標
    public int y; // タイルのY座標
    public bool isMine; // タイルが地雷かどうか
    public bool isRevealed = false; // タイルが開かれているかどうか
    public bool isFlagged = false; // タイルにフラグが立てられているかどうか
    public Sprite[] emptySprites; // 空のタイル用のスプライト配列
    public Sprite mineSprite; // 地雷のスプライト
    public Sprite flagSprite; // フラグのスプライト
    public GameObject explosionEffectPrefab; // 爆発エフェクトのプレハブ
    public AudioClip explosionSound; // 爆発音
    public AudioClip flagSound; // フラグ音

    private AudioSource audioSource; // オーディオソースの参照

    public void Init(Grid grid, int x, int y, bool isMine)
    {
        this.grid = grid;
        this.x = x;
        this.y = y;
        this.isMine = isMine;
        audioSource = GetComponent<AudioSource>(); // オーディオソースを取得
    }

    void Update()
    {
        if (Input.GetMouseButtonDown(1)) // 右クリックを検出
        {
            Vector2 worldPoint = Camera.main.ScreenToWorldPoint(Input.mousePosition);
            RaycastHit2D hit = Physics2D.Raycast(worldPoint, Vector2.zero);

            if (hit.collider != null && hit.collider.gameObject == gameObject)
            {
                ToggleFlag(); // フラグを切り替える
            }
        }

        if (Input.GetMouseButtonDown(0)) // 左クリックを検出
        {
            Vector2 worldPoint = Camera.main.ScreenToWorldPoint(Input.mousePosition);
            RaycastHit2D hit = Physics2D.Raycast(worldPoint, Vector2.zero);

            if (hit.collider != null && hit.collider.gameObject == gameObject)
            {
                OnLeftClick(); // 左クリック時の処理を実行
            }
        }
    }

    private void OnLeftClick()
    {
        if (isFlagged || isRevealed) return; // フラグが立っているか、既に開かれている場合は何もしない

        if (isMine)
        {
            GetComponent<SpriteRenderer>().sprite = mineSprite;
            Debug.Log("Boom! Game Over.");
            Instantiate(explosionEffectPrefab, transform.position, Quaternion.identity); // 爆発エフェクトを生成
            audioSource.PlayOneShot(explosionSound); // 爆発音を再生
            grid.GameOver(); // ゲームオーバーをトリガー
        }
        else
        {
            Reveal(); // タイルを開く
        }
    }

    public void Reveal()
    {
        if (isRevealed) return; // 既に開かれている場合は何もしない

        isRevealed = true; // タイルを開く
        int adjacentMines = CountAdjacentMines(); // 隣接する地雷の数を数える
        GetComponent<SpriteRenderer>().sprite = emptySprites[adjacentMines]; // 適切なスプライトを表示
        Debug.Log($"Safe. {adjacentMines} mines around.");
        grid.AddScore(10); // スコアを追加

        if (adjacentMines == 0)
        {
            RevealAdjacentTiles(); // 隣接タイルを開く
        }
    }

    public int CountAdjacentMines()
    {
        int count = 0;
        for (int dx = -1; dx <= 1; dx++)
        {
            for (int dy = -1; dy <= 1; dy++)
            {
                if (dx == 0 && dy == 0) continue;

                Tile tile = grid.GetTile(x + dx, y + dy);
                if (tile != null && tile.isMine)
                {
                    count++;
                }
            }
        }
        return count;
    }

    private void RevealAdjacentTiles()
    {
        for (int dx = -1; dx <= 1; dx++)
        {
            for (int dy = -1; dy <= 1; dy++)
            {
                if (dx == 0 && dy == 0) continue;

                Tile tile = grid.GetTile(x + dx, y + dy);
                if (tile != null)
                {
                    tile.Reveal(); // 隣接タイルを開く
                }
            }
        }
    }

    private void ToggleFlag()
    {
        if (isRevealed) return; // 既に開かれている場合は何もしない

        isFlagged = !isFlagged; // フラグを切り替える
        GetComponent<SpriteRenderer>().sprite = isFlagged ? flagSprite : emptySprites[0]; // スプライトを更新
        audioSource.PlayOneShot(flagSound); // フラグ音を再生
        Debug.Log(isFlagged ? "Flagged" : "Unflagged");
    }
}

GameManagerスクリプト

最後に、スコア管理を行うためにGameManager.csを作成します。

GamaManagerは、ゲーム全体のスコアを管理するスクリプトです。このスクリプトは、スコアの追加、リセット、およびスコアテキストの更新を行います。

using UnityEngine;
using UnityEngine.UI;

public class GameManager : MonoBehaviour
{
    public Text scoreText; // スコアを表示するテキスト
    private int score; // スコアの変数

    void Start()
    {
        score = 0; // スコアを初期化
        UpdateScoreText(); // スコアテキストを更新
    }

    public void AddScore(int points)
    {
        score += points; // スコアにポイントを追加
        UpdateScoreText(); // スコアテキストを更新
    }

    private void UpdateScoreText()
    {
        scoreText.text = "Score: " + score.ToString(); // スコアテキストを更新
    }

    public void ResetScore()
    {
        score = 0; // スコアをリセット
        UpdateScoreText(); // スコアテキストを更新
    }
}

ステップ5: オーディオの追加

  1. AssetsフォルダにAudioフォルダを作成し、爆発音やフラグ音などのオーディオクリップをインポートします。
  2. Tileプレハブを選択し、AudioSourceコンポーネントを追加します。
  3. TileスクリプトのexplosioniSoundとflagSoundフィールドにそれぞれのオーディオクリップを設定します。

ステップ6: UIの作成

  1. Hierarchyウィンドウで右クリックし、「UI」->「Canvas」を選択します。
  2. Canvas内にTextオブジェクトを作成し、名前をScoreTextに変更します。
  3. ScoreTextのテキストフィールドに初期値として「Score: 0」と入力し、フォントやサイズを調整します。

ステップ7: 統合

  1. GameManagerオブジェクトを作成し、GameManagerスクリプトをアタッチします。
  2. ScoreTextフィールドにCanvas内のScoreTextオブジェクトをドラッグ&ドロップします。
  3. GridオブジェクトにGridスクリプトをアタッチし、Tile PrefabbフィールドにTileプレハブを設定します。

ゲームオーバーとゲームクリアの処理

このままでは、ゲームが終わらないので、ゲームオーバーとゲームクリアの処理を追加しましょう!

ステップ1: UIの作成

  1. Canvasの設定
    • HierarchyウィンドウでScoreTextGameOverTextTextコンポーネントを削除し、それぞれTextMeshPro - Textコンポーネントを追加します。
  2. TextMeshProの設定
    • ScoreTextGameStatusTextの見た目を調整します(フォント、サイズ、位置など)。
  3. GameStatusTextの追加
    • Canvasに新しいTextMeshPro - Textコンポーネントを追加し、名前をGameStatusTextに変更します。
    • GameStatusTextの初期テキストを空にし、フォント、サイズ、位置を調整します。

ステップ2: GameManagerスクリプトの更新

GameManagerスクリプトを更新して、ゲームオーバーとゲームクリアのメッセージを表示する機能を追加します。

using UnityEngine;
using TMPro;

public class GameManager : MonoBehaviour
{
    public TMP_Text scoreText; // スコアを表示するTextMeshProテキスト
    public TMP_Text gameStatusText; // ゲームオーバーとゲームクリアを表示するTextMeshProテキスト
    private int score;

    void Start()
    {
        score = 0;
        UpdateScoreText();
        gameStatusText.text = ""; // 初期状態ではテキストを空にする
    }

    public void AddScore(int points)
    {
        score += points;
        UpdateScoreText();
    }

    private void UpdateScoreText()
    {
        scoreText.text = "Score: " + score.ToString();
    }

    public void ResetScore()
    {
        score = 0;
        UpdateScoreText();
        gameStatusText.text = ""; // スコアリセット時にステータステキストを消す
    }

    public void ShowGameOver()
    {
        gameStatusText.text = "Game Over"; // ゲームオーバーテキストを表示
    }

    public void ShowGameClear()
    {
        gameStatusText.text = "Game Clear"; // ゲームクリアテキストを表示
    }
}

ステップ3: Gridスクリプトの更新Gridスクリプトを更新して、ゲームクリアのチェック機能を追加し、適切なメッセージを表示するようにします

using UnityEngine;
using System.Collections;

public class Grid : MonoBehaviour
{
    public int width = 10;
    public int height = 10;
    public GameObject tilePrefab;
    private Tile[,] tiles;
    private GameManager gameManager;

    void Start()
    {
        gameManager = FindObjectOfType<GameManager>(); // GameManagerを見つける
        GenerateGrid(); // グリッドを生成する
    }

    void GenerateGrid()
    {
        tiles = new Tile[width, height];
        for (int x = 0; x < width; x++)
        {
            for (int y = 0; y < height; y++)
            {
                GameObject tile = Instantiate(tilePrefab, new Vector3(x, y), Quaternion.identity);
                tile.transform.parent = transform;
                Tile tileComponent = tile.GetComponent<Tile>();
                bool isMine = Random.value < 0.15f; // 15%の確率で地雷を配置
                tileComponent.Init(this, x, y, isMine);
                tiles[x, y] = tileComponent;
            }
        }
    }

    public Tile GetTile(int x, int y)
    {
        if (x >= 0 && y >= 0 && x < width && y < height)
        {
            return tiles[x, y];
        }
        return null;
    }

    public void GameOver()
    {
        Debug.Log("Game Over. Reveal all mines.");
        gameManager.ShowGameOver(); // ゲームオーバーのテキストを表示

        // 全ての地雷を表示する
        for (int x = 0; x < width; x++)
        {
            for (int y = 0; y < height; y++)
            {
                Tile tile = tiles[x, y];
                if (tile.isMine)
                {
                    tile.GetComponent<SpriteRenderer>().sprite = tile.mineSprite;
                }
            }
        }

        // 5秒後にリセット
        StartCoroutine(RestartAfterDelay(5f));
    }

    public void CheckGameClear()
    {
        // 全てのタイルが開かれ、かつ地雷を踏んでいない場合ゲームクリア
        foreach (Tile tile in tiles)
        {
            if (!tile.isRevealed && !tile.isMine)
            {
                return; // まだ開かれていないタイルがある場合はリターン
            }
        }

        Debug.Log("Game Clear. You win!");
        gameManager.ShowGameClear(); // ゲームクリアのテキストを表示

        // 5秒後にリセット
        StartCoroutine(RestartAfterDelay(5f));
    }

    private IEnumerator RestartAfterDelay(float delay)
    {
        yield return new WaitForSeconds(delay);
        gameManager.ResetScore(); // スコアをリセット
        Restart();
    }

    public void Restart()
    {
        // グリッドを再生成
        foreach (Transform child in transform)
        {
            Destroy(child.gameObject);
        }
        GenerateGrid();
    }

    public void AddScore(int points)
    {
        gameManager.AddScore(points);
    }
}

ステップ4: Tileスクリプトの更新

Tileスクリプトを更新して、タイルがすべて開かれたかどうかをチェックするようにします。

using UnityEngine;

public class Tile : MonoBehaviour
{
    private Grid grid; // グリッドの参照
    public int x; // タイルのX座標
    public int y; // タイルのY座標
    public bool isMine; // タイルが地雷かどうか
    public bool isRevealed = false; // タイルが開かれているかどうか
    public bool isFlagged = false; // タイルにフラグが立てられているかどうか
    public Sprite[] emptySprites; // 空のタイル用のスプライト配列
    public Sprite mineSprite; // 地雷のスプライト
    public Sprite flagSprite; // フラグのスプライト
    public GameObject explosionEffectPrefab; // 爆発エフェクトのプレハブ
    public AudioClip explosionSound; // 爆発音
    public AudioClip flagSound; // フラグ音

    private AudioSource audioSource; // オーディオソースの参照

    public void Init(Grid grid, int x, int y, bool isMine)
    {
        this.grid = grid;
        this.x = x;
        this.y = y;
        this.isMine = isMine;
        audioSource = GetComponent<AudioSource>(); // オーディオソースを取得
    }

    void Update()
    {
        if (Input.GetMouseButtonDown(1)) // 右クリックを検出
        {
            Vector2 worldPoint = Camera.main.ScreenToWorldPoint(Input.mousePosition);
            RaycastHit2D hit = Physics2D.Raycast(worldPoint, Vector2.zero);

            if (hit.collider != null && hit.collider.gameObject == gameObject)
            {
                ToggleFlag(); // フラグを切り替える
            }
        }

        if (Input.GetMouseButtonDown(0)) // 左クリックを検出
        {
            Vector2 worldPoint = Camera.main.ScreenToWorldPoint(Input.mousePosition);
            RaycastHit2D hit = Physics2D.Raycast(worldPoint, Vector2.zero);

            if (hit.collider != null && hit.collider.gameObject == gameObject)
            {
                OnLeftClick(); // 左クリック時の処理を実行
            }
        }
    }

    private void OnLeftClick()
    {
        if (isFlagged || isRevealed) return; // フラグが立っているか、既に開かれている場合は何もしない

        if (isMine)
        {
            GetComponent<SpriteRenderer>().sprite = mineSprite;
            Debug.Log("Boom! Game Over.");
            Instantiate(explosionEffectPrefab, transform.position, Quaternion.identity); // 爆発エフェクトを生成
            audioSource.PlayOneShot(explosionSound); // 爆発音を再生
            grid.GameOver(); // ゲームオーバーをトリガー
        }
        else
        {
            Reveal(); // タイルを開く
        }
    }

    public void Reveal()
    {
        if (isRevealed) return; // 既に開かれている場合は何もしない

        isRevealed = true; // タイルを開く
        int adjacentMines = CountAdjacentMines(); // 隣接する地雷の数を数える
        GetComponent<SpriteRenderer>().sprite = emptySprites[adjacentMines]; // 適切なスプライトを表示
        Debug.Log($"Safe. {adjacentMines} mines around.");
        grid.AddScore(10); // スコアを追加
        grid.CheckGameClear(); // ゲームクリアをチェック

        if (adjacentMines == 0)
        {
            RevealAdjacentTiles(); // 隣接タイルを開く
        }
    }

    public int CountAdjacentMines()
    {
        int count = 0;
        for (int dx = -1; dx <= 1; dx++)
        {
            for (int dy = -1; dy <= 1; dy++)
            {
                if (dx == 0 && dy == 0) continue;

                Tile tile = grid.GetTile(x + dx, y + dy);
                if (tile != null && tile.isMine)
                {
                    count++;
                }
            }
        }
        return count;
    }

    private void RevealAdjacentTiles()
    {
        for (int dx = -1; dx <= 1; dx++)
        {
            for (int dy = -1; dy <= 1; dy++)
            {
                if (dx == 0 && dy == 0) continue;

                Tile tile = grid.GetTile(x + dx, y + dy);
                if (tile != null)
                {
                    tile.Reveal(); // 隣接タイルを開く
                }
            }
        }
    }

    private void ToggleFlag()
    {
        if (isRevealed) return; // 既に開かれている場合は何もしない

        isFlagged = !isFlagged; // フラグを切り替える
        GetComponent<SpriteRenderer>().sprite = isFlagged ? flagSprite : emptySprites[0]; // スプライトを更新
        audioSource.PlayOneShot(flagSound); // フラグ音を再生
        Debug.Log(isFlagged ? "Flagged" : "Unflagged");
    }
}

ステップ5: Hierarchyでの設定

  1. GameManagerオブジェクトの作成
    • Hierarchyウィンドウで右クリックし、「Create Empty」を選択して空のオブジェクトを作成し、「GameManager」と名前を付けます。
    • GameManagerオブジェクトにGameManagerスクリプトをアタッチします。
    • ScoreTextフィールドに、Canvas内のScoreTextオブジェクトをドラッグ&ドロップします。
    • GameStatusTextフィールドに、Canvas内のGameStatusTextオブジェクトをドラッグ&ドロップします。

これで、ゲームオーバー時に「Game Over」、ゲームクリア時に「Game Clear」のテキストが表示され、5秒後にゲームが初期化されて再プレイできるようになります。

これで、基本的なマインスイーパーが完成しました。タイルをクリックしてゲームを楽しんでください。ゲームオーバー時には5秒後にリセットされ、新しいゲームが始まります。スコアもリアルタイムで更新されるので、ぜひ挑戦してみてください!

以上が、Unityでマインスイーパーを作成する手順のブログ記事です。簡単なマインスイーパーの作り方でしたが、まだまだオリジナルの要素など入れる余地があるので、ぜひチャレンジして見てください!