Unityでサイコロのチンチロを作った

# 1.　はじめに
私はお笑いやバラエティーが好きで、Youtubeを見ていると、霜降り明星の粗品さんのチャンネルで後輩芸人4人とチンチロを遊ぶ動画が上がっていた。[粗品さんの動画](https://youtu.be/PQmAyFzHIH0?list=PLeSkLEs4VpGwRqoKCbxqx9YsXlH_nPgLZ)
そこで、チンチロならゲームを作ったことのない自分でも作れるのではないかと、また、ゲームの初制作としてサイコロを使用したゲームは適しているのではないかと思い、制作に至りました。
# 2.　ゲームを開発するまでに
私は、プログラミングの経験は当然あったのですが、ゲームは一切作った事がなく全くの知識がなかったので、Unityのチュートリアルから始めました。
私がやったのは、Unity公式のチュートリアルの一番初めと最後の２つです。
他の言語などでプログラミングの知識がある方はこれで十分だと思います。
私は３～4時間で終わりました。
![SnapCrab_NoName_2023-11-26_11-30-6_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531555/087259a1-1f94-a478-b9e7-d29bd92f4647.png)

[Unity チュートリアル](https://learn.unity.com/course/unity-tutorials-for-beginners-jp)

# 3.準備するもの
```:開発環境
Unity
visual studio 2022
```
[unityとvisual studioの連携の参考](https://qiita.com/engr_murao/items/b7bbb0eaa48f1c66fc2b)

チンチロなので、当然サイコロなどが必要になります。
今回使用したのはすべて無料で利用できるアセットを探してダウンロードしました。
```:アセット
Dice d6 game ready PBR
Stylized - Simple Hands
Plates, Bowls & Mugs Pack
SAColliderBuilder
```
[サイコロ](https://assetstore.unity.com/packages/3d/props/tools/dice-d6-game-ready-pbr-200151)
[ボウル 器](https://assetstore.unity.com/packages/3d/props/interior/plates-bowls-mugs-pack-146682)
[手](https://assetstore.unity.com/packages/3d/characters/stylized-simple-hands-221297)
[SAcollider](https://assetstore.unity.com/packages/tools/sacolliderbuilder-15058)
SAColliderは器（どんぶり）の当たり判定を付与するために使用しました。
TextMeshProはデフォルトでは日本語の表示が出来ないので、下記を参考
[TextMeshProで日本語を表示する方法](https://vanikki.com/unity-textmeshpro/)
# 4.オブジェクト構成
![SnapCrab_NoName_2023-11-26_11-40-18_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531555/cefd2b80-0b74-3024-eee8-2eaf0c2c6236.png)
![SnapCrab_NoName_2023-11-26_11-40-51_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531555/292d138f-f030-3055-163d-0f6004be4c50.png)
![SnapCrab_NoName_2023-11-26_11-40-31_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531555/f75b2d86-c139-0530-d945-050e5b6f2b73.png)
サイコロのInspector
![SnapCrab_NoName_2023-11-26_11-41-55_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531555/d2ddd16f-26bf-5e77-928d-c81c887b1a32.png)
手(Player)のInspector
![SnapCrab_NoName_2023-11-26_11-42-36_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531555/6f05ce19-8f3d-ec50-bb66-935181f86129.png)
GameManagerのInspector
![SnapCrab_NoName_2023-11-26_11-43-20_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531555/d4bc86b8-369c-6134-93de-2a51172b9af5.png)
TextMeshProUGUIのInspector
![SnapCrab_NoName_2023-11-26_11-44-11_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531555/2ecb4bbb-72a8-7e3c-b809-f8ee13556a2a.png)
# 5.プログラム

```c#:PlayerController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerController : MonoBehaviour
{
    public GameObject hand; // 手のアセット
    public float moveSpeed = 15.0f; // 手の移動速度
    public Dice dice; // サイコロのスクリプト
    public DiceManager manager;
    private List<Dice> dicesInHand = new List<Dice>(); // 手にあるサイコロのリスト
    private int someThreshold = 20;
    void Start()
    {
        Debug.Log("Manager active state at Start: " + manager.gameObject.activeInHierarchy);
        if (manager != null)
        {
            manager.gameObject.SetActive(true);
        }
        Debug.Log(manager); 
    }

    void Update()
    {
        if (manager != null && !manager.gameObject.activeInHierarchy)
        {
            Debug.LogError("DiceManager GameObject is not active.");
            return; // DiceManagerが非アクティブならば、以降の処理を行わない
        }
        float moveX = Input.GetAxis("Horizontal") * moveSpeed * Time.deltaTime; // X軸の移動量
        float moveZ = Input.GetAxis("Vertical") * moveSpeed * Time.deltaTime; // Z軸の移動量
        float moveY = 0;
        hand.transform.Translate(moveX, 0, moveZ); // 手の位置を更新
        if (Input.GetKeyDown(KeyCode.F))
        {
            AttachDicesToHand();
        }
        if (Input.GetKeyDown(KeyCode.H))
        {
            ReleaseAllDices();
        }
        if (Input.GetKey(KeyCode.Space))
        {
            moveY = moveSpeed * Time.deltaTime; // 上昇
        }
        else if (Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift))
        {
            moveY = -moveSpeed * Time.deltaTime; // 下降
        }
        hand.transform.Translate(0, moveY, 0); // 手の位置を更新

        Debug.Log("Manager active state in Update: " + manager.gameObject.activeInHierarchy);
    }
    void AttachDicesToHand()
    {
        foreach (Dice dice in FindObjectsOfType<Dice>())
        {
            if (IsDiceAboveHand(dice))
            {
                dicesInHand.Add(dice);
                dice.AttachToHand(hand);
            }
        }
    }

    void ReleaseAllDices()
    {
        // dicesInHandがnullでないことを確認
        if (dicesInHand != null)
        {
            // 各サイコロに対してループ処理
            foreach (Dice dice in dicesInHand)
            {
                // diceがnullでないことを確認
                if (dice != null)
                {
                    // サイコロを手からリリースする処理
                    dice.ReleaseFromHand();
                }
            }

           

            if (manager != null && manager.gameObject.activeInHierarchy)
            {
                // ここでmanagerのメソッドを呼び出す
                manager.RollDice();
            }
            else
            {
                // GameObjectが非アクティブな場合のエラーメッセージ
                Debug.LogError("DiceManager GameObject is not active.");
            }
            // リストからすべての要素を削除
            dicesInHand.Clear();
        }
    }


        bool IsDiceAboveHand(Dice dice)
    {
        // サイコロと手の距離が一定値以下かどうかを判定
        float distanceToHand = Vector3.Distance(dice.transform.position, hand.transform.position);
        return distanceToHand <= someThreshold; // someThresholdは適切な距離の値
    }
}
```
```c#:ChinchiroManager.cs
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
public class ChinchiroManager : MonoBehaviour
{
    public GameObject[] dice; // サイコロオブジェクトの配列
    public TextMeshProUGUI resultTextMesh; // 役の結果を表示するテキストメッシュ

    // サイコロの出目を評価し、役を決定する
    public void EvaluateDice(int[] values)
    {
        string hand = GetHand(values);
        UpdateResultText(hand); // 役の結果を画面に表示
        Debug.Log(hand);
    }
    void UpdateResultText(string text)
    {
        if (resultTextMesh != null)
        {
            resultTextMesh.text = text; // TextMeshProのtextプロパティを更新
        }
    }
    // サイコロの目から役を判断するメソッド
    string GetHand(int[] values)
    {
        if (values.Distinct().Count() == 1) // 全ての目が同じ
        {
            if (values[0] == 1) return "ピンゾロ";
            else return "アラシ";
        }
        else if (values.Contains(4) && values.Contains(5) && values.Contains(6))
        {
            return "シゴロ";
        }
        else if (values.Distinct().Count() == 2) // 2つの目が同じ
        {
            return "残りの一つの目：" + values.GroupBy(v => v).OrderByDescending(g => g.Count()).First().Key.ToString();
        }
        else if (values.OrderBy(v => v).SequenceEqual(new int[] { 1, 2, 3 }))
        {
            return "ヒフミ";
        }
        else if (values.Any(v => v == 0)) // サイコロがボウルからはみ出ている場合（0がはみ出た目を示す）
        {
            return "ションベン";
        }
        else
        {
            return "役なし";
        }
    }
}
```



```c#:Dice.cs
using Unity.VisualScripting;
using UnityEngine;

public class Dice : MonoBehaviour
{
    private Rigidbody diceRigidbody;
    private bool isHeld = false; // サイコロが掴まれているかどうか
    void Start()
    {
        diceRigidbody = GetComponent<Rigidbody>();
    }
    // サイコロの上面の値を取得するメソッド
    public int GetUpwardFaceValue()
    {
        Vector3 up = transform.up;

        if (Vector3.Dot(up, Vector3.up) > 0.9f) return 1; // 上面が1
        if (Vector3.Dot(-up, Vector3.up) > 0.9f) return 6; // 上面が6

        if (Vector3.Dot(up, Vector3.forward) > 0.9f) return 5; // 前面が5
        if (Vector3.Dot(-up, Vector3.forward) > 0.9f) return 2; // 前面が2

        if (Vector3.Dot(up, Vector3.right) > 0.9f) return 4; // 右面が4
        if (Vector3.Dot(-up, Vector3.right) > 0.9f) return 3; // 右面が3

        return 0; // どの面も上向きではない場合
    }

    public void AttachToHand(GameObject hand)
    {
        isHeld = true;
        diceRigidbody.isKinematic = true;
        this.transform.parent = hand.transform; // 手を親オブジェクトとする
    }

    public void ReleaseFromHand()
    {
        isHeld = false;
        diceRigidbody.isKinematic = false;
        this.transform.parent = null; // 親オブジェクトの関連付けを解除

        // ランダムなトルクを加えてサイコロを振る
        Vector3 torq = new Vector3(Random.Range(0, 100), Random.Range(0, 100), Random.Range(0, 100));
        diceRigidbody.AddTorque(torq);
    }

    public int GetValue()
    {
        return GetUpwardFaceValue(); // 上面の値を返す
    }
}
```
メソッドの解説:
Start: スクリプトが最初に実行された時に一度だけ呼び出され、diceRigidbody変数を初期化します。

GetUpwardFaceValue: サイコロが停止したときに上面になっている面の数値を判定します。これはVector3.Dot関数を使用して、サイコロの各面が上向きになっているかどうかを計算することで実現しています。Vector3.up, Vector3.forward, Vector3.rightはそれぞれワールド座標系の上、前、右方向を示します。Vector3.Dotは2つのベクトルのドット積を計算し、それが0.9よりも大きければほぼ同じ方向を向いていると判定されます。これにより、サイコロのどの面が上を向いているかを判定しています。

AttachToHand: このメソッドはサイコロをプレイヤーの手に"アタッチ"するために呼び出されます。サイコロのRigidbodyをキネマティック（物理演算の影響を受けない）に設定し、サイコロの親を手のオブジェクトに設定します。

ReleaseFromHand: プレイヤーがサイコロを放すときに呼び出されます。isKinematicをfalseに戻し、親オブジェクトの設定を解除して物理演算による動きを可能にします。さらに、Random.Rangeを使用してランダムなトルクをサイコロに加え、実際にサイコロを振るような効果を生み出します。

GetValue: 上面の数値を取得するために公開されているメソッドで、GetUpwardFaceValueメソッドを呼び出してその値を返します。

サイコロの目の判定ロジック:
サイコロの目の判定ロジックはGetUpwardFaceValueメソッド内に実装されています。このロジックは、サイコロが静止した後、サイコロの上向きのベクトルとワールド座標系の各軸とのドット積を計算して、どの面が上を向いているかを判定します。ドット積が0.9より大きい場合、それは二つのベクトルがほぼ同じ方向、つまりサイコロのその面が上を向いていると判定されるため、対応する面の数値を返します。

この判定方法は、サイコロが完全に停止している（つまり、動いていない）ことを前提としているので、サイコロが静止していない場合、このメソッドは正確な結果を返さない可能性があります。そのため、このメソッドはサイコロが完全に停止してから呼び出す必要があります。

```c#:DiceManager.cs
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

public class DiceManager : MonoBehaviour
{
    public ChinchiroManager chinchiro;
    public GameObject dicePrefab; // サイコロのプレファブへの参照
    public Vector3 spawnPosition; // サイコロの生成位置
    private int[] diceValues;
    public Dice[] dice = new Dice[3]; // 3つのサイコロを格納する配列
    void Start()
    {
       //SpawnDice(); // ゲーム開始時にサイコロを生成する
        diceValues = new int[dice.Length]; // 出目の配列をサイコロの数に合わせて初期化
    }

    private void SpawnDice()
    {
        // 既存のサイコロを破棄する
        foreach (var existingDice in FindObjectsOfType<Dice>())
        {
            Destroy(existingDice.gameObject);
        }
        // 3つのサイコロを生成する
        for (int i = 0; i < 3; i++)
        {
            GameObject diceGO = Instantiate(dicePrefab, spawnPosition + new Vector3(i * 2.0f, 0, 0), Quaternion.identity);
            dice[i] = diceGO.GetComponent<Dice>();
        }
    }
    public void RollDice()
    {
        // サイコロを振
        StartCoroutine(WaitForDiceToSettle());
    }

    private IEnumerator WaitForDiceToSettle()
    {
        yield return new WaitUntil(() => dice.All(d => d.GetComponent<Rigidbody>().IsSleeping()));
        EvaluateDice();
    }

    private void EvaluateDice()
    {
        for (int i = 0; i < dice.Length; i++)
        {
            diceValues[i] = dice[i].GetValue();
        }

        chinchiro.EvaluateDice(diceValues);
        Debug.Log("Dice values: " + string.Join(", ", diceValues));
    }
}
```
```c#:DiceChecker.cs
using System.Collections;
using UnityEngine;
using TMPro;

public class DiceChecker : MonoBehaviour
{
    public Rigidbody[] dice;
    public TextMeshProUGUI textMeshPro; 
    public float checkInterval = 0.5f; // 静止しているかをチェックする間隔

    private bool areAllDiceStopped;

    void Start()
    {
        StartCoroutine(CheckIfDiceStopped());
    }

    IEnumerator CheckIfDiceStopped()
    {
        // 無限ループを使い、全てのサイコロが静止するまでチェックし続ける
        while (!areAllDiceStopped)
        {
            areAllDiceStopped = true; // フラグを初期化
            foreach (var die in dice)
            {
                // サイコロが動いているかチェック
                if (die.IsSleeping() == false)
                {
                    areAllDiceStopped = false; // サイコロが動いていたらフラグをリセット
                    break; // 1つでも動いていればループを抜ける
                }
            }

            if (!areAllDiceStopped)
            {
                // サイコロがまだ動いている場合は、次のチェックまで待機
                yield return new WaitForSeconds(checkInterval);
            }
        }

        // 全てのサイコロが静止したらテキストのアニメーションを開始
        StartCoroutine(StartTextAnimation());
    }

    IEnumerator StartTextAnimation()
    {
        // テキストアニメーションのコードをここに書く
        // 例えば、テキストを徐々に表示する
        float duration = 1.0f;
        float time = 0;
        while (time < duration)
        {
            textMeshPro.alpha = time / duration; // アルファ値を徐々に上げる
            time += Time.deltaTime;
            yield return null;
        }
        textMeshPro.alpha = 1; // アルファ値を最終的に1に設定
    }
}
```
```c#:TextMeshProAnimation.cs
using System.Collections;
using TMPro;
using UnityEngine;

public class TextMeshProAnimation : MonoBehaviour
{
    public TextMeshProUGUI textMeshPro; // Inspectorからアサイン
    public float rotationSpeed = 360f; // 1秒間に回転する度数
    public float deceleration = 25f; // 減速度

    private bool isAnimating = false;

    public void StartTextAnimation()
    {
        if (!isAnimating)
        {
            StartCoroutine(RotateAndFadeInText());
        }
    }

    private IEnumerator RotateAndFadeInText()
    {
        isAnimating = true;
        float currentRotationSpeed = rotationSpeed;
        while (currentRotationSpeed > 0)
        {
            // テキストを回転させる
            textMeshPro.transform.Rotate(0, 0, currentRotationSpeed * Time.deltaTime);
            currentRotationSpeed -= deceleration * Time.deltaTime;

            // フェードイン
            float alpha = textMeshPro.alpha;
            alpha += Time.deltaTime; // フェードイン速度を調整するには、ここの値を変更
            textMeshPro.alpha = Mathf.Clamp01(alpha);

            yield return null;
        }
        isAnimating = false;
    }
}

```
# 6.実行結果
[動画](https://youtu.be/JQYYLU1D9E0)
# 7.やりたいこと
　-　テキストの表示の調整
　-　プログラムでアニメーションをつけたが反映されていなさそう
　-　UIの改善
　-　手のアセットを有料のものを購入し、手を振るアニメーションなどを追加したい
　-　効果音やBGMの追加
　-　簡易的なオンライン対戦
　-　スコア機能の実装
# 8.最後に
初Unityなだけに開発時間は20時間ほどになってしまいました。
しかし、そのほとんどはコーディングではなくオブジェクトの配置やコンポーネントのエラーでした。
ですが、今回の経験やチュートリアルを通して次回からの開発は大幅に改善されそうです。