i-school - 2D数当てゲーム(ヒットアンドブロー) 手順4
 未編集

 各スクリプト



View



using System;
using System.Collections.Generic; // IObservable を使うために必要
using System.Linq;
using System.Text;  // StringBuilder を使うために必要
using UnityEngine;
using UnityEngine.UI;
using UniRx;

public class NumberGameView : MonoBehaviour
{
    [SerializeField] private Text[] txtSelectNumbers;
    [SerializeField] private Text txtExplanation;
    [SerializeField] private Text txtCount;
    [SerializeField] private Text txtCountMax;
    
    [SerializeField] private Button callButton;
    public Button CallButton => callButton;  // プロパティ

    [SerializeField] private NumberButton numberButtonPrefab;
    [SerializeField] private Transform numberButtonTran;
    private int buttonCount = 10;

    [SerializeField] private List<Button> numberButtonList = new();
    [SerializeField] private Button deleteButton;

    // OnClickAsObservable()の購読処理を施したボタンのプロパティ
    public IObservable<Unit> OnCallButtonClickAsObservable => callButton.OnClickAsObservable();
    public IObservable<Unit> OnDeleteButtonClickAsObservable => deleteButton.OnClickAsObservable();

    // OnNumberButtonClickAsObservableプロパティは、numberButtons配列の各ボタンに対してOnClickAsObservable()を購読し、クリックされたボタンの要素番号をストリームに流すIObservable<int>を作成している
    // numberButtonList.Select()は、各ボタンとそのインデックスを引数として、ボタンがクリックされたときにインデックスを流すIObservable<int>を返す
    // これにより、numberButtons配列の各ボタンが自分のインデックスを記録し、クリックされたときにそれをストリームに流すことができる
    // また、最後にあるMerge()は、numberButtonList.Select()によって生成された複数のIObservable<int>ストリームを1つのIObservable<int>ストリームに統合している
    // これにより、OnNumberButtonClickAsObservableプロパティは、どのボタンがクリックされたかに関係なく、クリックされたボタンのインデックスを流す単一のIObservable<int>ストリームとして扱うことができる
    public IObservable<int> OnNumberButtonClickAsObservable => numberButtonList.Select((button, index) => button.OnClickAsObservable().Select(_ => index)).Merge();


    void Start() {
        GenerateNumberButtons();    
    }

    public void GenerateNumberButtons() {
        for (int i = 0; i < buttonCount; i++) {
            int index = i;
            NumberButton numberButton = Instantiate(numberButtonPrefab, numberButtonTran, false);
            numberButton.SetUpNumberButton(index);
            numberButtonList.Add(numberButton.BtnNumber);
        }
    }
    

    /// <summary>
    /// ボタンを非活性化
    /// </summary>
    /// <param name="number"></param>
    public void DisableNumberButton(int number)
    {
        numberButtonList[number].interactable = false;
    }

    /// <summary>
    /// ボタンを活性化
    /// </summary>
    /// <param name="number"></param>
    public void EnableNumberButton(int number)
    {
        numberButtonList[number].interactable = true;
    }


    public void InitialView() {
        callButton.interactable = false;

        SwitchAllButtons(true);

        txtExplanation.text = "";
    }
    
    
    /// <summary>
    /// 全数字ボタンの状態の切り替え
    /// </summary>
    /// <param name="isSwitch"></param>
    public void SwitchAllButtons(bool isSwitch) {

        for (int i = 0; i < numberButtonList.Count; i++) {
            numberButtonList[i].interactable = isSwitch;
        }
    }
    
    
    
    
        /// <summary>
    /// 入力した数字を画面に表示
    /// </summary>
    /// <param name="inputNumberList"></param>
    public void UpdateInputDisplay(ReactiveCollection<int> inputNumberList) {
        //Debug.Log(inputNumberList.Count);
        for (int i = 0; i < txtSelectNumbers.Length; i++) {
            txtSelectNumbers[i].text = i < inputNumberList.Count ? inputNumberList[i].ToString() : "";
        }
    }

