Unity シングルトン 今さらSingletonMonoBehaviour を作ってみた
Unityでシングルトンクラス用の基底クラスを作成してみました
SingletonMonoBehaviour は 「MonoBehaviour」を継承したシングルトンクラスでありMonoBehaviourの特性も活用できる便利なシングルトンクラスとして多くの方々に使用されています。
私が作ったSingletonMonoBehaviourは以下の形式になっています。
using System; using System.Collections; using System.Collections.Generic; using UnityEngine; //=============================================== // デザインパターン : シングルトン //=============================================== /// <summary> /// MonoBehaviourを継承したシングルトンクラス /// </summary> // namespaceをつけてね namespace Framework { public class SingletonMonoBehaviour<T> : BaseSingletonMonoBehaviour where T : BaseSingletonMonoBehaviour { //=============================================== // シングルトン機構 //=============================================== private static T mInstance; public static T Instance { get { // インスタンスを生成する if (mInstance == null) { // テンプレートに合わせた名前のオブジェクトを生成する GameObject obj = new GameObject (typeof (T).FullName); // シーン遷移後も破棄されないオブジェクトに設定する DontDestroyOnLoad (obj); // インスタンスに生成したオブジェクトを設定する mInstance = obj.AddComponent<T> (); // 初期化処理 mInstance.Init (); } return mInstance; } } //=============================================== // メソッド //=============================================== /// <summary> /// 自身のオブジェクトを削除する /// </summary> public static void OnDestroy () { // インスタンスが存在する場合 if (mInstance != null) { // インスタンスオブジェクトを削除する Destroy (mInstance.gameObject); mInstance = null; } } } /// <summary> /// SingletonMonoBehaviourの基底クラス /// </summary> public class BaseSingletonMonoBehaviour : MonoBehaviour { /// <summary> /// 継承先の初期化処理 /// </summary> protected virtual void mOnInit () { } /// <summary> /// 初期化処理 /// </summary> public void Init () { // 継承先の初期化処理 this.mOnInit (); } } }
解説
まず「SingletonMonoBehaviour」の基底クラスになる「BaseSingletonMonoBehaviour」を作成しました。
こちらは「SingletonMonoBehaviour」クラスの継承先で初期化処理 を使うときに役立つかな〜と思い作成しました。
こちらは特別必要ではないので、いらない人は消してもらって構いません。
「SingletonMonoBehaviour」ではインスタンスを生成しています。
オブジェクトを作成し、DontDestroyOnLoad で破棄されない用にしています。
これでシーン遷移が起きても存在し続けます。
Unityでもじぴったんのスタンバイアニメーションを再現する!
先週は「Unity1Week」開催中なこともあって、お題の「探す」をテーマにしたゲームを作ろうとしていたのですが、どういう訳か19年ほど前の名作ゲーム「もじぴったん」のゲーム開始前の演出を作成していました。
もう19年も前なのか
1. もじぴったんを再現したソースコード
using System; using System.Collections; using System.Collections.Generic; using DG.Tweening; using UniRx; using UnityEngine; using UnityEngine.UI; public class ImgFade : MonoBehaviour { public RectTransform imageRt; public RectTransform LimitTextRt; public Image image; public CanvasGroup canvasGroup; public CanvasGroup canvasGroupText; private Vector3 saveSize; public Text FadeText; public RectTransform LimitTitleText; public RectTransform LimitText; public RectTransform ReadyText; public Text mReadyText; private int colorType = 0; void Awake () { this.canvasGroup.alpha = 0f; this.canvasGroupText.alpha = 0f; this.mSetYellowText (); } private void mSetYellowText () { string red = "<size=84><color=#ffea00>4</color></size>"; string yellow = "<color=#ff0000>文字</color>"; string white = "<color=#ffffff>のことばを あと</color>"; string yellow2 = "<size=84><color=#ff0000>3</color></size>"; string red2 = "<color=#ffea00>こ</color>"; string white2 = "<color=#ffffff>作れ</color>"; this.FadeText.text = red + yellow + white + yellow2 + red2 + white2; } private void mSetRedText () { string red = "<size=84><color=#fad000>4</color></size>"; string yellow = "<color=#fad000>文字</color>"; string white = "<color=#fad000>のことばを あと</color>"; string yellow2 = "<size=84><color=#fad000>3</color></size>"; string red2 = "<color=#fad000>こ</color>"; string white2 = "<color=#fad000>作れ</color>"; this.FadeText.text = red + yellow + white + yellow2 + red2 + white2; } void Start () { Observable.Timer (TimeSpan.FromSeconds (2)) .Subscribe (_ => { Sequence sequence = DOTween.Sequence () .OnStart (() => { }) .Append (this.imageRt.DOScale (Vector3.one, 1f)).SetEase (Ease.OutCubic) .Join (this.canvasGroup.DOFade (1f, 0.8f)).SetEase (Ease.Linear) .OnComplete (() => { Debug.Log ("Comp"); this.setColor (); // 回数表示 this.mSetLimitText (); }); sequence.Play (); }).AddTo (this); } private void setColor () { Observable.Interval (TimeSpan.FromSeconds (1)) .Subscribe (_ => { if (this.colorType == 0) { this.colorType = 1; this.image.color = Color.red; this.mSetRedText (); } else { this.image.color = new Color (1, 0.4f, 0); this.mSetYellowText (); this.colorType = 0; } }).AddTo (this); } private void mSetLimitText () { Observable.Timer (TimeSpan.FromSeconds (2)) .Subscribe (_ => { this.mLimitTextAnimation (); }).AddTo (this); } private void mLimitTextAnimation () { Sequence sequence = DOTween.Sequence () .Append (this.LimitTitleText.DOScale (Vector3.one, 1f)).SetEase (Ease.OutCubic) .Join (this.LimitText.DOScale (Vector3.one, 1.5f)).SetEase (Ease.OutCubic) .Join (this.canvasGroupText.DOFade (1f, 0.8f)).SetEase (Ease.Linear) .OnComplete (() => { Debug.Log ("Comp mLimitTextAnimation"); this.mLimitTextCoolTime (); }); } private void mLimitTextCoolTime () { Observable.Timer (TimeSpan.FromSeconds (2)) .Subscribe (_ => { this.mLimitTextUpper (); }).AddTo (this); } private void mLimitTextUpper () { Sequence sequence = DOTween.Sequence () .Append (this.LimitTitleText.DOAnchorPosY (400f, 1f)).SetEase (Ease.Linear) .OnComplete (() => { Debug.Log ("Comp mLimitTextUpper"); }); Sequence sequence2 = DOTween.Sequence () .Append (this.LimitText.DOAnchorPosY (280f, 1f)).SetEase (Ease.Linear).SetDelay (0.5f) .OnComplete (() => { Debug.Log ("Comp mLimitTextUpper"); }); Sequence sequence3 = DOTween.Sequence () .Append (this.imageRt.DOAnchorPosY (-450, 1f)).SetEase (Ease.Linear).SetDelay (0.8f) .OnComplete (() => { Debug.Log ("Comp mLimitTextUpper"); this.mSetReadyText (); }); } private void mSetReadyText () { Sequence sequence = DOTween.Sequence () .Append (this.ReadyText.DOAnchorPosX (50f, 1f)).SetEase (Ease.Linear) .AppendInterval (1f) .OnComplete (() => { this.mReadyText.text = "ごー!"; this.mSetGoText (); }); } private void mSetGoText () { Sequence sequence = DOTween.Sequence () .AppendInterval (0.5f) .Append (this.ReadyText.DOAnchorPosX (1300f, 1f)).SetEase (Ease.Linear) .OnComplete (() => { Debug.Log ("mSetGoText"); }); } }
テスト用に作成したのでスクリプトや関数名はすごい適当です。
アニメーションの再現方法は
1 . Dotweenを使ってテキストやイメージのアニメーションを行う
2 . TextのRichTextを適応して動的にテキストの色を変更する
の2つを使用しています。
次にUnity側の実装を見ていきましょう。
思わずなんだこれは!?と思ってしまう散らかりっぷりですね・・・(反省)
最初のimage ですが、ここでは最初のオレンジの帯とテキストの部分を担当しています。
上記動画の部分ですね。最初に表示され、ゲームのルールを知らせます。
次は残り時間などを表示する部分です。
今回は残り回数という表記で表しています (当初作成する予定のゲームルールに合わせて)
ここもUnityのTextをDotweenで動かしています。
あと、ゲームルールを知らせるTextですがタイマー制御で1秒ごとに背景とTextの色とサイズを変更しています。
ソースコードでいうとこの部分に該当
private void mSetYellowText () { string red = "<size=84><color=#ffea00>4</color></size>"; string yellow = "<color=#ff0000>文字</color>"; string white = "<color=#ffffff>のことばを あと</color>"; string yellow2 = "<size=84><color=#ff0000>3</color></size>"; string red2 = "<color=#ffea00>こ</color>"; string white2 = "<color=#ffffff>作れ</color>"; this.FadeText.text = red + yellow + white + yellow2 + red2 + white2; } private void mSetRedText () { string red = "<size=84><color=#fad000>4</color></size>"; string yellow = "<color=#fad000>文字</color>"; string white = "<color=#fad000>のことばを あと</color>"; string yellow2 = "<size=84><color=#fad000>3</color></size>"; string red2 = "<color=#fad000>こ</color>"; string white2 = "<color=#fad000>作れ</color>"; this.FadeText.text = red + yellow + white + yellow2 + red2 + white2; }
RichTextが有効の場合、html形式が有効になるのでソースコード上で動的に変更することが可能です。
細かいアニメーションをやりたい場合はオススメの方法ですね。
最後に左側から出現する「レディー?」部分を見てみましょう。
こちらは左側からTextを中央に移動させ、タイマー管理で文字を変更し右側に移動させています。
色々とゴチャゴチャしていますが1つ1つの処理はとても単純で作りやすいですね
流石もじぴったん
UIアニメーションを入れるだけでゲームの面白さがガツンと上がると思っています。
これからも積極的に導入していきたいですね。
【Unity ゲーム制作】神経衰弱ゲームを作ろう!〜その11 (スタートとリザルト画面を作成しよう!)
前回はEnum (列挙型 )を使って、ゲームのステート管理を実装しました。
今回は 「スタート」と「リザルト」のステート画面の作成を行いましょう!
1 . 「スタート」画面を作成する
まずがUnityのHierarchyにStart画面の作成を作成しましょう。
今回は、このような形のスタート画面を作成しました
(センスについては言わないで)
内容はTextとButtonを組み合わせた形式になっています。
次にスタート画面用のスクリプトを作成します。
「StartStateManager.cs」を作成しましょう。
スタート画面を作成しましたが、何かしらのアニメーションを追加したいと思います。
先ほど作成したGameStartText に拡大縮小アニメーションを追加しましょう。
2. StartStateManager.cs に拡大縮小アニメーションを追加する
StartStateManager.csに以下の処理を実装しましょう。
using System.Collections; using System.Collections.Generic; using DG.Tweening; using UnityEngine; using UnityEngine.UI; public class StartStateManager : MonoBehaviour { // ゲームの開始テキストの座標 public RectTransform GameStartTextRt; /// <summary> /// テキストの拡大アニメーション /// </summary> public void EnlarAnimation () { this.GameStartTextRt.DOScale (Vector3.one * 1.5f, 0.5f) .OnComplete (() => { // テキストの縮小アニメーション this.mShrinkAnimation (); }); } /// <summary> /// テキストの縮小アニメーション /// </summary> private void mShrinkAnimation () { this.GameStartTextRt.DOScale (Vector3.one * 0.5f, 0.5f) .OnComplete (() => { // テキストの縮小アニメーション this.EnlarAnimation (); }); } }
これを作成したら「GameStateManager.cs」に StartStateManager の処理を追加しましょう
3. GameStateManager.cs に StartStateManagerの処理を追加する
まずは GameStateManager.cs にスタートステート時の処理を追加しましょう。
先ほど作った「StartArea」に「StartStateManager.cs」をアタッチします。
public class GameSceneManager : MonoBehaviour { // 一致したカードIDリスト private List<int> mContainCardIdList = new List<int> (); // カード生成マネージャクラス public CardCreateManager CardCreate; // 時間管理クラス public TimerManager timerManager; // スタートステートクラス public StartStateManager startStateManager;
private void mSetStartState () { // テキストの拡大縮小アニメーション this.startStateManager.EnlarAnimation (); } /// <summary> /// Readyステートに遷移する /// </summary> public void OnGameStart () { // ゲームステートを初期化 this.mEGameState = EGameState.READY; // ゲームのステート管理 this.mSetGameState (); }
宣言した変数にStartAreaをアタッチしましょう。
次に、Start()関数の初期化処理も変更しましょう
void Start () { // ゲームステートを初期化 this.mEGameState = EGameState.START; // スタートエリアを表示 this.startStateManager.gameObject.SetActive (false); // ゲームのステート管理 this.mSetGameState (); }
そして、先ほど作った「GameStateText」のButtonのOnClick に「OnGameState」を追加しましょう。
ここまで出来たら、一度UnityのGameを起動してみましょう。
上記のような動作になったら成功です!
スタート画面は出来たので、次は「リザルト」画面を作成しましょう!
3. リザルトステート画面を作成しよう!
スタート画面と同様にリザルト画面も作成しましょう。
センスとは問わないで
そして、ResultStateManager を作成し変数などをアタッチします。
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class ResultStateManager : MonoBehaviour { // 時間を表示するテキスト public Text TimerText; /// <summary> /// ゲームで経過した時間を表示する /// </summary> public void SetTimerText (int timer) { this.TimerText.text = timer.ToString (); } }
RsultStateManagerの準備が整ったら、GameStateManagerにリザルト処理を追加しましょう。
/// <summary> /// ゲームステートで処理を変更する /// </summary> private void mSetGameState () { switch (this.mEGameState) { // スタート画面 case EGameState.START: // スタートエリアを表示 this.startStateManager.gameObject.SetActive (true); // ゲームスタートの開始 this.mSetStartState (); break; // ゲーム準備期間 case EGameState.READY: // ゲームの準備ステートを開始する this.mSetGameReady (); break; // ゲーム中 case EGameState.GAME: break; // 結果画面 case EGameState.RESULT: this.resultStateManager.gameObject.SetActive (true); this.mSetResultState (); break; } } /// <summary> /// リザルトステートの設定処理 /// </summary> private void mSetResultState () { this.resultStateManager.SetTimerText ((int) this.mElapsedTime); } /// <summary> /// スタート画面に遷移する /// </summary> public void OnBackStartState () { // ResultAreaを非表示にする this.resultStateManager.gameObject.SetActive (false); // ゲームステートをStartに変更 this.mEGameState = EGameState.START; // ゲームのステート管理 this.mSetGameState (); }
そして、Update部分で「全てのカードを取得したらリザルトステートに遷移する」処理を実装します
void Update () { // GameState が GAME状態なら if (this.mEGameState == EGameState.GAME) { this.mElapsedTime += Time.deltaTime; this.timerManager.SetText ((int) this.mElapsedTime); // 選択したカードが2枚以上になったら if (GameStateController.Instance.SelectedCardIdList.Count >= 2) { // 最初に選択したCardIDを取得する int selectedId = GameStateController.Instance.SelectedCardIdList[0]; // 2枚目にあったカードと一緒だったら if (selectedId == GameStateController.Instance.SelectedCardIdList[1]) { Debug.Log ($"Contains! {selectedId}"); // 一致したカードIDを保存する this.mContainCardIdList.Add (selectedId); } // カードの表示切り替えを行う this.CardCreate.HideCardList (this.mContainCardIdList); // 選択したカードリストを初期化する GameStateController.Instance.SelectedCardIdList.Clear (); } // 配置した全種類のカードを獲得したら if (this.mContainCardIdList.Count >= 6) { // ゲームをリザルトステートに遷移する this.mEGameState = EGameState.RESULT; this.mSetGameState (); } }
今回は 6 と直打ちしていますが、配布するカードの枚数を予め設定しておくのをオススメします!
また、ゲームを繰り返す処理を実装するため、CardCreateManager.csのCreateCard関数の最初に CardCreateParent の子オブジェクトを
削除する処理を実装しましょう。
public void CreateCard () {
foreach (Transform child in this.CardCreateParent) {
Destroy (child.gameObject);
}
最後に「OnBackStartState」をResutAreaに作成したButtonにアタッチしましょう。
これでリザルト画面の準備が整いました。
UnityのGameを起動してみましょう。
上記のように画面が切り替われば成功です!
【Unity ゲーム制作】神経衰弱ゲームを作ろう!〜その10 (Enumを使ってゲームの進行を管理しよう!)
前回は、タイマー機能を追加しました。
今回は「Enum」を使用してゲームの進行 ( ステート ) を管理したいと思います、。
1. カードの種類を増やそう
まずは神経衰弱で使うカードを増やしましょう。
現時点では12枚のカードが表示されていると思うので、新たに「3つ」の新規カードを導入します。
今回は「いらすとや」さんにて、フリー画像を拝借しました。
その後、「CardCreateManager.cs」を以下のように変更します。
// Resources/Imageフォルダ内にある画像を取得する imgList.Add (Resources.Load<Sprite> ("Image/card_image_000")); imgList.Add (Resources.Load<Sprite> ("Image/card_image_001")); imgList.Add (Resources.Load<Sprite> ("Image/card_image_002")); imgList.Add (Resources.Load<Sprite> ("Image/card_image_003")); imgList.Add (Resources.Load<Sprite> ("Image/card_image_004")); imgList.Add (Resources.Load<Sprite> ("Image/card_image_005")); // forを回す回数を取得する int loopCnt = imgList.Count; for (int i = 0; i < loopCnt; i++) { // カード情報を生成する CardData cardata = new CardData (i, imgList[i]); cardDataList.Add (cardata); } this.mIndex = 0; this.mHelgthIdx = 0; this.mWidthIdx = 0; // 生成したカードリスト2つ分のリストを生成する List<CardData> SumCardDataList = new List<CardData> (); SumCardDataList.AddRange (cardDataList);
読み込むカードの種類を増やしました。
では、次はゲーム進行部分を作成したいと思います。
2 . Enum で 「EGameState」を作成する
新規で「EGameState.cs」を作成します
/// <summary> /// ゲームの進行ステート /// </summary> public enum EGameState { START = 0, READY = 1, GAME = 2, RESULT = 3 }
今回は enum を使用しました。
enum は「列挙型」と呼ばれるもので、状態管理などで使用されます。
それでは、作成した EGameState をゲームロジックに組み合わせましょう。
3 . GameSceneManager .cs でゲームステート関数を作成する
まずは「GameSceneManager.cs 」にEGameState の変数を追加します。
public class GameSceneManager : MonoBehaviour { // 一致したカードIDリスト private List<int> mContainCardIdList = new List<int> (); // カード生成マネージャクラス public CardCreateManager CardCreate; // 時間管理クラス public TimerManager timerManager; // 経過時間 private float mElapsedTime; // ゲームステート管理 private EGameState mEGameState;
そして、Start関数で初期化しましょう。
void Start () { // 一致したカードIDリストを初期化 this.mContainCardIdList.Clear (); // カードリストを生成する this.CardCreate.CreateCard (); // 時間を初期化 this.mElapsedTime = 0f; // ゲームステートを初期化 this.mEGameState = EGameState.START; }
そして、「ゲームのステートによって処理を振り分ける関数」を作成します。
/// <summary> /// ゲームステートで処理を変更する /// </summary> private void mSetGameState () { switch (this.mEGameState) { // スタート画面 case EGameState.START: break; // ゲーム準備期間 case EGameState.READY: break; // ゲーム中 case EGameState.GAME: break; // 結果画面 case EGameState.RESULT: break; } }
内容はEnum ( 列挙型 ) のEGameState をSwitch文で切り分けます。
先ほどのStart関数にmSetGameState を宣言しましょう。
void Start () { // 一致したカードIDリストを初期化 this.mContainCardIdList.Clear (); // カードリストを生成する this.CardCreate.CreateCard (); // 時間を初期化 this.mElapsedTime = 0f; // ゲームステートを初期化 this.mEGameState = EGameState.START; // ゲームのステート管理 this.mSetGameState (); }
それでは、mSetGameState 関数の Switch文に処理を追加していきましょう。
4. ゲームの準備ステート用の関数を作成し、mSetGameStateに追加する
今回は、「ゲームの準備ステート」から作成していきます。
まずは 「mSetGameReady」関数を作成し、今までStart 関数で初期化していた部分をこの関数の中に移しましょう。
/// <summary> /// ゲームの準備ステートを開始する /// </summary> private void mSetGameReady () { // 一致したカードIDリストを初期化 this.mContainCardIdList.Clear (); // カードリストを生成する this.CardCreate.CreateCard (); // 時間を初期化 this.mElapsedTime = 0f; }
そしてこの関数をmSetGameState の EGameState.Ready のswitch文に宣言します。
/// <summary> /// ゲームステートで処理を変更する /// </summary> private void mSetGameState () { switch (this.mEGameState) { // スタート画面 case EGameState.START: break; // ゲーム準備期間 case EGameState.READY: // ゲームの準備ステートを開始する this.mSetGameReady (); break; // ゲーム中 case EGameState.GAME: break; // 結果画面 case EGameState.RESULT: break; } }
これを行うことで、今までStart関数で行なっていた初期化処理を、EGameState が READY になった時に行えるようになりました。
次にStart関数で初期化していたmEGameState の初期化を「Start」から「Ready」に変更しましょう。
void Start () { // ゲームステートを初期化 this.mEGameState = EGameState.READY; // ゲームのステート管理 this.mSetGameState (); }
Start関数がサッパリしたと思います。
次は、EGameStateが 「Game」の時の処理を実装しましょう
5. UpdateにEGameState が GAME の時のみ、処理を実行するように変更しよう!
では、Update 関数に 「ゲームステートが GAME の状態の時のみ、カードの判別処理を実装する」処理を実装しましょう。
void Update () { // GameState が GAME状態なら if (this.mEGameState == EGameState.GAME) { this.mElapsedTime += Time.deltaTime; this.timerManager.SetText ((int) this.mElapsedTime); // 選択したカードが2枚以上になったら if (GameStateController.Instance.SelectedCardIdList.Count >= 2) { // 最初に選択したCardIDを取得する int selectedId = GameStateController.Instance.SelectedCardIdList[0]; // 2枚目にあったカードと一緒だったら if (selectedId == GameStateController.Instance.SelectedCardIdList[1]) { Debug.Log ($"Contains! {selectedId}"); // 一致したカードIDを保存する this.mContainCardIdList.Add (selectedId); } // カードの表示切り替えを行う this.CardCreate.HideCardList (this.mContainCardIdList); // 選択したカードリストを初期化する GameStateController.Instance.SelectedCardIdList.Clear (); } } }
これで、GAMEステートの時 ( ゲーム中 )のみ、タイマーが加算され、カード処理が実装されるようになりました。
最後にREADYの処理が完了したら、GameStateをGAME に変更する処理を実装しましょう。
まずはCardCreateManager.csにカードのアニメーションが終了した時に呼ぶコールバック関数を作成しましょう。
public class CardCreateManager : MonoBehaviour { // 生成するCardオブジェクト public Card CardPrefab; // 「カード」を生成する親オブジェクト public RectTransform CardCreateParent; // 生成したカードオブジェクトを保存する public List<Card> CardList = new List<Card> (); // カード情報の順位をランダムに変更したリスト private List<CardData> mRandomCardDataList = new List<CardData> (); // GridLayoutGroup public GridLayoutGroup GridLayout; // カードの生成アニメーションが終わった時 public Action OnCardAnimeComp;
そして、「mSetDealCardAnime」関数のアニメーションが終了した処理部分に、コールバックが呼ばれる処理を追加しましょう。
// DOAnchorPosでアニメーションを行う card.mRt.DOAnchorPos (new Vector2 (posX, posY), this.DEAL_CAED_TIME) // アニメーションが終了したら .OnComplete (() => { // 生成したカードオブジェクトを保存する this.CardList.Add (card); // 生成するカードデータリストのインデックスを更新 this.mIndex++; this.mWidthIdx++; // 生成インデックスがリストの最大値を迎えたら if (this.mIndex >= this.mRandomCardDataList.Count) { // GridLayoutを有効にし、生成処理を終了する this.GridLayout.enabled = true; // アニメーション終了時の関数を宣言する if (this.OnCardAnimeComp != null) { this.OnCardAnimeComp (); } }
そして、Ready の準備関数に「カード配布アニメーションが終了した時のコールバック処理」を作成しましょう。
private void mSetGameReady () { // カード配布アニメーションが終了した後のコールバック処理を実装する this.CardCreate.OnCardAnimeComp = null; this.CardCreate.OnCardAnimeComp = () => { // ゲームステートをGAME状態に変更する this.mEGameState = EGameState.GAME; this.mSetGameState (); }; // 一致したカードIDリストを初期化 this.mContainCardIdList.Clear (); // カードリストを生成する this.CardCreate.CreateCard (); // 時間を初期化 this.mElapsedTime = 0f; }
ここまで出来たら UnityでGameを実行してみましょう。
カードの配布アニメーションが終了してからタイマーが起動したら成功です!
【Unity ゲーム制作】神経衰弱ゲームを作ろう!〜その9 ( タイマーを作成しよう!)
前回はカード配置のアニメーションを作成しました。
ゲームとしての見栄えも良くなったので、次はゲーム内部のレベルアップを行なっていきましょう!
まずは、神経衰弱をやる上で重要な「タイマー」機能を実装します!
1 . CanvasにTimerAreaを作成する
まずはHierarchy のCanvasにTimer用のAreaを作成しましょう。
2 . TimerAreaにTextを作成する
次は先ほど作ったTimerAreaにTextオブジェクトを作成しましょう。
「UI → Text」で作成します
コンポーネントはこのような状態です
次はタイマー用のスクリプトを作成します。
3. 「TimerManager」を作成しよう!
TimerManager.csを作成し、以下のコードを記述してください。
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class TimerManager : MonoBehaviour { // 時間表示 public Text TimerText; /// <summary> /// タイマーのテキストの設定 /// </summary> public void SetText (int time) { this.TimerText.text = "Time : " + time; } }
こちらは先ほど実装したTextをアタッチして使用します。
では、実際にゲーム中にタイマーが起動するようにGameSceneManager にタイマー機能を追加しましょう!。
4 . 「GameSceneManager.cs」にタイマー機能を実装する
using System; using System.Collections; using System.Collections.Generic; using UnityEngine; public class GameSceneManager : MonoBehaviour { // 一致したカードIDリスト private List<int> mContainCardIdList = new List<int> (); // カード生成マネージャクラス public CardCreateManager CardCreate; // 時間管理クラス public TimerManager timerManager; // 経過時間 private float mElapsedTime; void Start () { // 一致したカードIDリストを初期化 this.mContainCardIdList.Clear (); // カードリストを生成する this.CardCreate.CreateCard (); // 時間を初期化 this.mElapsedTime = 0f; } void Update () { this.mElapsedTime += Time.deltaTime; this.timerManager.SetText ((int) this.mElapsedTime); // 選択したカードが2枚以上になったら if (GameStateController.Instance.SelectedCardIdList.Count >= 2) { // 最初に選択したCardIDを取得する int selectedId = GameStateController.Instance.SelectedCardIdList[0]; // 2枚目にあったカードと一緒だったら if (selectedId == GameStateController.Instance.SelectedCardIdList[1]) { Debug.Log ($"Contains! {selectedId}"); // 一致したカードIDを保存する this.mContainCardIdList.Add (selectedId); } // カードの表示切り替えを行う this.CardCreate.HideCardList (this.mContainCardIdList); // 選択したカードリストを初期化する GameStateController.Instance.SelectedCardIdList.Clear (); } } }
まずはGameSceneManager のプロパティ に TimerManagerを追加します。
その後 Update() でTime.deltatime を使用して時間を加算していきます。
int 側にキャストしているのは、「1分」ごとの表記を表しているためです。
ここまで出来たら、実際にGameを起動してみましょう。
タイマーを作成することが出来ました!
【Unity ゲーム制作】神経衰弱ゲームを作ろう!〜その8 (カードを配るアニメーション実装)
今回は動画のような「カードを配るアニメーション処理」を作成したいと思います!
CardField 及び Card オブジェクトの調整しよう!
まずは「CardField」の設定を変更しましょう。
・Grid Layout Group のCellSize を X 100 : Y 200 に変更
・Grid Layout Group のChild Alignment を Upper Left に変更
・ContentSizeFitter の Horizontal Fit を 「Unconstrained」に変更
カードの生成枚数を増やすため、1枚のサイズを小さくしました。
次は Card.オブジェクトの設定を変更します
Cardオブジェクトの pivot の Y値を 1 に変更します。
CardCreateManager に GridLayoutGroup のプロパティを追加する
public class CardCreateManager : MonoBehaviour { // 生成するCardオブジェクト public Card CardPrefab; // 「カード」を生成する親オブジェクト public RectTransform CardCreateParent; // 生成したカードオブジェクトを保存する public List<Card> CardList = new List<Card> (); // カード情報の順位をランダムに変更したリスト private List<CardData> mRandomCardDataList = new List<CardData> (); // GridLayoutGroup public GridLayoutGroup GridLayout;
GridLayout を設定したら、CardFieldのGridLayoutGroupをアタッチしましょう!。
こちらの準備ができたら、ソースコードの変更に入ります。
Card.csのmRt変数をpublic に変更する
public class Card : MonoBehaviour { // カードのID public int Id; // 表示するカードの画像 public Image CardImage; // 透過処理用 public CanvasGroup CanGroup; // 選択されているか判定 private bool mIsSelected = false; public bool IsSelected => this.mIsSelected; // カード情報 private CardData mData; // 座標情報 public RectTransform mRt;
Cardオブジェクトの座標変更の準備が整いました。
CardCreateManagerに処理を追加
using System; using System.Collections; using System.Collections.Generic; using System.Linq; using DG.Tweening; using UnityEngine; using UnityEngine.UI; public class CardCreateManager : MonoBehaviour { // 生成するCardオブジェクト public Card CardPrefab; // 「カード」を生成する親オブジェクト public RectTransform CardCreateParent; // 生成したカードオブジェクトを保存する public List<Card> CardList = new List<Card> (); // カード情報の順位をランダムに変更したリスト private List<CardData> mRandomCardDataList = new List<CardData> (); // GridLayoutGroup public GridLayoutGroup GridLayout; // カード配列のインデックス private int mIndex; // カードを生成する時の高さインデックス private int mHelgthIdx; // カードを生成する時の幅インデックス private int mWidthIdx; // カードの生成アニメーションのアニメーション時間 private readonly float DEAL_CAED_TIME = 1f;
・DOTweenを使用するため、 using DG.Tweening を追加
・カードの生成処理を実装するためのインデックス変数を追加
変数制限の次はいよいよ内部の処理に進みます。
this.mIndex = 0; this.mHelgthIdx = 0; this.mWidthIdx = 0; // 生成したカードリスト2つ分のリストを生成する List<CardData> SumCardDataList = new List<CardData> (); SumCardDataList.AddRange (cardDataList); SumCardDataList.AddRange (cardDataList); // ランダムリストの初期化 this.mRandomCardDataList.Clear (); // リストの中身をランダムに再配置する this.mRandomCardDataList = SumCardDataList.OrderBy (a => Guid.NewGuid ()).ToList (); this.mRandomCardDataList.AddRange (SumCardDataList.OrderBy (a => Guid.NewGuid ()).ToList ()); // GridLayoutを無効 this.GridLayout.enabled = false; // カードを配るアニメーション処理 this.mSetDealCardAnime ();
前回と違う部分は「mRandomCardDataList」をメンバー変数にしたことです。
また、カードを生成するタイミングで GridLayoutGorup の変数を無効化しています。
アニメーション処理をやる場合、GridLayoutGroupが発動中だと邪魔になるため、生成が終わるまでは無効化しておきます。
それでは、カードの生成アニメーション処理を見ていきましょう。
/// <summary> /// カードを配るアニメーション処理 /// </summary> private void mSetDealCardAnime () { var _cardData = this.mRandomCardDataList[this.mIndex]; // Instantiate で Cardオブジェクトを生成 Card card = Instantiate<Card> (this.CardPrefab, this.CardCreateParent); // データを設定する card.Set (_cardData); // カードの初期値を設定 (画面外にする) card.mRt.anchoredPosition = new Vector2 (1900, 0f); // サイズをGridLayoutのCellSizeに設定 card.mRt.sizeDelta = this.GridLayout.cellSize; // カードの移動先を設定 float posX = (this.GridLayout.cellSize.x * this.mWidthIdx) + (this.GridLayout.spacing.x * (this.mWidthIdx + 1)); float posY = ((this.GridLayout.cellSize.y * this.mHelgthIdx) + (this.GridLayout.spacing.y * this.mHelgthIdx)) * -1f; // DOAnchorPosでアニメーションを行う card.mRt.DOAnchorPos (new Vector2 (posX, posY), this.DEAL_CAED_TIME) // アニメーションが終了したら .OnComplete (() => { // 生成したカードオブジェクトを保存する this.CardList.Add (card); // 生成するカードデータリストのインデックスを更新 this.mIndex++; this.mWidthIdx++; // 生成インデックスがリストの最大値を迎えたら if (this.mIndex >= this.mRandomCardDataList.Count) { // GridLayoutを有効にし、生成処理を終了する this.GridLayout.enabled = true; } else { // GridLayoutの折り返し地点に来たら if (this.mIndex % this.GridLayout.constraintCount == 0) { // 高さの生成箇所を更新 this.mHelgthIdx++; this.mWidthIdx = 0; } // アニメーション処理を再帰処理する this.mSetDealCardAnime (); } }); }
まずは生成したCard.オブジェクトの座標を好きな位置に設定してください。
今回は画面外からカードが配られるイメージなので、画面外に設定しています。
前回と違うのは foreach ではなく、 インデックス変数から手動でカードの生成処理を行なっています。
ここまで処理が完成したらGameを起動してみましょう!
最初の動画の通りの動きになったら成功です!
【Adjust】リアルタイムコールバックのカスタムパラメータについて
久々のAdjust記事です。
今や多くのアプリで使用されているAdjustですが、公式サイトの情報が古く、セットアップに難儀している人も多いかなと思います。
その中でも一番面倒だったのだ、イベントの
リアルタイムコールバックでの「ステークホルダー」以外の情報をコールバックURLに載せて送信するやり方でした。
Adjustはコールバック用のステークホルダーを定義してあります。
例えば「Country」なら国名 「device」なら端末がスマホかタブレットか識別できます。
このように多様なステークホルダーがあるため、データ収集やKPIでは本当に助かります。
しかし、ステークホルダーにないユニーク要素を取得する場合はどうしてらいいのでしょうか?
例えば 「UserId」をコールバックで受け取りたいとします。
その場合、従来のステークホルダーの記述方法「&userid={userid}」と同じ書き方では情報の送信は上手くいきません。
コールバック用のDBを設定していた場合は{Userid}と取得されてしまうでしょう。
この場合の対処法はこのように記述すればOKです!
&UserId=
{}などの記述を省いてください。
この場合、コールバックの送信がうまくいけば、Useridの情報がしっかりと取得できます!
この部分は公式サイトを見ても中々見つからなかったのでとても苦労しました。
【Unity】 DOTSについて調べてみた
先日のUnite2019 にて DOTS (Data-Oriented Tech Stack) の講演が実に刺激的だったので調べてみました。
DOTSとは
DOTS「Data-Oriented Tech Stack」とはUnityが開発を進めている機能(大まかにいうと概念)です。
近年のPCやスマートフォンはCPUやGPUの進化は目覚ましく、コア数や性能も数年前より段違いで向上しています。
その影響を受けて最新のコンピューターゲームではより「ハイエンド」で「ハイパフォーマンス」「ハイボリューム」なゲームが求められるようになり、
それを実現するためにゲームエンジン側でもマルチスレッド(複数の処理を並行して進めること)化がふ必要不可欠な状態になっています。
そこでUnityが考え出したのが「DOTS」です!。
DOTSの仕組み
DOTSは主に「3つの機能」から成り立っています。
- Job System ......... (マルチコアで並列処理を行うシステム)
- Burst Compiler .........(CPU演算に最適化したコンパイルを作成する)
- ECS ...........(
MonoBehaviorをぶっ壊す!超高速キャッシュ機能 )
少し古いですが「Unityの3本の矢」と言われていますね。
この3本の矢 ( 3種の神器 )を使いこなすことで例えば「1万体もの3Dが暴れまくるハイエンドゲーム」をモバイルゲームで作ることができるようになるわけですよ!!! (すごい!!!)
こんだけ凄い機能ですが、現場に普及するのはまだ時間がかかりそうな気がします。
その理由を簡単に説明すると、
・ECS が不安定 かつ、既存の概念を根本的に覆すシステム
・Job Systemでは値型 (ValueType)しか保管できない
etc....
などなどありますが、ザックリ言うと「未だ発展途上でかつ難しい!」 これに尽きると思います。
よく新しい技術が出てきたらすぐにそれを使おうとする人がいますが、趣味ならともかく業務でそれはオススメしません。(手痛い目にあったので。。。)
まずはその技術をしっかりと勉強し、1つ1つの動作を検証した上で導入すべきだと思っています。
DOTSは概念の理解や導入は大変難しいと思いますが、それを乗り越え使いこなした暁には素晴らしい結果をもたらしてくれると信じています。
最後に筆者がDOTSを試して見た感想ですが
・ECSを扱うには基盤作成が最重要
・C,C++を勉強していない人には ポインタ処理が難しい
ザックリ言うとこのような感じでした。
ECS関連はちょくちょくと記事にしていきたいですね。
【Unity ゲーム制作】神経衰弱ゲームを作ろう!〜その9 ~( DoTweenで綺麗なアニメーション)
前っせっwっqZzz回のカードの表示切り替えの処理を実装しましたが、
動きが雑に見えた人も多いと思います。
そこで今回は、切り替え処理を綺麗にアニメーションするように変更していきましょう。
そのためには「Dotween」コンポーネントを新たに導入しようと思います。
Dotween は Unityに搭載されているコンポーネントではないため、AssetStoreから導入していきましょう!
AssetStoreからDotweenを導入しよう!
まずはUnityからAssetStoreを開きます。
画像を参照に開きましょう。
AssetStoreが開けたら検索をします!
Dtoweenが表示されたら、右側にある無料版を選択してUpdateボタンを押し、importしましょう。
Unityでこの画面が出たら、右下のimportを押します。
フォルダにDotweenが表示されたら導入成功です!
では、実際にDotweenを動かしてみましょう!
DotweenをCard.csに組みこもう!
Dotweenを使用するには、以下のusing を記述してください
using DG.Tweening;
試しに、カードを選択したら回転する 処理を実装してみましょう。
まずはCard.csにRectTransform 型の変数を実装します。
// 座標情報
private RectTransform mRt;
そうしたら、その変数にカードオブジェクトの座標情報を代入しましょう。
// カードの設定 public void Set (CardData data) { // カード情報を設定 this.mData = data; // IDを設定する this.Id = data.Id; // 表示する画像を設定する // 初回は全て裏面表示とする this.CardImage.sprite = Resources.Load<Sprite> ("Image/card_back"); // 選択判定フラグを初期化する this.mIsSelected = false; // アルファ値を1に設定 this.CanGroup.alpha = 1; // 座標情報を取得しておく this.mRt = this.GetComponent<RectTransform> (); }
mRt変数に、このCardオブジェクトの座標情報が導入されました。
次は、onClickにて、カードが回転する処理を記述します。
public void OnClick () { // カードが表面になっていた場合は無効 if (this.mIsSelected) { return; } Debug.Log ("OnClick"); // Dotweenで回転処理を行う this.mRt.DORotate (new Vector3 (0f, 90f, 0f), 0.2f) // 回転が完了したら .OnComplete (() => { // 選択判定フラグを有効にする this.mIsSelected = true; // カードを表面にする this.CardImage.sprite = this.mData.ImgSprite; // Y座標を元に戻す this.onReturnRotate (); }); } /// <summary> /// カードの回転軸を元に戻す /// </summary> private void onReturnRotate () { this.mRt.DORotate (new Vector3 (0f, 0f, 0f), 0.2f) // 回転が終わったら .OnComplete (() => { // 選択したCardIdを保存しよう! GameStateController.Instance.SelectedCardIdList.Add (this.mData.Id); }); }
this.mRt.DORotate () では、「回転する回転」と「回転する時間」を設定しています。
この処理では カードオブジェクトのY軸を「0.2秒」かけて 90度 に変更する 内容になっています。
下の OnComplete() は、上のDORotate()の処理が終了した後に呼ばれます。
回転が終わった後にカード画像を「表面」に変更して、「onReturnRotate」関数を読んでおります。
onReturnRotate関数は 先ほど 90度にしたカードオブジェクトの角度を元に戻す関数です。
選択したカードが表面に戻ったら、「SelectedCardIdList」に選択したカードIDを保管しています。
ここまで出来たらGameを起動して、実際にカードを選択しましょう。
どうでしょうか! カードをくるっと回転したと思います。
次はカードを背面する時にも同様の処理を実装していきましょう。
今回の回転処理は、
1.カードを90度に回転した後、カード画像を変更する
2.変更したあと、カードの角度を元に戻す
の2つになっています。
そこで、「カードを90度に回転する」関数と「カードの角度を元に戻す」関数の2つを作成しましょう。
そうすれば、「カードを選択した時」「カードを裏面に戻す時」でも、同じ関数を呼ぶだけで処理を実行できるようになります。
今回はこの関数の共通化を実装していきましょう!
まずは using System を追加してください。
using System;
/// <summary> /// カードを90度に回転する /// </summary> private void onRotate (Action onComp) { // 90度回転する this.mRt.DORotate (new Vector3 (0f, 90f, 0f), 0.2f) // 回転が終了したら .OnComplete (() => { if (onComp != null) { onComp (); } }); } /// <summary> /// カードの回転軸を元に戻す /// </summary> private void onReturnRotate (Action onComp) { this.mRt.DORotate (new Vector3 (0f, 0f, 0f), 0.2f) // 回転が終わったら .OnComplete (() => { if (onComp != null) { onComp (); } }); }
「カードを90度に回転する」関数と「カードの角度を元に戻す」関数を作成しました。
ポイントは それぞれの関数の引数にある「Action onComp」という部分です。
今回は「Action」側の コールバック関数を実装しました。
コールバックとは、「何かの処理が終わった後に呼ばれる処理を予め設定しておく」関数のことです。
今回の例だと、「回転する処理が終わった」後に設定しておいたコールバックを実行する!
内容になっております。
では、共通化した部分をOnClick関数に導入し、コールバックがどのように設定されているのかを確かめてみましょう!
public void OnClick () { // カードが表面になっていた場合は無効 if (this.mIsSelected) { return; } Debug.Log ("OnClick"); // 回転処理を行う this.onRotate (() => { // 選択判定フラグを有効にする this.mIsSelected = true; // カードを表面にする this.CardImage.sprite = this.mData.ImgSprite; // Y座標を元に戻す this.onReturnRotate (() => { // 選択したCardIdを保存しよう! GameStateController.Instance.SelectedCardIdList.Add (this.mData.Id); }); }); }
ここでポイントなのが、this.onRotate (() => {} のように、関数の引数のところでなにやら怪しげな処理を行なっているところです。
これは「無名関数」と呼ばれるもので その名の通り、「名前を付けずに定義された関数」になります。
主にUnityでは「ラムダ式」による記述が主流とおり、
() => {
//ここに処理を書く
}
上記のように{}で囲った部分に記述した処理が、無名関数が呼ばれた時に実行されます。
カード背面表示にする時は以下のように記述しましょう。
/// <summary> /// カードを背面表記にする /// </summary> public void SetHide () { // 90度回転する this.onRotate (() => { // 選択判定フラグを初期化する this.mIsSelected = false; // カードを背面表示にする this.CardImage.sprite = Resources.Load<Sprite> ("Image/card_back"); // 角度を元にもどす this.onReturnRotate (() => { Debug.Log ("onhide"); }); }); }
それでは、Gameを起動して動作をみてみましょう。
カードが回転するようになりましたね。
ですが、選択したカード以外も回転してしまう問題があります。
そこでCard.csに 選択済みのカードか否か外部からでも判別できる変数を追加しましょう。
選択済みのカードだけ回転するようにしよう!
public class Card : MonoBehaviour { // カードのID public int Id; // 表示するカードの画像 public Image CardImage; // 透過処理用 public CanvasGroup CanGroup; // 選択されているか判定 private bool mIsSelected = false; public bool IsSelected => this.mIsSelected;
Card.csに IsSelected 変数を実装しました。
ここでは 「IsSelected => this.mIsSelected」と記述しており、
これによって mIsSelected の値が IsSelectedにいつでも反映される状態になっています。
逆に、IsSelected の値を変更しようとしても、mIsSelectedには全く反映されません。
これで外部からでも選択済みカードか判別できるようになったため、CardCrateManagerにてその判別処理を実装します。
// <summary> /// 取得していないカードを背面にする /// </summary> public void HideCardList (List<int> containCardIdList) { foreach (var _card in this.CardList) { // 既に獲得したカードIDの場合、非表示にする if (containCardIdList.Contains (_card.Id)) { // カードを非表示にする _card.SetInvisible (); } // カードが表麺 && 獲得していないカードは裏面表示にする else if (_card.IsSelected) { // カードを裏面表示にする _card.SetHide (); } } }
判別処理が実装出来ましたね。
では、Gameを起動してみましょう!。
綺麗にアニメーションするようになりましたね!。
ここまでで、神経衰弱の土台は出来上がりました!
次回からはブラッシュアップ版を作成していきたいと思います!
【Unity ゲーム制作】神経衰弱ゲームを作ろう!〜その8 ( カードの裏返し処理を作ろう!)
前回は同じカードを選択したときの処理を実装しました!
今回は「同じカードを選択したらそのカードを非表示にする」処理と
異なったカードを選択したら、選択したカードを背面表示に戻す処理の両方を実装していきましょう!
「Card.prefab」に「CanvasGroup」を導入しよう!
まずは、「カードを非表示」にするために、とあるコンポーネントを導入します。
CanvasGroupについては、公式サイトを参照してください
UnityEngine.CanvasGroup - Unity スクリプトリファレンス
簡単に説明すると、Canvas内部で使用するコンポーネントであり、導入したオブジェクトのアルファ値などを変更したいときに使用します!。
では、早速導入していきましょう。
Card.prefabを開き、Inspector から AddComponentを選択 → CanvasGroup を検索して導入してください。
するとこのような画面が出ると思います。
このAlphaによってオブジェクトのアルファ値を変更することができるようになります。
コンポーネントが導入できたら、 Card.csに CanvasGroup の変数を作成し、そこにアタッチしましょう!
public class Card : MonoBehaviour { // カードのID public int Id; // 表示するカードの画像 public Image CardImage; // 透過処理用 public CanvasGroup CanGroup; // 選択されているか判定 private bool mIsSelected = false; // カード情報 private CardData mData;
アタッチできたら、Card.csに処理を追加していきます。
public void Set (CardData data) { // カード情報を設定 this.mData = data; // IDを設定する this.Id = data.Id; // 表示する画像を設定する // 初回は全て裏面表示とする this.CardImage.sprite = Resources.Load<Sprite> ("Image/card_back"); // 選択判定フラグを初期化する this.mIsSelected = false; // アルファ値を1に設定 this.CanGroup.alpha = 1; }
OnClick()で選択した場合、Cardオブジェクトは表示されている状態になります。
次に、「選択したカードが異なったカード同士」だった場合の処理と
「同じだった場合」の処理を記述します。
それぞれの役割を整理すると
- 「選択したカードは同じではない」 : 「カードを背面表記に戻す」
- 「選択したカードが同じ」 : 「カードを非表示にする」
それぞれの役割を実行する関数を作成しましょう!
/// <summary> /// カードを背面表記にする /// </summary> public void SetHide () { // 選択判定フラグを初期化する this.mIsSelected = false; // カードを背面表示にする this.CardImage.sprite = Resources.Load<Sprite> ("Image/card_back"); } /// <summary> /// カードを非表示にする /// </summary> public void SetInvisible () { // 選択済設定にする this.mIsSelected = true; // アルファ値を0に設定 (非表示) this.CanGroup.alpha = 0; }
SetHide 関数では、「カードを背面状態に戻し、選択可能状態にする」処理を記述しています。
SetInvisible 関数では、「選択済み状態にし、カードのアルファ値を 0 (非表示)にする」処理を行います。
先ほど導入した「CanvasGroup」のアルファ値を変更することで、カードを見えない状態にしております。
次に、CardCreateManager.csの内容も修正していきましょう。
CardCreateManager.csの処理を変更しよう!
まずはCardCreateManager.csに「生成したCardオブジェクトを保管する変数」を作成しましょう!
public class CardCreateManager : MonoBehaviour { // 生成するCardオブジェクト public Card CardPrefab; // 「カード」を生成する親オブジェクト public RectTransform CardCreateParent; // 生成したカードオブジェクトを保存する public List<Card> CardList = new List<Card> ();
そして、生成したCardオブジェクトを CardList に保管します。
// リストの中身をランダムに再配置する List<CardData> randomCardDataList = SumCardDataList.OrderBy (a => Guid.NewGuid ()).ToList (); // カードオブジェクトを生成する foreach (var _cardData in randomCardDataList) { // Instantiate で Cardオブジェクトを生成 Card card = Instantiate<Card> (this.CardPrefab, this.CardCreateParent); // データを設定する card.Set (_cardData); // 生成しかカードオブジェクトを保存する this.CardList.Add (card); }
これで生成したCardオブジェクトを変更することができるようになりました。
現在は、Start() にて一回だけカードを生成する処理が記述されておりますが、新たに関数を追加していきましょう。
追加する関数は
- カードを生成する専用の関数
- カードを背面、非表示にする関数
を作成していきます。
/// <summary> /// カードを生成する /// </summary> public void CreateCard () { } /// <summary> /// 取得していないカードを背面にする /// </summary> public void HideCardList () { }
CreateCard()関数には、現在 Start()に記述している生成処理をそのままコピペしてください。
HideCardList() 関数では、「生成したCardオブジェクト」に処理を実行していきます。
/// <summary> /// 取得していないカードを背面にする /// </summary> public void HideCardList (List<int> containCardIdList) { foreach (var _card in this.CardList) { // 既に獲得したカードIDの場合、非表示にする if (containCardIdList.Contains (_card.Id)) { // カードを非表示にする _card.SetInvisible (); } // 獲得していないカードは裏面表示にする else { // カードを裏面表示にする _card.SetHide (); } } }
HideCardListでは、生成したカードをforeachで呼び出し、「取得したカードと同じID」だった場合は、そのカードを非表示に、
違う場合はカードを背面表示にする 処理を記述しています。
それでは、その処理をGameSceneManagerに記述しましょう。
public class GameSceneManager : MonoBehaviour { // 一致したカードIDリスト private List<int> mContainCardIdList = new List<int> (); // カード生成マネージャクラス public CardCreateManager CardCreate; void Start () { // 一致したカードIDリストを初期化 this.mContainCardIdList.Clear (); // カードリストを生成する this.CardCreate.CreateCard (); } void Update () { // 選択したカードが2枚以上になったら if (GameStateController.Instance.SelectedCardIdList.Count >= 2) { // 最初に選択したCardIDを取得する int selectedId = GameStateController.Instance.SelectedCardIdList[0]; // 2枚目にあったカードと一緒だったら if (selectedId == GameStateController.Instance.SelectedCardIdList[1]) { Debug.Log ($"Contains! {selectedId}"); // 一致したカードIDを保存する this.mContainCardIdList.Add (selectedId); } // カードの表示切り替えを行う this.CardCreate.HideCardList (this.mContainCardIdList); // 選択したカードリストを初期化する GameStateController.Instance.SelectedCardIdList.Clear (); } }
まずはプロパティに 「CardCreateManager」の変数を追加し、Hierarchyにあるオブジェクトにアタッチしてください。
その後、CardCreateManager.CreateCard()関数を実行し、カードの生成を行なっております。
そして、前回記述した、「同じカードを選択していたら〜」の処理に先ほど作成した「HideCardList」関数を呼ぶように記述します。
ここまでできたらGameを起動してみましょう!
上記のように動作していたら成功です!
しかし、切り替えが雑な状態ですよね。。。
このままでは 不恰好なので「DOTween」を導入して綺麗なアニメーションを実装してみましょう!
【Unity ゲーム制作】神経衰弱ゲームを作ろう!〜その7 (カードを2枚選択したら)
前回はカードを選択した時の処理を作りました!
今回はカードを2枚数選択した時の処理を実装していきましょう!
選択したカードを管理するクラスを作ろう!
神経衰弱は2枚とも同じカードを選択していたらカードを獲得できるゲームですね。
そのためにはプレイヤーが選択したカードIDを保管しておく必要があります。
せっかくなので、今回はシングルトンクラスを作成し、選択したカードIDの管理をしてもらいましょう。
using System.Collections; using System.Collections.Generic; using UnityEngine; public class GameStateController : MonoBehaviour { // 選択されたカードIDリスト public List<int> SelectedCardIdList = new List<int> (); // シングルトンの生成 private static GameStateController mInstance; public static GameStateController Instance { get { // インスタンスが生成されていない場合、自動で生成する if (mInstance == null) { GameObject obj = new GameObject ("GameStateController"); mInstance = obj.AddComponent<GameStateController> (); } return mInstance; } } }
GameStateController.csを作成し、処理を記述してください。
では、前回作成した 「Card.cs」のOnClick()関数に選択したカードIDをGameStateControllerに保管する処理を記述します!
カード.csの部分
/// <summary> /// 選択された時の処理 /// </summary> public void OnClick () { // カードが表面になっていた場合は無効 if (this.mIsSelected) { return; } Debug.Log ("OnClick"); // 選択判定フラグを有効にする this.mIsSelected = true; // カードを表面にする this.CardImage.sprite = this.mData.ImgSprite; // 選択したCardIdを保存しよう! GameStateController.Instance.SelectedCardIdList.Add (this.mData.Id); }
GameStateController.Instance.SelectedCardIdList.Add()にthis.mData.Idを追加しています。
これでGameStateControllerに選択したカードIDを保管することができました。
次は、「選択したカード枚数が2枚数以上になった場合」の処理を記載しましょう。
そのためには、「ゲーム全体を管理するクラス」を作成し、そこに処理を記載したいと思います。
GameSceneManager.csを作成しよう!
まずは GameSceneManager.cs を作成し、Hierarchyにある「GameSceneManager」にアタッチしましょう!
そうしたら、GameSceneManagerのUpdate関数に以下の処理を記述します。
using System; using System.Collections; using System.Collections.Generic; using UnityEngine; public class GameSceneManager : MonoBehaviour { // 一致したカードリストID private List<int> mContainCardIdList = new List<int> (); void Update () { // 選択したカードが2枚以上になったら if (GameStateController.Instance.SelectedCardIdList.Count >= 2) { // 最初に選択したCardIDを取得する int selectedId = GameStateController.Instance.SelectedCardIdList[0]; // 2枚目にあったカードと一緒だったら if (selectedId == GameStateController.Instance.SelectedCardIdList[1]) { Debug.Log ($"Contains! {selectedId}"); // 一致したカードIDを保存する this.mContainCardIdList.Add (selectedId); } // 選択したカードリストを初期化する GameStateController.Instance.SelectedCardIdList.Clear (); } } }
「.SelectedCardIdList.Count 」は、リストに保管されている要素数を取得できます。
そして、最初に選択したCardIdと、次に選択したCardIdが一緒だった場合、 this.mContainCardIdListに選択したカードIDを保管しています。
this.mContainCardIdList は 「獲得したカードIDを保管するリスト」になります。
こちらは次回以降で活用していきます!
最後に 「 GameStateController.Instance.SelectedCardIdList.Clear ()」を宣言しています。
こちらは 「SelectedCardIdList」の中身を初期化する処理です。
では、Gameを起動して動作を見てみましょう!
同じカードが選択したら専用のLogが表示されれば成功です!。
【Unity ゲーム制作】神経衰弱ゲームを作ろう!〜その6 (カードの裏面画像と切り替え処理 )
カードの裏面処理と切り替え処理の実装です
裏面用のカードをUnityに導入しよう!
前回 ( その5) にてフライングしていましたが、裏面表記用の画像をUnityに導入しましょう。
UnityのAssets/Resources/Imageフォルダ内に画像を D&D してください。
その後、画像のTypeをSpriteにすれば導入完了です。
Cardオブジェクトに裏面画像を読み込もう!
ではソースコードに記述しましょう。
Card.csを変更します。
Cardオブジェクトの生成関数部分にて、今までは読み込んだ表面のカードをそのまま読み込んでいましたが、
今回はそれを修正し、初回は必ず「裏面画像」を読み込むようにしましょう!。
// カード情報 private CardData mData; // カードの設定 public void Set (CardData data) { // カード情報を設定 this.mData = data; // IDを設定する this.Id = data.Id; // 表示する画像を設定する // 初回は全て裏面表示とする this.CardImage.sprite = Resources.Load<Sprite> ("Image/card_back"); // 選択判定フラグを初期化する this.mIsSelected = false; }
ポイントは this.CardImage.sprite = Resources.Load
先ほど読み込んだ裏面画像をここで反映しておきます。
そして、もう一つのポイントとして private CardData mData があります。
こちらは カード情報クラスのプライベート変数です。
mData にCardData情報を保存しておきます。
裏面表記になっているか確認
Gameを起動しましょう。
画像のように読み込んだ裏面画像が表示されていれば成功です!。
カードをクリックしたら表面が表示されるようにしよう!
いよいよ神経衰弱っぽくなってきました。
神経衰弱と言えば「裏面カードを選んで、同じカードを当てる」ゲームですよね!
そのためには 裏面表記のカードを選んだら、表面が表記される」必要があります。
前回記載した、OnClick()関数に処理を追記しましょう!
/// <summary> /// 選択された時の処理 /// </summary> public void OnClick () { // カードが表面になっていた場合は無効 if (this.mIsSelected) { return; } Debug.Log ("OnClick"); // 選択判定フラグを有効にする this.mIsSelected = true; // カードを表面にする this.CardImage.sprite = this.mData.ImgSprite; }
this.CardImage.sprite = this.mData.ImgSprite; によって、先ほど保存したmDataのImgSpriteに画像表記を切り替えています。
これで裏面表示のカードを選択したら、表面カードが表記されるようになりました!。
実際に動作を確認しよう!
Gameを起動して、記事最初のGIFや画像のように裏面をクリックしたら表面画像が表示されれば成功です!
次はいよいよ神経衰弱のゲーム部分、カードを2枚選択して、一緒だったら・・・!? の処理を作成して行きましょう!
【Unity ゲーム制作】神経衰弱ゲームを作ろう!〜その5 (カードを選択しよう!)
今回は前回表示したカードオブジェクトを実体にタップ (クリック) してカードをめくる処理を作ってみましょう!
Card.prefabを編集しよう!
Unity 2018.3以降ですとPrefabを選択すると編集モードに切り替わります。
右上のOpenを押してみてください。
Prefabの中身が表示されました。
この状況では、PREFABに新しいコンポーネントを追加するなどの処理を行うことができます。
ただし、変更したら保存しないと反映さ」ないので注意しましょう。
それでは、Card.Prefabをいじってみましょう。
ButtonのTransition を 「NONE」に変更しよう!
Transitionは遷移という意味になります。
初期設定のColoeTintはボタンが選択されたらオブジェクトの色を変化させる処理です。
今回は色を変化させる必要がないため、Transitionの機能をオフにしましょう。
Buttonの要素にある「Transition」をクリックしてNONEを選択してください。
画像の通りになったら成功です!
Card.csに カードを選択した 時の処理を記述しよう!
今度はソースコードに、カードが選択された時の処理を記載します。
Card.csに「OnClick」という関数を作成しましょう。
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class Card : MonoBehaviour { // カードのID public int Id; // 表示するカードの画像 public Image CardImage; // 選択されているか判定 private bool mIsSelected = false; // カードの設定 public void Set (CardData data) { // IDを設定する this.Id = data.Id; // 表示する画像を設定する this.CardImage.sprite = data.ImgSprite; // 選択判定フラグを初期化する this.mIsSelected = false; } /// <summary> /// 選択された時の処理 /// </summary> public void OnClick () { // カードが表面になっていた場合は無効 if (this.mIsSelected) { return; } Debug.Log ("OnClick"); // 選択判定フラグを有効にする this.mIsSelected = true; } }
まずは 変数に bool型のisSelectedを追加しました。
初期設定の時はfalse (未選択)の状態にしておきます。
そして、カードが選択(めくられた)ときにtrue(選択)状態にしています。
Debug.Log は、Unityのコンソール画面にログを表示する関数です。
今回はクリックされたらそれを知らせるようにしております。
Onclick関数はクリックされたとき用の関数です。
これを先程のButtonのOnclickプロパティにアタッチしましょう!
まずはButtonのコンポーネントのOnClickをクリックします。
その後、Card.csをアタッチしましょう。
そうしたら、OnClickと書かれた右上のボタンを押し、出てきた一覧から「Card」→ OnClick()を選択してください。
上の画像の様になれば、ボタンをクリックした時に、OnClick関数が実行する様になりました。
それでは、Gameを起動して実際にカードをクリックしてみましょう!
上記の様にクリックしたらConsoleにログが出現したら成功です!
【Unity ゲーム制作】神経衰弱ゲームを作ろう!〜その4 ( カードの複数生成 )
今回は以下の動画の様にランダムに複数のカードオブジェクトを生成する処理を実装しましょう!
CardData.class に関する記述が抜けていたため、修正いたします。
まずは、Cardクラスで表示する画像情報などをまとめたクラスを作りましょう。
Card.csに「CardData」クラスを作成しよう!
/// <summary> /// カードの情報クラス /// </summary> public class CardData { // カードID public int Id { get; private set; } // 画像 public Sprite ImgSprite { get; private set; } public CardData (int _id, Sprite _sprite) { this.Id = _id; this.ImgSprite = _sprite; } }
上記のスクリプトを「Card.cs」の内部に作成してください。
作成し終わったら、内容について説明していきます。
このクラスの役割はズバリ 「Cardクラスで使用する情報用のクラス」です。
どういう意味かと言いますと、「public Sprite ImgSprite { get; private set; }」という変数を定義しています。
こちらは 「Cardに表示させる画像情報」を保管する変数です。
public CardData (int _id, Sprite _sprite) {
this.Id = _id;
this.ImgSprite = _sprite;
}
上のコードを見てください。
コンストラクタ ( クラスを new するときに発生する関数 ) でid と 画像データを設定しています。
つまり、Cardクラスを使って何かを表示したいときに、直接 Cardクラスに画像データを送るのではなく、
CardData クラスを作成して「表示する画像情報」などを設定し終わったあと、その内容をCardクラスに反映する仕組みになります。
CardDataクラスが作成し終わった、「CardCreateManager.cs」のStart関数に以下の処理を書き込みましょう。
usign System.Linq; == このusing を新規に記入しておいてください。 void Start () { // カード情報リスト List<CardData> cardDataList = new List<CardData> (); // 表示するカード画像情報のリスト List<Sprite> imgList = new List<Sprite> (); // Resources/Imageフォルダ内にある画像を取得する imgList.Add (Resources.Load<Sprite> ("Image/card_image_000")); imgList.Add (Resources.Load<Sprite> ("Image/card_image_001")); imgList.Add (Resources.Load<Sprite> ("Image/card_image_002"));
ここではまずは生成するカード画像情報を取得しています。
List
このcardDataList にカードオブジェクトの情報を設定します。
次に List
そして次の行から、Sprite型の情報をimgListに追加しています。
今回は、Resources.Load() を使って前回導入したカード情報を取得しています。
Resources.Load() はUnityに設定されている関数で、Assets配下の「Resources」フォルダに格納されているファイルを参照して取得できる関数です。
今回は Load
注意点としては、Resources配下に設定した画像などが存在しない場合、Null値を出力するので、使用する際はnullチェックを忘れないでください。
次は取得したカード用の画像の分だけ、CardDataを作成しましょう。
// forを回す回数を取得する int loopCnt = imgList.Count; for (int i = 0; i < loopCnt; i++) { // カード情報を生成する CardData cardata = new CardData (i, imgList[i]); cardDataList.Add (cardata); }
ここではfor文をしようしています。
int loopCnt = imgList.Count;
この部分ですが、List.Count() をfor文など使用する場合、List.Count()が呼ばれる度にCount()関数内部の処理が実行されます。
今回は数枚規模なので処理に大きな差は出ませんが、何百単位でのループ処理が必要な場合などは、list.Count()を使用する場合は変数に代入してから
使用することをお勧めします。
Listについては以下の記事も参考にしてください!
wojtek.hatenablog.com
CardDataの生成が完了しました。
神経衰弱で大事なのは、同じカードが2枚存在していることですよね
なのでもう一つのList
// 生成したカードリスト2つ分のリストを生成する List<CardData> SumCardDataList = new List<CardData> (); SumCardDataList.AddRange (cardDataList); SumCardDataList.AddRange (cardDataList);
これで同じカードが2枚ずつ存在する状態になりました。
次は表示するカードの順番をランダムにする処理です。
// リストの中身をランダムに再配置する List<CardData> randomCardDataList = SumCardDataList.OrderBy (a => Guid.NewGuid ()).ToList ();
List
OrderBy() はLingの機能で要素を昇順に並べ替えています。
今回はその要素にGuidを使用しました。
Guidは値がめったに重複しない様に生成される識別子です。
今回はこの値を元にListの中身をランダムに入れ替えています。
これで表示するCardData情報は完成しました。
あとはこの情報を実体オブジェクトに格納しましょう。
// オブジェクト生成
// カードオブジェクトを生成する foreach (var _cardData in randomCardDataList) { // Instantiate で Cardオブジェクトを生成 Card card = Instantiate<Card> (this.CardPrefab, this.CardCreateParent); // データを設定する card.Set (_cardData); }
ここではforeachを使っています。
これで格納された数のCardオブジェクトを生成することができます。
上記スクリプトで書かれている 「card.Set (_cardData);」について解説します。
Card.csに以下の関数を作成してください。
public class Card : MonoBehaviour { // カードのID public int Id; // 表示するカードの画像 public Image CardImage; // 選択されているか判定 private bool mIsSelected = false; // カードの設定 public void Set (CardData data) { // IDを設定する this.Id = data.Id; // 表示する画像を設定する this.CardImage.sprite = data.ImgSprite; // 選択判定フラグを初期化する this.mIsSelected = false; }
これは先ほど作成したCardDataの情報を「Cardクラス」に反映してしています。
画像情報やID番号を反映することができるようになりました。
この後Gameを起動してみて、最初のGifの様な動作になれば成功です!。