前回の記事でシーンに必要なものが配置できました。
前回の記事:
で、今回の記事ではトランプのカードを配れるように山札を作り、カードを引く処理を作っていきます。
ここまでできるとデッキからカードを引いて戦うようなゲームの基本部分も作れるようになりますね。
まずはシーンを管理するSceneManagerを作ろう!
先にシーンを管理するゲームオブジェクトを作成します。
シーンに空のGameObjectを作成し、名前を「SceneManager」にしてください。
次にスクリプトをファイル名を「SceneManager」として作成してゲームオブジェクトにアタッチしておきましょう。
この段階ではスクリプトはいじらなくてOKです。
これから機能を順に追加していきます。
カード用のコンポーネントを作ろう!
今回の記事ではカードを配れるようにしていきますが、まずカードを表すスクリプトを作成しましょう!
スクリプトを新しく作成してください。ファイル名は「Card」として作成します。
作成したスクリプトは「Card」プレハブにコンポーネントとしてアタッチしてください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// … using UnityEngine; public class Card : MonoBehaviour { public enum Mark { Heart, Diamond, Spade, Crub, } public bool IsReverse = false; [Range(1, 13)] public int Number = 1; public Mark CurrentMark = Mark.Heart; public void SetCard(int number, Mark mark, bool isReverse) { Number = number; CurrentMark = mark; IsReverse = isReverse; } } |
Cardコンポーネントには次のメンバを定義しています。
- IsReverseフィールド:裏向きかどうか?
- Numberフィールド:トランプの数字。
- Markフィールド:トランプのマーク。
- SetCardメソッド:カードの内容を設定するメソッド
トランプの数字はNumberフィールドで表しています。
注意点としてトランプのAは1として、Jack,Queen,Kingはそれぞれ11,12,13として表現しています。
コンポーネントのフィールドが変更されたらプレハブの内容も変更するようにする
このままだとCardコンポーネントのフィールドをInspectorから変更しても「Card」プレハブの見た目は変更されません。
なので、Cardコンポーネントを修正し、プレハブのGameObjectの親子階層を調べてコンポーネントから設定できるようにしましょう!
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 |
// … using UnityEngine; using UnityEngine.UI; public class Card : MonoBehaviour { //… public void SetCard(int number, Mark mark, bool isReverse) { Number = Mathf.Clamp(number, 1, 13); CurrentMark = mark; IsReverse = isReverse; //CardプレハブのGameObjectを更新する。 //カードの裏表に合わせて色などを設定する var image = GetComponent<Image>(); if (IsReverse) { image.color = Color.black; } else { image.color = Color.white; } foreach(Transform child in transform) { child.gameObject.SetActive(!IsReverse); } //マークに合わせてGameObjectを設定する var markObj = transform.Find("Mark"); var markText = markObj.GetComponent<Text>(); switch(CurrentMark) { case Mark.Heart: markText.text = "❤️"; markText.color = Color.red; break; case Mark.Diamond: markText.text = "♦️"; markText.color = Color.red; break; case Mark.Spade: markText.text = "♠️"; markText.color = Color.black; break; case Mark.Crub: markText.text = "♣️"; markText.color = Color.black; break; } //数字に合わせてGameObjectを設定する var numberObj = transform.Find("NumberText"); var numberText = numberObj.GetComponent<Text>(); if(Number == 1) { numberText.text = "A"; } else if(Number == 11) { numberText.text = "J"; } else if(Number == 12) { numberText.text = "Q"; } else if(Number == 13) { numberText.text = "K"; } else { numberText.text = Number.ToString(); } } } |
少し長いコードになってしまいましたが、SetCardメソッドでは次の3つのことを行なっています。
- IsReverseの値を見て「Card」プレハブの背景色を変更し、子GameObjectをアクティブ・非アクティブ化している。
- CurrentMarkの値を見て「 Card」プレハブの「Mark」のテキストと色を変更している。
- Numberの値を見て「Card」プレハブの「NumberText」のテキストを変更している。
また、次のGameObject及びTransformのメンバを使用して「Card」プレハブのGameObjectにアクセスしています。
- GameObject.GetComponent<T>()メソッド: T型のコンポーネントをGameObjectから取得する
- Transform.FInd(string)メソッド:引数に渡した名前の子GameObjectを検索し取得する。
ここまでできたら、Cardコンポーネントのパラメータに合わせてプレハブの内容も変更されるようになっています。
コンポーネントは再生しないと実行されないですが、毎回再生するのもめんどうなので、OnValidateメソッドを追加し、Inspectorから入力した時でもプレハブに入力内容が適応されるようにしましょう!
1 2 3 4 5 6 7 8 9 |
//… public class Card : MonoBehaviour { //… private void OnValidate() { SetCard(Number, CurrentMark, IsReverse); } } |
トランプ山札を実装しよう カードの初期化・生成・山札を切る処理
カードの実装もできたので、次はカードを配れるようにするためシーンに山札を追加したいと思います。
ただし実際に山札をシーンに配置するのではなくて、SceneManagerコンポーネントの中のメンバフィールドとして用意します。
そのため画面からは内容を確認できないので注意してください。
それではトランプのカードをデータとして表現するために先にCardコンポーネントにDataクラスを追加します。
1 2 3 4 5 6 7 8 9 |
//… public class Card : MonoBehaviour { public class Data { public Mark Mark; public int Number; } //… } |
DataクラスはCardコンポーネント以外でカードを扱うためのクラスになります。
このクラスのリストを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 System.Collections.Generic; using UnityEngine; public class SceneManager : MonoBehaviour { [Min(100)] public int ShuffleCount = 100; List<Card.Data> cards; private void Awake() { InitCards(); //確認用のコード } 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; } } } |
上のサンプルコードでは次のことを行なっています。ループ文や乱数などが入り、少し複雑なコードになっていますが、頑張って実装してください。
- AwakeメソッドでInitCardsメソッドを呼び出し山札を初期化している。
- InitCardsメソッドではCard.Dataクラスをトランプの枚数分だけ作成している。
- InitCardsメソッドの最後でShuffleCardsメソッドを呼び出し作成したカードの順番を適当にシャッフルしている。
- ShuffleCardsメソッドでは乱数を表すSystem.Randomクラスを使用し、ShuffleCountフィールドに設定した回数分cardsフィールドの要素の順番を一枚づつ入れ替えている
ShuffleCardsメソッドのループ分にあるカードの位置を入れ替えるというコメントの部分で実際に山札のカードを入れ替えています。
その部分のコードは2つの変数の値を入れ替える処理になっています。
入れ替えの時に変数が2つだけだとどちらかが上書きされてしまい入れ替えできないので、tmp変数を新しく用意し、片方の変数の値を記録しているのに注目してください。
乱数の使い方
C#において乱数を使用する際はSystem.Random
クラスを使うのが一番簡単です。
System.Random
クラスの使い方は次のサンプルコードのようなものになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
{//numとnum2の値は実行するたびに異なる値が設定される var random = new System.Random(); var num = random.Next(); // int型のランダムな値が返ってくる var num2 = random.Next(); // int型のランダムな値が返ってくる } {//シード値を渡すと乱数ではあるがシード値によって毎回決まった値が返ってくるようになる int seed = 100; // <- 値は適当でOK var random = new System.Random(seed); //numとnum2の値は乱数ではあるが、毎回決まったものになる。 // どの値になるかはseedの値によって決まる。 var num = random.Next(); var num2 = random.Next(); } |
一般的なプログラミングおよびコンピュータの世界において完璧な乱数を作ることはできません。
そのため数値の計算を工夫した擬似的な乱数が使用されています。
上のサンプルコードの下にあるコードではSystem.Random
クラスのコンストラクタにシード値を渡していますが、それは擬似的な乱数を計算する上での初期値を渡すことになります。
擬似的な乱数はあくまで1+1みたいな普通の数値計算を行なっているため、System.Random
クラスの内部のデータが同じならSystem.Random.Next
メソッドが返す値は同じになってしまいます(乱数の種となるシード値は有限の数であるため、真の乱数は作れない)。
C#ではシード値を渡さなければ、毎回異なるシード値が使用されるのでそこまで気にしなくてもいいようになっています。
が、擬似的な乱数ということを覚えておくと乱数を多用した際に同じような数値が出てきても慌てずに済みます。
(C#の標準ライブラリより精度の高い乱数生成器なども存在します。メルセンヌツイスタなどが有名どころで、もし乱数のパターン化を避けたい場合にはこうした上質な乱数生成器を利用しましょう)
トランプのカードを配る処理を実装する
それでは山札をSceneManagerに組み込みましたので、ひとまず簡単にシーン上にあるディーラーとプレイヤーにカードを渡してみましょう!
こちらも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 58 59 60 61 62 63 64 |
// … using System.Collections.Generic; using UnityEngine; public class SceneManager : MonoBehaviour { // … public Card CardPrefab; public GameObject Dealer; public GameObject Player; //… private void Update() {//確認用のコード if(Input.GetKeyDown(KeyCode.Space)) { DealCards(); } } //.. 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); } } } } |
上のサンプルコードでは次のものをSceneManagerコンポーネントに追加しています。
- 「Card」プレハブを表すフィールド
- ディーラーのカードのルートGameObjectを表すフィールド
- プレイヤーのカードのルートGameObjectを表すフィールド
- カードを一枚配るDealCardメソッド
- カードをシーン全体に配るDealCardsメソッド
フィールドを追加しているのでシーンにあるSceneManagerコンポーネントに忘れずアタッチするようにしてください。
- CardPrefab:「Card」プレハブ
- Dealer:シーンの「Dealer」GameObject
- Player:シーンの「Player」GameObject
また、動作確認用にUpdateメソッドの中でスペースキーを押すとDealCardsメソッドを呼び出すようにしています。
この処理は動作確認用なため後々Updateメソッドごと削除します。
注意点として、ボタンを押し続けていくと山札のカードがなくなってDealCardメソッドがnullを返すようになってしまいます。
そうなるとサンプルコードではnullデータに対応していないので例外が発生してしまいます。
実際のブラックジャックではそこまでカードを引くことはないのでこの講座では問題が出るまでこのまま進めていきます。
ここまでできたら、山札からカードをシーン上に配れる状態になっているはずです。
スクリプト上からプレハブを複製する方法
DealCardsメソッドの中で「Card」プレハブを複製しています。
スクリプト上でプレハブを複製するにはUnityEngine.Object.Instantiateメソッド
を使用します。
1 |
var holeCardObj = Object.Instantiate(CardPrefab, Dealer.transform); |
UnityEngine.Object.Instantiate
メソッドにはいくつかメソッドのオーバーロードが用意されています。
この記事では複製するプレハブと複製した際の親にするGameObjectを指定するものを使用しています。
まとめ
今回の記事でトランプのカードを表すプレハブとそれを制御するコンポーネントを追加しました。
また、まだ仮ではありますがシーンにカードを配れるようにもしました。
今回の記事をまとめると次のようになります。
Transform.Find
メソッドを使用すると子GameObjectを名前で検索できる。- 乱数を扱うには
System.Random
クラスを使用する。 - プログラミング上の乱数は擬似的なものなので、内部データによっては同じ値を返すこともある。
- スクリプト上でプレハブを複製するときは
UnityEngine.Object.Instantiate
メソッドを使用する。
それでは次の記事に行ってみましょう!
次回の記事:
参考用:スクリプトの完成図
ちなみに今回の記事で作成したスクリプトの最終的な内容は次のものになります。
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 |
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class Card : MonoBehaviour { public class Data { public Mark Mark; public int Number; } // Start is called before the first frame update public enum Mark { Heart, Diamond, Spade, Crub, } public bool IsReverse = false; [Range(1, 13)] public int Number = 1; public Mark CurrentMark = Mark.Heart; public void SetCard(int number, Mark mark, bool isReverse) { Number = Mathf.Clamp(number, 1, 13); CurrentMark = mark; IsReverse = isReverse; //CardプレハブのGameObjectを更新する。 //カードの裏表に合わせて色などを設定する var image = GetComponent<Image>(); if (IsReverse) { image.color = Color.black; } else { image.color = Color.white; } foreach (Transform child in transform) { child.gameObject.SetActive(!IsReverse); } //マークに合わせてGameObjectを設定する var markObj = transform.Find("Mark"); var markText = markObj.GetComponent<Text>(); switch (CurrentMark) { case Mark.Heart: markText.text = "❤️"; markText.color = Color.red; break; case Mark.Diamond: markText.text = "♦️"; markText.color = Color.red; break; case Mark.Spade: markText.text = "♠️"; markText.color = Color.black; break; case Mark.Crub: markText.text = "♣️"; markText.color = Color.black; break; } //数字に合わせてGameObjectを設定する var numberObj = transform.Find("NumberText"); var numberText = numberObj.GetComponent<Text>(); if (Number == 1) { numberText.text = "A"; } else if (Number == 11) { numberText.text = "J"; } else if (Number == 12) { numberText.text = "Q"; } else if (Number == 13) { numberText.text = "K"; } else { numberText.text = Number.ToString(); } } private void OnValidate() { SetCard(Number, CurrentMark, IsReverse); } } |
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 |
using System.Collections; using System.Collections.Generic; using UnityEngine; public class SceneManager : MonoBehaviour { public Card CardPrefab; public GameObject Dealer; public GameObject Player; [Min(100)] public int ShuffleCount = 100; List<Card.Data> cards; private void Awake() { InitCards(); //確認用のコード } private void Update() {//確認用のコード if (Input.GetKeyDown(KeyCode.Space)) { DealCards(); } } 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); } } } } |
コメント