    /// <summary>
    /// 入力した回答の画面表示更新
    /// </summary>
    /// <param name="ansCount"></param>
    /// <param name="inputNumbers"></param>
    /// <param name="hit"></param>
    /// <param name="blow"></param>
    public void UpdateExplanation(int ansCount, ReactiveCollection<int> inputNumbers, int hit, int blow) {
        
        // string.Join を使うことで、第2引数の配列か List の要素を1つずつ取り出し、第1引数の文字を間に加える
        // カンマを指定すればカンマ区切りの文字列になり、今回のように空白を入れれば要素同士がつながる
        var inputNumbersStr = string.Join("", inputNumbers);
        
        // 文字列補完
        var result = $"回答 {ansCount}回目:{inputNumbersStr}: {hit} HIT {blow} BLOW ";
        
        // StringBuiler クラスをインスタンスし、コンストラクタに txtExplanation.text を渡して初期化
        var stringBuilder = new StringBuilder(txtExplanation.text);
        
        // AppendLine メソッドを使い、result をstringBuilder の最後の行に加える
        // 明示的な改行命令がなくても、自動的に改行した上で最後の行に追加される
        stringBuilder.AppendLine(result);
        
        // 文字列に変換して画面表示を更新
        // StringBuilder を使用する理由は、文字列の連結操作が繰り返される場合に、パフォーマンスが向上するため
        // 文字列はイミュータブル(不変)なので、+= を使って文字列を連結するたびに新しい文字列が生成される
        // これが多くの連結操作で行われると、パフォーマンスが低下することがある
        // StringBuilder を使うことで、この問題を回避し、効率的に文字列の連結を行うことができる
        txtExplanation.text = stringBuilder.ToString();
    }

    /// <summary>
    /// 現在までの回答数と最大回答数の画面表示更新
    /// </summary>
    /// <param name="ansCount"></param>
    /// <param name="maxCount"></param>
    public void UpdateAnswerCount(int ansCount, int maxCount) {
        txtCount.text = ansCount.ToString();
        txtCountMax.text = maxCount.ToString();
    }
}



Model


using System;
using System.Collections.Generic;
using System.Linq;
using UniRx;

[Serializable]
public class NumberGameModel
{
    public ReactiveCollection<int> InputNumberList = new(); 

    //public List<int> InputNumbers { get; private set; }
    public List<int> CorrectNumbers { get; private set; }
    
    public ReactiveProperty<int> AnsCount = new ();
    public int MaxCount { get; private set; }
    
    public ReactiveProperty<NumberGameState> CurrentNumberGameState { get; } = new ();

    // 入力された数字の数を監視するプロパティを追加
    //public ReactiveProperty<int> InputNumbersCount { get; private set; } = new ReactiveProperty<int>(0);
    
    
    /// <summary>
    /// コンストラクタ
    /// </summary>
    /// <param name="maxCount"></param>
    public NumberGameModel(int maxCount) {
        CurrentNumberGameState.Value = NumberGameState.Play;

        //InputNumbers = new ();
        CorrectNumbers = GenerateCorrectNumbers(); // ここで正解の数字を設定する
        AnsCount.Value = 0;
        MaxCount = maxCount;
    }
    
    /// <summary>
    /// ReactiveCollection に追加。
    /// Presenter で購読し、Call ボタンのオンオフを監視
    /// </summary>
    /// <param name="number"></param>
    public void AddInputNumber(int number) {
        // InputNumbers.Add(number);
        //
        // // 数字が追加されたら、入力された数字の数を更新
        // InputNumbersCount.Value = InputNumbers.Count;
        
        InputNumberList.Add(number);
    }

