この記事はハクスラローグライク×デッキ構築型カードバトルRPG「呪術迷宮」の作り方講座の第5回です。
前回では、戦闘に使うカードを手札やフィールド間でドラッグ&ドロップしたり、変な場所で手を離した場合にカードを手札の元の位置に戻すカード操作の実装を行いました。
前回の記事:
カードバトルの戦闘システムを開発する前にまだまだ準備しておかなければならないことがあります。
今回はターン開始時の処理として、プレイヤーが決まった枚数だけ手札を補充するという山札からのカードドローシステムを作っていきます。
UIを整列させるLayout Groupの設定
UIオブジェクトを規則的に整列させたいという場合、エディター上で座標を手入力したりスクリプトで座標を計算して反映させる等といった方法もあるのですが、今回はUnityで用意されているLayout Group系のコンポーネントを使用してみましょう。
Layout Groupと名の付く整列用コンポーネントは3種類あります。いずれも子のUIオブジェクト全てに対して影響を与えます。
Horizontal Layout Group | X軸上のUIを整列させる |
Vertical Layout Group | Y軸上のUIを整列させる |
Grid Layout Group | 四角いマスの並びにUIを整列させる |
今回、手札のカードは横方向に並ばせたいのでHorizontal Layout Groupを使用します。Handsオブジェクト内に新規コンポーネントとして追加しましょう。
まだ子オブジェクトが存在しないため何の変化もありませんが、試しにCardプレハブをHandsオブジェクト以下に複数配置してみると…
1つ1つオブジェクトの座標設定をしなくても自動的に位置を揃えてくれていることが確認できます。
しかしこの確認で分かる通り、左側に寄せるような整列の仕方になってしまっているためHorizontal Layout Groupの設定を変更して中央に寄せる形にしましょう。主に変更することが多い設定項目は以下です。
Padding | 領域の端からの(各方向ごとの)距離を離す |
Spacing | 子UIのそれぞれの距離 |
Child Alignment | 整列の位置(始点)。Upper Leftなら左上が始点になりMiddle Centerなら中央が始点になる。 |
Child Force Expand | 領域内に隙間がある時、子UIを隙間なく始点から詰めるか領域いっぱいに等間隔に分散させるかの設定 |
今回はUIを中央揃えにしたいので、Child Alignmentの設定をMiddle Centerに変更します。
中央を始点にUIが整列されていることが確認できたら、確認用の子オブジェクトは全て削除しましょう。
ダミー手札オブジェクトを作成 カードをはみ出さないように重ねて持つ
手札カードを整列する仕組みは出来上がりましたが、実際にプレイヤーがドローするカードをHandsオブジェクトの子に入れると以下の問題が発生します。
- カードが移動するアニメーションを表示できない(Layout Groupによって座標が固定されるため)
- 手札が多くなった時に領域の右側にはみ出てしまう(Layout GroupではUIの重なりが苦手なため)
これを解決するためにHandsオブジェクトの子には実体のない「ダミー手札オブジェクト」を手札枚数と同じだけ用意し、実際に作成する手札オブジェクトはそれぞれダミー手札オブジェクトと同じ位置に移動させるという実装を行います。
ダミー手札オブジェクトをカードよりも小さい大きさで作ればカード同士の重なりも実現できるため一石二鳥です。
それでは空のUIオブジェクトとしてDammyHandを作成しましょう。場所はどこでも大丈夫です。大きさ(Width・Height)も適当に小さめの値を入力しました。
このオブジェクトはゲーム中に生成・削除されるオブジェクトなのでプレハブ化します。
プレハブ化したらヒエラルキーからは削除します。
ダミー手札を制御するスクリプトを作成
ダミー手札オブジェクトを必要な数になるまで生成、あるいは削除しそれぞれの座標を取得する処理を担うクラスを作成します。Scripts > Battleの中にC#スクリプトを作成し、名前はDammyHandUIとします。
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 |
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; /// <summary> /// ダミー手札制御クラス /// </summary> public class DammyHandUI : MonoBehaviour { // ダミー手札整列用HorizontalLayoutGroup [SerializeField] private HorizontalLayoutGroup layoutGroup = null; // ダミー手札プレハブ [SerializeField] private GameObject dammyHandPrefab = null; // 生成したダミー手札のリスト private List<Transform> dammyHandList; /// <summary> /// 指定の枚数になるようダミー手札を作成または削除する /// </summary> /// <param name="value">設定枚数</param> public void SetHandNum (int value) { if (dammyHandList == null) {// 初回実行時 // リスト新規生成 dammyHandList = new List<Transform> (); AddHandObj (value); } else { // 現在から変化する枚数を計算 int differenceNum = value - dammyHandList.Count; // ダミー手札作成・削除 if (differenceNum > 0) // 手札が増えるならダミー手札作成 AddHandObj (differenceNum); else if (differenceNum < 0) // 手札が減るならダミー手札削除 RemoveHandObj (differenceNum); } } /// <summary> /// ダミー手札を指定枚数追加する /// </summary> private void AddHandObj (int value) { // 追加枚数分オブジェクト作成 for (int i = 0; i < value; i++) { // オブジェクト作成 var obj = Instantiate (dammyHandPrefab, transform); // リストに追加 dammyHandList.Add (obj.transform); } } /// <summary> /// ダミー手札を指定枚数削除する /// </summary> private void RemoveHandObj (int value) { // 削除枚数を正数で取得 value = Mathf.Abs (value); // 削除枚数分オブジェクト削除 for (int i = 0; i < value; i++) { if (dammyHandList.Count <= 0) break; // オブジェクト削除 Destroy (dammyHandList[0].gameObject); // リストから削除 dammyHandList.RemoveAt (0); } } /// <summary> /// 該当番号のダミー手札の座標を返す /// </summary> public Vector2 GetHandPos (int index) { if (index < 0 || index >= dammyHandList.Count) return Vector2.zero; // ダミー手札の座標を返す return dammyHandList[index].position; } /// <summary> /// レイアウトの自動整列機能を即座に適用する /// </summary> public void ApplyLayout () { layoutGroup.CalculateLayoutInputHorizontal (); layoutGroup.SetLayoutHorizontal (); } } |
ターン開始時、あるいはカードを移動させたり合成させた時など、手札の枚数が増減する度にSetHandNumを呼び出すことになります。
これによってダミー手札オブジェクトが増減するので、GetHandPosメソッドでその位置を取得しそこへ手札カードのオブジェクトを移動させます。
スクリプトによってオブジェクトの作成を行った直後はLayout Groupの整列機能がまだ働いていないので、強制的にその機能を呼び出すApplyLayoutメソッドも用意しています。
このスクリプトはHandsオブジェクトへアタッチして使用します。パラメータの指定も忘れずに行いましょう。dammyHandPrefabには先ほど作成したダミー手札のプレハブを指定します。
カードのゾーン(領域)を設定する
手札処理を実装する上でもう一つやっておくべき準備として、いまプレイヤーカードが置かれている場所という情報を「カードゾーン」として管理するシステムの作成です。
手札に配られたカードはその後プレイボードに置かれたりトラッシュゾーンに送られることもあります。この3種類のゾーンをスクリプト内で定義しておきましょう。
まずはスクリプトから作成します。クラス名はCardZoneとしました。
ゲームを実行すると、開始から1秒後に5枚のカードが山札からドローされている動きが確認できます。
カードを別のゾーンに移動する処理を作成する
仕上げとして、プレイヤーのドラッグ操作によってカードを別のゾーンに設置するという機能を作成しておきましょう。
FieldManager.cs
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 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 |
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using DG.Tweening; /// <summary> /// フィールド管理クラス /// </summary> public class FieldManager : MonoBehaviour { // オブジェクト・コンポーネント参照 private BattleManager battleManager; // 戦闘画面マネージャ public RectTransform canvasRectTransform; // CanvasのRectTransform public Camera mainCamera; // メインカメラ [SerializeField] private DammyHandUI dammyHandUI = null; // ダミー手札制御クラス // カード関連参照 [SerializeField] private GameObject cardPrefab = null; // カードプレハブ [SerializeField] private Transform cardsParent = null; // 生成するカードオブジェクトの親Transform [SerializeField] private Transform deckIconTrs = null; // デッキオブジェクトTransform // 各種変数・参照 private Card draggingCard; // ドラッグ操作中カード private List<Card> cardInstances; // 生成したプレイヤー操作カードリスト private bool reserveHandAlign; // 手札整列フラグ private bool isDrawing; // true:手札補充中である // 初期化処理 public void Init (BattleManager _battleManager) { // 参照取得 battleManager = _battleManager; // 変数初期化 cardInstances = new List<Card> (); // デバッグ用ドロー処理(遅延実行) DOVirtual.DelayedCall ( 1.0f, // 1.0秒遅延 () => { DrawCardsUntilNum (5); } ); } // Update void Update() { // ドラッグ操作中の処理 if (draggingCard != null) { // 更新処理 UpdateDragging (); } } // OnGUI(Updateのように繰り返し実行・UI制御用) void OnGUI () { // 手札整列フラグが立っているなら整列 if (reserveHandAlign) { AlignHandCards (); reserveHandAlign = false; } } #region プレイヤー側手札・デッキ処理 /// <summary> /// デッキからカードを1枚引き手札に加える /// </summary> /// <param name="handID">対象手札番号</param> private void DrawCard (int handID) { // オブジェクト作成 var obj = Instantiate (cardPrefab, cardsParent); // カード処理クラスを取得・リストに格納 Card objCard = obj.GetComponent<Card> (); cardInstances.Add (objCard); // カード初期設定 objCard.Init (this, deckIconTrs.position); objCard.PutToZone (CardZone.ZoneType.Hand, dammyHandUI.GetHandPos (handID)); } /// <summary> /// 手札が指定枚数になるまでカードを引く /// </summary> /// <param name="num">指定枚数</param> private void DrawCardsUntilNum (int num) { // 現在の手札枚数を取得 int nowHandNum = 0; foreach (var card in cardInstances) { if (card.nowZone == CardZone.ZoneType.Hand) nowHandNum++; } // 新たに引くべき枚数を取得 int drawNum = num - nowHandNum; if (drawNum <= 0) return; // 手札UIに枚数を指定 dammyHandUI.SetHandNum (nowHandNum + drawNum); // 連続でカードを引く(Sequence) const float DrawIntervalTime = 0.1f; // ドロー間の時間間隔 var drawSequence = DOTween.Sequence (); isDrawing = true; for (int i = 0; i < drawNum; i++) { // 1枚引く処理 drawSequence.AppendCallback (() => { DrawCard (nowHandNum); nowHandNum++; }); // 時間間隔を設定 drawSequence.AppendInterval (DrawIntervalTime); } drawSequence.OnComplete (() => isDrawing = false); } /// <summary> /// 手札のカードを整列させる /// </summary> private void AlignHandCards () { // 手札整列処理 int index = 0; // 手札内番号 // ダミー手札を整列 dammyHandUI.ApplyLayout (); // 各カードをダミー手札に合わせて移動 foreach (var card in cardInstances) { if (card.nowZone == CardZone.ZoneType.Hand) { card.PutToZone (CardZone.ZoneType.Hand, dammyHandUI.GetHandPos (index)); index++; } } } /// <summary> /// 現在の手札の枚数を手札UI処理クラスに反映させて整列する /// </summary> private void CheckHandCardsNum () { // 現在の手札枚数を取得 int nowHandNum = 0; foreach (var item in cardInstances) { if (item.nowZone == CardZone.ZoneType.Hand) nowHandNum++; } // ダミー手札に枚数を指定 dammyHandUI.SetHandNum (nowHandNum); // 手札枚数に合わせて手札を整列 // (手札枚数を変更した同フレームではダミー手札オブジェクトが動いていないため一瞬だけ遅延実行) reserveHandAlign = true; } #endregion #region カードドラッグ処理 /// <summary> /// カードのドラッグ操作を開始する /// </summary> /// <param name="dragCard">操作対象カード</param> public void StartDragging (Card dragCard) { // 手札補充演出中なら終了 if (isDrawing) return; // 操作対象カードを記憶 draggingCard = dragCard; // 他のカードオブジェクトより兄弟間で一番後ろにする(最前面表示にする) draggingCard.transform.SetAsLastSibling (); } /// <summary> /// ドラッグ操作更新処理 /// </summary> private void UpdateDragging () { // タップ位置を取得 Vector2 tapPos = Input.mousePosition; // タップ座標を変換する(スクリーン座標→Canvasのローカル座標) RectTransformUtility.ScreenPointToLocalPointInRectangle ( canvasRectTransform,// CanvasのRectTransform tapPos, // 変換元座標データ mainCamera, // メインカメラ out tapPos); // 変換先座標データ // 座標を適用 draggingCard.rectTransform.anchoredPosition = tapPos; } /// <summary> /// カードのドラッグ操作を終了する /// </summary> public void EndDragging () { // 重なっているオブジェクトの情報を全て取得する // (判定が必要なオブジェクトには全てBoxCollider2Dが付与されているのでそれを利用して判定) // このオブジェクトのスクリーン座標を取得する Vector3 pos = RectTransformUtility.WorldToScreenPoint (mainCamera, draggingCard.transform.position); // メインカメラから上記で取得した座標に向けてRayを飛ばす Ray ray = mainCamera.ScreenPointToRay (pos); // ドラッグ先のオブジェクト取得処理 CardZone targetZone = null; // ドラッグ先カードゾーン Card targetCard = null; // ドラッグ先カード // Rayが当たった全オブジェクトに対しての処理 foreach (RaycastHit2D hit in Physics2D.RaycastAll (ray.origin, ray.direction, 10.0f)) { // 当たったオブジェクトが存在しないなら終了 if (!hit.collider) break; // 当たったオブジェクトがドラッグ中のカードと同一なら次へ var hitObj = hit.collider.gameObject; if (hitObj == draggingCard.gameObject) continue; // オブジェクトがカードエリアなら取得して次へ var hitArea = hitObj.GetComponent<CardZone> (); if (hitArea != null) { targetZone = hitArea; continue; } // オブジェクトがカードなら取得して次へ var hitCard = hitObj.GetComponent<Card> (); if (hitCard != null) { targetCard = hitCard; continue; } } // 重なった対象ごとによる処理 if (targetCard != null && (targetCard.nowZone >= CardZone.ZoneType.PlayBoard0 && targetCard.nowZone <= CardZone.ZoneType.PlayBoard4)) {// プレイボードにあるカードと重なった場合 // 合成処理(未実装) } else if (targetZone != null) {// カードと重ならずカードエリアと重なった場合 // 設置処理 draggingCard.PutToZone (targetZone.zoneType, targetZone.GetComponent<RectTransform> ().position); CheckHandCardsNum (); // 手札以外→手札への移動の場合、カードをリスト内で一番後ろにする if (draggingCard.nowZone == CardZone.ZoneType.Hand) { cardInstances.Remove (draggingCard); cardInstances.Add (draggingCard); } } else {// いずれとも重ならなかった場合 // 元の位置に戻す draggingCard.BackToBasePos (); } // 後処理 draggingCard = null; } #endregion } |
- OnGUIメソッドはUpdateと同様に標準の機能であり、ほぼ1フレームごとに実行されます。Updateでなくこちらを選んだ理由として、Layout Groupの仕様との兼ね合いがありUpdate側では正常に動作しなかったためです。
先ほどカードゾーンごとに設定した当たり判定を利用してドラッグ先地点にあるゾーン情報を取得しています。
判定にはRaycastを使用しています。今回は指先が触れている地点から画面の奥方向に向かって光線を飛ばすようなイメージで、その光線に触れた(当たり判定を持った)オブジェクト全ての情報を取得し、foreachによってそれぞれの情報を確認しています。
現在はカードに対して当たり判定を設けていないのでゾーンのオブジェクトにのみヒットしています。
カードをドラッグ&ドロップして離した場所の情報を取得し、場合分けして処理を実行しています。細かい内容はコメントを参照してください。
実装が完了したらカードを移動・設置させる動作を確認してみましょう。今回はInspector側の操作は不要です。
まとめ
DOTweenのSequenceや遅延実行処理などを活かしてデッキからカードをドローする演出を作成しました。Tweenを含めたこれら3つの機能は非常によく使いますので少しずつ使い方を覚えていきましょう。
そしてカードゾーンのシステムを設計し、当たり判定をとって別のゾーンに移動させる処理も作りました。GUIベースのゲーム設計でもスプライト用の当たり判定が使用可能ですのでこちらも覚えておくと良いでしょう。
次の記事:
コメント
執筆ご苦労様です。誤記のご報告です。
最後のFieldManager.csの6行目と85行目(もしかしたらまだあるかもしれません)から、黄色のライン表示が抜けています。
大変勉強になる記事を公開していただきありがとうございます。これからも頑張ってください。
講座を読んでいただきありがとうございます。
こちら、アプリ版でのみ利用しており、講座版では不要な処理の消し忘れでした。
この後も使わない部分となりますのでスクリプトから削除していただいて大丈夫です。
講座からも該当箇所の処理を削除して修正を行いました。
ご報告ありがとうございます。