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(); } }
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; } }
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(); // } }