前回の記事でカードを配れるようになりました!
前回の記事:
今回の記事では実際にブラックジャックのゲームの流れを作っていきましょう!
賭け金を決めてベットする処理やコルーチンを用いたゲームの管理システムを構築していきます。
ブラックジャックのゲームの流れをコルーチンで管理する
ブラックジャックでは次の流れを繰り返します。
- このゲームのベットを決める
- カードを配る
- 配られたカードを見てヒットするかスタンドするか決める
- ゲームの結果を判定する
これを実装するにあたってコンポーネントのUpdateメソッドを利用すると余計な処理が必要になり、管理が難しくなります。
なので、この記事ではUpdateメソッドを使わず代わりにコルーチンを利用します。
次のサンプルコードからはコルーチンを使用してゲームの流れを管理していきます。
関連記事:Unity C#のIEnumerable・IEnumeratorとコルーチンの使い方・作り方
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
//… public class SceneManager : MonoBehaviour { //… IEnumerator GameLoop() { while(true) { InitCards(); //カードを初期化する yield return null;//何か実装するまで残しておく //ベットを決めるまで待つ //カードを配る //プレイヤーが行動を決めるまで待つ //行う行動に合わせて処理を分岐する //ゲームの結果を判定する } } Coroutine _gameLoopCoroutine; private void Start() { _gameLoopCoroutine = StartCoroutine(GameLoop()); } //Updateメソッドは削除する //… } |
GameLoopメソッドのwhile文の中でゲームの流れを管理するようにします。
While文は条件式にtrueを渡し無限ループするようになっていますが、メソッドがコルーチンとして実行されかつ、while文の中でyield returnしているので、プログラムがフリーズすることはありません。
この記事ではこのメソッドの内容を順に拡張していきますので、今は実装していく内容のコメントだけにしてください。
また、何か実装するまでwhile文の中のyield return nullを忘れないようにしてください。
もしこの部分がない状態で再生すると無限ループに入り、Unityエディターがフリーズしてしまうので注意してください。
また、この段階で前の記事で追加したUpdateメソッドを削除してください。
ベット(掛け金)を指定できるシステムを作ろう!
まずブラックジャックではゲーム開始時にそのゲームに掛けるポイント(ベット)を指定します。
そのためにはどれくらいのポイントを掛けるのか入力する必要があるので、ベット専用のUIをシーンに追加していきましょう!
そのために次の手順を行なってください。
- メニューのGameObject > UI > Canvasをクリックし新しいCavansを追加する
追加したCanvasの名前を「InputBetsDialog」とする
- 「InputBetsDialog」の各コンポーネントは初めに作った「Canvas」と同じものにする
- 「InputBetsDialog」のCanvasコンポーネントのSort Orderを100に設定する。
- 「InputBetsDialog」の子GameObjectとしてUI > Imageを作成し、名前を「Background」にする(背景の白画像が不要であればImageコンポーネントを消してもOK)
- 「Background」の子GameObjectとして次のものを追加する
- – UI > TextでGameObjectを作成し、「Label」という名前にする。
- – UI > Input FieldでGameObjectを作成し、「BetsInput」という名前にする。
- – UI > ButtonでGameObjectを作成し、「OKButton」という名前にする。テキストは「OK」にする。
- 作成したGameObject達をいい感じになるように配置する
これらの作業が終わると次のような画面になります。
配置に関しては読者の方の自由でOKです。RectTransformのアンカーやLayoutGroupなどを使用するといいでしょう。
この作業の中で「BetsInput」が値を入力するためのGameObjectになります。
「BetsInput」にはUnityEngine.UI.InputField
コンポーネントがアタッチされており、そのコンポーネントが入力を管理しています。
それでは再生中に「BetsInput」に値を入力したら、「OKButton」をクリックすることでゲームにかけるベットを決定できるようにしていきましょう!
Canvasの描画順序の指定の仕方
CanvasコンポーネントのSorting OrderでCanvasコンポーネントの描画順序を指定することができます。
大きい値のものほど画面の前に表示されますので、複数のCavansを使用する際に利用してください。
アタッチされたコンポーネントのコピーの仕方
UnityエディターではInspectorからアタッチされているコンポーネントをコピーしたり、貼り付けたりできます。
これらの作業を行いたい場合はInspectorのコンポーネントの右上にあるボタンを押してメニューを開き、その中から行いたいものをクリックしてください。
SceneManagerコンポーネントに組み込む
シーンにベット入力用のGameObjectを配置したら、次はSceneManagerにそれを組み込みましょう!
この記事の序盤で説明した通り、GameLoopメソッドの中に処理を追加していきます。
GameLoopメソッドのwhile文の初めにベット入力を待つ処理を追加します。
ベット入力には先に作った「BetsInputDialog」を使用しますので、それと「BetsInput」をSceneManagerコンポーネントのフィールドに追加しています。
スクリプトのコンパイルに成功したら、そちらも忘れずにアタッチしてください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
//… using UnityEngine.UI; public class SceneManager : MonoBehaviour { //… public GameObject BetsInputDialog; public InputField BetsInput; public Text BetsText; public Text PointText; //パラメータ public int StartPoint = 20; int currentPoint; int currentBets; //… IEnumerator GameLoop() { //… currentPoint = StartPoint; BetsText.text = "0"; PointText.text = currentPoint.ToString(); //… while(true) { //… //ベットを決めるまで待つ do { BetsInputDialog.SetActive(true); yield return new WaitWhile(() => BetsInputDialog.activeSelf); //入力したテキストを使用できるものかチェックする if(int.TryParse(BetsInput.text, out var bets)) { if(0 < bets && bets <= currentPoint) { currentBets = bets; break; } } } while (true); //画面の更新 BetsInputDialog.SetActive(false); BetsText.text = currentBets.ToString(); yield return new WaitForSeconds(2);// <- 動作確認用なので後々消す //カードを配る // プレイヤーが行動を決めるまでまつ //行う行動に合わせて処理を分岐する // ゲームの結果を判定する } } //… } |
上のサンプルコードではベットの入力するために次の処理を行なっています。
- まず「BetsInputDialog」をアクティブにし画面に表示する
- 「BetsInputDialog」が非アクティブになるまで待つ(yield return new WaitUntil(条件);で条件の間処理を止めてくれる)
- 非アクティブになったら「BetsInput」に入力されたテキストをチェックする
- チェックの結果、賭けられる数値ならそれを使用する
- できない場合はベット入力をもう一度初めからやり直す。
結構難しい処理になっていますが、入力した値の検証をする必要があるのでこのような手順で処理を行っています。
無事作成できたら、次は「OKButton」のクリック操作を設定しましょう。
「OKButton」のInspectorからButtonコンポーネントのOnClickイベントを次のように設定してください。
- 「InputBetsDialog」をアタッチし、その中のGameObjectのSetActiveメソッドを選択する。引数はfalseにする。
こうすると「OKButton」を押すたびに「InputGetsDialog」が非アクティブになります。
ただし、現在のポイントより大きいものを入力したときや数値でない時は何もおきません。
InputFieldコンポーネントのイベントを利用して入力値を検証する
Unity UIのInputFieldコンポーネントには入力したテキストが変更された時のイベントが用意されています。
それを利用すると入力値の検証ができるので使用できるベットの値の時のみ「OKButton」を押すことができるようにしましょう!
次のサンプルコードを実装できましたら必ずSceneManagerコンポーネントのBetsInputOKButtonフィールドには「OKButton」をアタッチしてください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
//… public class SceneManager : MonoBehaviour { //… public Button BetsInputOKButton; //… private void Awake() { BetsInput.onValidateInput = BetsInputOnValidateInput; BetsInput.onValueChanged.AddListener(BetsInputOnValueChanged); } char BetsInputOnValidateInput(string text, int startIndex, char addedChar) { if (!char.IsDigit(addedChar)) return '\0'; return addedChar; } void BetsInputOnValueChanged(string text) { BetsInputOKButton.interactable = false; if (int.TryParse(BetsInput.text, out var bets)) { if (0 < bets && bets <= currentPoint) { BetsInputOKButton.interactable = true; } } } //… } |
今回はInspector上ではなくスクリプト上から直接イベントを設定しています。
InputField.onValidateInput
はスクリプト上からしかイベントを設定できないので注意しましょう!
設定しているイベントは次のものになります。
どれも「BetsInput」のテキストが変更されたときに呼び出されるイベントになります。
- BetsInputOnValidateInput:入力された文字が数字かどうか判定している。
- BetsInputOnValueChanged:入力された値がベットに使えるなら、「OKButton」を有効化する。
InputField.onValidateInputの戻り値にヌル文字(’\0’)を指定すると入力した文字はテキストに追加されませんので覚えておくといいでしょう!
また、Unity UIのButtonコンポーネントのinteractableをfalseに設定するとそのボタンはクリックすることができません。trueにすると再びクリックすることができます。
このサンプルコードを組み込むと「BetsInput」には数字しか入力できず、使用できるベットの値の時のみ「OKButton」が押せるようになります。
ベットを入力したらプレイヤーとディーラーにカードを配ろう
ここまででベットの入力ができたので、次はプレイヤーとディーラーにカードを配ってみましょう!
こちらはGameLoopメソッドを拡張します。カードを配る処理はDealCardsメソッドで既に実装できているので、そちらを使用します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
//… public class SceneManager : MonoBehaviour { public GameObject BetsInputDialog; public InputField BetsInput; //… IEnumerator GameLoop() { while(true) { //ベットを決めるまで待つ //… //カードを配る DealCards(); yield return new WaitForSeconds(2);// <- 動作確認用なので後々消す // プレイヤーが行動を決めるまでまつ //行う行動に合わせて処理を分岐する // ゲームの結果を判定する } } //… } |
プレイヤーの行動を選択するためのボタンを実装しよう
無事カードを配れたので、次はプレイヤーの行動を実装しましょう!
プレイヤーの行動は画面右にある「Buttons」の中のボタンで行います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
//… public class SceneManager : MonoBehaviour { //… public enum Action { WaitAction = 0, Hit = 1, Stand = 2, } Action CurrentAction = Action.WaitAction; public void SetAction(int action) { CurrentAction = (Action)action; } //… IEnumerator GameLoop() { while(true) { //ベットを決めるまで待つ //… //カードを配る //… // プレイヤーが行動を決めるまで待つ bool waitAction = true; do { CurrentAction = Action.WaitAction; yield return new WaitWhile(() => CurrentAction == Action.WaitAction); // 行う行動に合わせて処理を分岐する switch (CurrentAction) { case Action.Hit: PlayerDealCard(); waitAction = true; break; case Action.Stand: waitAction = false; break; default: waitAction = true; throw new System.Exception("知らない行動をしようとしています。"); } } while (waitAction); // ゲームの結果を判定する } } //… void PlayerDealCard() { var cardObj = Object.Instantiate(CardPrefab, Player.transform); var card = DealCard(); cardObj.SetCard(card.Number, card.Mark, false); } //… } |
上のサンプルコードを追加できたら、「Buttons」のボタンにSceneManagerのメソッドを設定しましょう。
設定するメソッドはSetActionメソッドになります。
ここで注意点としてUnityエディターは標準で列挙型に対応していないので代わりにint型を引数に渡すようにしています。
- 「HitButton」にSceneManagerコンポーネントのSetActionメソッドを設定し、引数を1にする
- 「StandButton」にSceneManagerコンポーネントのSetActionメソッドを設定し、引数を2にする
int型は列挙型に変換できます。その際、実際の列挙型の値と一致する数値を使用してください。
ただし、存在する列挙型の値かどうかはint型の値次第なので、念のためチェック処理をGameLoopメソッドの中で行っています。
Action列挙型の値の各数値は次のようになっています。
- Action.WaitAction: 0
- Action.Hit:1
- Action.Stand:2
ここまで問題なくできたら、行動ボタンによって処理を分岐できるようになっています。
最後に、動作確認用に入れておいた「yield return new WaitForSeconds(2);」の命令を消しておきましょう。
まとめ
今回の記事でブラックジャックのゲームの流れができました!
あとはゲームの判定処理や細かいルールを実装すると完成になのでもう少し頑張ってみましょう!
今回の記事をまとめると次のようになります。
- コルーチンを使うとゲームの流れを簡単に作ることができる
UnityEngine.Canvas
のSortingOrderを使うと複数のCanvasの描画順を決めることができる。UnityEngine.UI.InputField
を使用するとテキストを簡単に入力することができる。- UnityEventの引数には列挙型を設定できないので代わりにint型を使用する
それでは次の記事に行ってみましょう!
次回の記事:
参考用:スクリプトの完成図
ちなみに今回の記事で作成したスクリプトの完成図は次のようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 |
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class SceneManager : MonoBehaviour { public Card CardPrefab; public GameObject Dealer; public GameObject Player; public GameObject BetsInputDialog; public InputField BetsInput; public Button BetsInputOKButton; public Text BetsText; public Text PointText; //パラメータ public int StartPoint = 20; int currentPoint; int currentBets; [Min(100)] public int ShuffleCount = 100; List<Card.Data> cards; public enum Action { WaitAction = 0, Hit = 1, Stand = 2, } Action CurrentAction = Action.WaitAction; public void SetAction(int action) { CurrentAction = (Action)action; } private void Awake() { BetsInput.onValidateInput = BetsInputOnValidateInput; BetsInput.onValueChanged.AddListener(BetsInputOnValueChanged); } char BetsInputOnValidateInput(string text, int startIndex, char addedChar) { if (!char.IsDigit(addedChar)) return '\0'; return addedChar; } void BetsInputOnValueChanged(string text) { BetsInputOKButton.interactable = false; if (int.TryParse(BetsInput.text, out var bets)) { if (0 < bets && bets <= currentPoint) { BetsInputOKButton.interactable = true; } } } IEnumerator GameLoop() { currentPoint = StartPoint; BetsText.text = "0"; PointText.text = currentPoint.ToString(); while (true) { InitCards(); //カードを初期化する yield return null;//何か実装するまで残しておく //ベットを決めるまで待つ do { BetsInputDialog.SetActive(true); yield return new WaitWhile(() => BetsInputDialog.activeSelf); //入力したテキストを使用できるものかチェックする if (int.TryParse(BetsInput.text, out var bets)) { if (0 < bets && bets <= currentPoint) { currentBets = bets; break; } } } while (true); //画面の更新 BetsInputDialog.SetActive(false); BetsText.text = currentBets.ToString(); //カードを配る DealCards(); // プレイヤーが行動を決めるまで待つ bool waitAction = true; do { CurrentAction = Action.WaitAction; yield return new WaitWhile(() => CurrentAction == Action.WaitAction); // 行う行動に合わせて処理を分岐する switch (CurrentAction) { case Action.Hit: PlayerDealCard(); waitAction = true; break; case Action.Stand: waitAction = false; break; default: waitAction = true; throw new System.Exception("知らない行動をしようとしています。"); } } while (waitAction); //行う行動に合わせて処理を分岐する //ゲームの結果を判定する } } Coroutine _gameLoopCoroutine; private void Start() { _gameLoopCoroutine = StartCoroutine(GameLoop()); } void InitCards() { cards = new List<Card.Data>(13 * 4); var marks = new List<Card.Mark>() { Card.Mark.Heart, Card.Mark.Diamond, Card.Mark.Spade, Card.Mark.Crub, }; foreach(var mark in marks) { for(var num=1; num<=13; ++num) { var card = new Card.Data() { Mark = mark, Number = num, }; cards.Add(card); } } ShuffleCards(); } void ShuffleCards() { //シャッフルする var random = new System.Random(); for(var i=0; i<ShuffleCount; ++i) { var index = random.Next(cards.Count); var index2 = random.Next(cards.Count); //カードの位置を入れ替える。 var tmp = cards[index]; cards[index] = cards[index2]; cards[index2] = tmp; } } Card.Data DealCard() { if (cards.Count <= 0) return null; var card = cards[0]; cards.Remove(card); return card; } void DealCards() { foreach (Transform card in Dealer.transform) { Object.Destroy(card.gameObject); } foreach (Transform card in Player.transform) { Object.Destroy(card.gameObject); } { //ディーラーに2枚カードを配る var holeCardObj = Object.Instantiate(CardPrefab, Dealer.transform); var holeCard = DealCard(); holeCardObj.SetCard(holeCard.Number, holeCard.Mark, true); var upCardObj = Object.Instantiate(CardPrefab, Dealer.transform); var upCard = DealCard(); upCardObj.SetCard(upCard.Number, upCard.Mark, false); } { //プレイヤーにカードを2枚配る for (var i = 0; i < 2; ++i) { var cardObj = Object.Instantiate(CardPrefab, Player.transform); var card = DealCard(); cardObj.SetCard(card.Number, card.Mark, false); } } } void PlayerDealCard() { var cardObj = Object.Instantiate(CardPrefab, Player.transform); var card = DealCard(); cardObj.SetCard(card.Number, card.Mark, false); } } |
コメント
コメント失礼します。
「SceneManegerコンポーネントに組み込む」でサンプルコードは「yield return new WaitWhile(()」、説明は「yield return new WaitUntil(条件);」となってますがこれはどちらが正しいのでしょうか
どっちも正しいですよー!
yield return new WaitWhile(() => CurrentAction == Action.WaitAction);
みたいな感じで()の中に() => CurrentAction == Action.WaitActionの条件文が入っています。
yield return new WaitWhile(() で分けて考えるものではないですね。
別講座になりますが、Wait系メソッドやラムダ式(ラムダ関数)に関する解説は
https://feynman.co.jp/unityforest/unity-introduction/unity-csharp-programming/delegete-event-unityaction/
https://feynman.co.jp/unityforest/game-create-lesson/tower-defense-game/atari-hantei-arrow-shooting/
このあたりでも行っています。