    /// <summary>
    /// ReactiveCollection から削除。
    /// Presenter で購読し、Call ボタンのオンオフを監視
    /// </summary>
    public void RemoveLastInputNumber() {
        // // 最後に登録された番号を削除
        // InputNumbers.RemoveAt(InputNumbers.Count - 1);
        //
        // // 数字が削除されたら、入力された数字の数を更新
        // InputNumbersCount.Value = InputNumbers.Count;
        
        // 最後に登録された番号を削除
        InputNumberList.RemoveAt(InputNumberList.Count - 1);
    }

    public void IncrementAnsCount() {
        AnsCount.Value++;

        UnityEngine.Debug.Log(AnsCount.Value);
    }

    /// <summary>
    /// 数あてゲームの正解を作る
    /// </summary>
    private List<int> GenerateCorrectNumbers() {
        
        // 初期値の情報を元に、新しい List 作成
        List<int> availableNumbers = new (){ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
            
        // // 正解用の数字格納用の配列の初期化
        // int[] correctNumbers = new int[3];
        //
        // // ランダムな値を3つ取得。Remove することで重複する数字を選択しないようにする
        // for (int i = 0; i < correctNumbers.Length; i++) {
        //     
        //     //int randomIndex = UnityEngine.Random.Range(0, availableNumbers.Count);
        //     // System の Random クラスの場合、int 型の乱数は Next メソッドで作成
        //     int randomIndex = new Random().Next(0, availableNumbers.Count);
        //     correctNumbers[i] = availableNumbers[randomIndex];
        //     availableNumbers.RemoveAt(randomIndex);
        // }

        // Random はここで1つだけインスタンスする。OrderBy の中で new すると毎回インスタンスされて効率が悪いため
        var random = new Random();
        
        // OrderBy を利用して、ランダムに取得された値を取得した順番に並べ、Take で先頭の3つを取り出す
        var correctNumbers = availableNumbers.OrderBy(x => random.Next()).Take(3).ToList();
        
        Console.WriteLine($"正解 : { string.Join(", ", correctNumbers)}");
        UnityEngine.Debug.Log($"正解 : { string.Join(", ", correctNumbers)}");

        return correctNumbers;
    }

    public void ResetGame() {
        //InputNumbers.Clear();
        
        InputNumberList.Clear();
    }




    /// <summary>
    /// Model のインスタンスを1つだけ作り、それを使い続ける場合にはこの処理で初期化する
    /// 購読処理は動いているので不要
    /// </summary>
    public void InitialModel() {
        CurrentNumberGameState.Value = NumberGameState.Play;
        CorrectNumbers = GenerateCorrectNumbers(); // ここで正解の数字を設定する
        AnsCount.Value = 0;
    }
}


Presenter



using System.Collections;
using UnityEngine;
using UniRx;
using System.Linq;

public class NumberGamePresenter : MonoBehaviour
{
    [SerializeField] private NumberGameView view;

    private NumberGameModel model;
    public NumberGameModel NumberGameModel => model;
    private GameLogic gameLogic;

    private float waitTime = 3.0f; // 結果を出す時間を調整する
    [SerializeField] private int maxChallengeCount = 10;

    [SerializeField, Header("数あてゲーム 成果発表用Prefab")]
    private NumberGameResultPopUp numberGameResultPopUpPrefab;

    [SerializeField, Header("数あてゲーム 回答結果用Prefab")]
    private NumberGameDetailPopUp numberGameDetailPopUpPrefab;

    private NumberGameDetailPopUp numberGameDetailPopUp;

    private Transform canvasTran;
    private CompositeDisposable disposableModels = new();

    
    // /// <summary>
    // /// 初期化
    // /// </summary>
    // /// <param name="disposables"></param>
    // /// <param name="canvasTran"></param>
    // public void InitializeGame(CompositeDisposable disposables, Transform canvasTran) {
    //
    //     this.canvasTran = canvasTran;
    
    //     view.GenerateNumberButtons();
    //
    //     // 各数字ボタンのクリックイベントを購読。結合してあるので、個別に購読しなくてよい
    //     view.OnNumberButtonClickAsObservable
    //         // 選択されている数が3以下 かつ すでに選択されている数字ではない
    //         .Where(number => model.InputNumberList.Count < 3 && !model.InputNumberList.Contains(number))
    //         .Subscribe(number =>
    //         {
    //             // 入力値として保持し、画面更新
    //             model.AddInputNumber(number);
    //
    //             view.UpdateInputDisplay(model.InputNumberList);
    //
    //             // 押された数字のボタンのみを無効にする
    //             view.DisableNumberButton(number);
    //
    //             //Debug.Log(number);
    //         })
    //         .AddTo(disposables);
    //
    //     // 削除ボタンのクリックイベントを購読
    //     view.OnDeleteButtonClickAsObservable
    //         .Where(_ => model.InputNumberList.Count > 0)
    //         .Subscribe(_ =>
    //         {
    //             // List の最後の要素を取り出す(Linq)
    //             int deletedNumber = model.InputNumberList.Last();
    //             model.RemoveLastInputNumber();
    //
    //             // 削除したボタンを有効にする
    //             view.EnableNumberButton(deletedNumber);
    //
    //             view.UpdateInputDisplay(model.InputNumberList);
    //             //Debug.Log(deletedNumber);
    //         })
    //         .AddTo(disposables);
    //
    //     // Call ボタンのクリックイベントを購読
    //     // 上の処理で Call ボタンのオン・オフ切り替えをしているため、ここでは Subscribe のみでよい
    //     view.OnCallButtonClickAsObservable
    //         .Subscribe(_ => ProcessInputNumbers())
    //         .AddTo(disposables);
    // }
    //
    // /// <summary>
    // /// ゲームの初期化
    // /// InitializeGame と Dispose のチェックのために分けているが、1つにしても問題ない
    // /// </summary>
    // public void ExecuteGame() {
    //     // Model と GameLogic のインスタンス作成。ここで最大試行回数を設定
    //     model = new NumberGameModel(maxChallengeCount);
    //     gameLogic = new GameLogic(model.CorrectNumbers);
    //
    //     // チャレンジ回数の購読
    //     model.AnsCount.Subscribe(count =>
    //     {
    //         // 画面の回答回数表示を更新
    //         view.UpdateAnswerCount(count, model.MaxCount);
    //     }).AddTo(disposableModels);
    //
    //     // Call ボタンの有効状態(on/off)を入力された数字の数に応じて変更
    //     // ReactiveCommandの実行可否状態をButton.interactableプロパティにバインドして
    //     // ReactiveCommandの状態に応じてボタンが有効化・無効化される
    //     // ReactiveCommandの状態は Select の条件の評価により、true か false になる
    //     // model.InputNumbersCount
    //     //     .Select(count => count == 3)  // 評価後、true になったらストリームを int から bool に変換
    //     //     .ToReactiveCommand()  // bool型のストリームをReactiveCommandに変換
    //     //     .BindTo(view.CallButton) // その後BindToを使ってViewクラスのCallButtonプロパティに紐づけ
    //     //     .AddTo(this);
    //
    //     // 上記と同じ処理だが、List を購読させることで ReactiveProperty(InputNumbersCount 変数) を1つ削除できる
    //     // Call ボタンの有効状態(on/off)を入力された数字の数に応じて変更
    //     // ReactiveCommandの実行可否状態をButton.interactableプロパティにバインドして
    //     // ReactiveCommandの状態に応じてボタンが有効化・無効化される
    //     // ReactiveCommandの状態は Select の条件の評価により、true か false になる
    //     model.InputNumberList.ObserveCountChanged()
    //         .Select(count => count == 3) // 評価後、true になったらストリームを int から bool に変換
    //         .ToReactiveCommand() // bool型のストリームをReactiveCommandに変換
    //         .BindTo(view.CallButton) // その後BindToを使ってViewクラスのCallButtonプロパティに紐づけ
    //         .AddTo(disposableModels);
    //
    //     // ReactiveCollection クリア時の処理が必要な場合には追加
    //     model.InputNumberList.ObserveReset()
    //         .Subscribe(_ =>
    //         {
    //             view.UpdateInputDisplay(model.InputNumberList);
    //             view.SwitchAllButtons(true);
    //             //Debug.Log("Clear");
    //         })
    //         .AddTo(disposableModels);
    //
    //     // GameState の購読
    //     model.CurrentNumberGameState
    //         .Where(state => state == NumberGameState.Win || state == NumberGameState.Lose)
    //         .Subscribe(state =>
    //         {
    //             // ゲームの勝敗を反映してリザルト表示
    //             bool isSuccess = state == NumberGameState.Win;
    //             StartCoroutine(GenerateResult(isSuccess));
    //         })
    //         .AddTo(disposableModels);
    //
    //     //model.InitialModel();
    //     view.InitialView();
    //     //gameLogic.InitialGameLogic(model.CorrectNumbers);
    // }
    //
    // /// <summary>
    // /// 入力番号の評価処理
    // /// </summary>
    // private void ProcessInputNumbers() {
    //
    //     (int hit, int blow) result;
    //     model.IncrementAnsCount();
    //     result = gameLogic.CheckHitAndBlow(model.InputNumberList);
    //
    //     // 3HIT検出した場合
    //     if (result.hit == 3) {
    //         // ゲームクリア。解除成功メッセージを表示
    //         model.CurrentNumberGameState.Value = NumberGameState.Win;
    //
    //         return;
    //     }
    //
    //     // チャレンジ回数に達した場合
    //     else if (model.AnsCount.Value >= model.MaxCount) {
    //         // ゲーム失敗。解除失敗メッセージを表示
    //         model.CurrentNumberGameState.Value = NumberGameState.Lose;
    //
    //         return;
    //     }
    //
    //     // 不正解の場合
    //     StartCoroutine(ShowInputDetailCoroutine(result.hit, result.blow));
    // }
    //
    // // 不正解の場合の処理
    // private IEnumerator ShowInputDetailCoroutine(int hit, int blow) {
    //
    //     // NumberGameDetailPopUp が生成されていない場合
    //     if (!numberGameDetailPopUp) {
    //         // ポップアップ生成
    //         numberGameDetailPopUp = Instantiate(numberGameDetailPopUpPrefab, canvasTran, false);
    //     }
    //
    //     // 結果表示
    //     numberGameDetailPopUp.ShowResult(model.InputNumberList[0], model.InputNumberList[1], model.InputNumberList[2],
    //         hit, blow);
    //
    //     view.UpdateExplanation(model.AnsCount.Value, model.InputNumberList, hit, blow);
    //
    //     // 待機する
    //     yield return new WaitForSeconds(waitTime);
    //
    //     // NumberGameDetailPopUpを閉じる
    //     numberGameDetailPopUp.HidePopUp();
    //
    //     // 入力された数値をクリアし、表示を更新する
    //     model.ResetGame();
    // }
    //
    // /// <summary>
    // /// リザルトの生成
    // /// </summary>
    // /// <param name="isSuccess"></param>
    // /// <returns></returns>
    // private IEnumerator GenerateResult(bool isSuccess) {
    //
    //     // リザルトポップアップを生成
    //     NumberGameResultPopUp numberGameResultPopUp = Instantiate(numberGameResultPopUpPrefab, canvasTran, false);
    //
    //     // リザルト表示。ゲーム結果を受けて内容を作成
    //     yield return StartCoroutine(numberGameResultPopUp.ShowPopUp(isSuccess));
    //
    //     // ゲーム終了
    //     model.CurrentNumberGameState.Value = NumberGameState.GameUp;
    //
    //     // 入力された数値をクリアし、表示を更新する
    //     model.ResetGame();
    //
    //     disposableModels.Dispose();
    //
    //     // 登録した購読処理は、Dispose してしまうとずっと購読停止され続けるので、新しくインスタンスを作り直す
    //     // よって CompositeDisposable は使いまわせない。
    //     disposableModels = new();
    // }
}