最近のゲームではクエストと呼ばれる小さなイベントがありそれをこなしていくという機能が多くなりましたね。
ゲームをコンプリートしようという気持ちが強い人は全クエストをこなしたくなるものです。
(^_^)v
そこで今回はUnityでクエスト管理をする機能を作成してみようと思います。
クエストはステータス画面を開いた時に表示されるようにし、
クエスト画面ではクエストが終了したもの、してないものが一発でわかるようにしたり、タイトルやその内容を表示するようにします。
全クエストが100個あった場合等は全部のクエストをクエスト画面に表示すると大変なので数を決めて表示し、前のページ、次のページに遷移出来るようにボタンを配置します。
非常にシンプルに作成してはいますが、内容的にはボリュームが多いです。
それぞれの関連性を丁寧に見ていってください(あいかわらず説明は下手です)。
クエストを管理するゲームオブジェクトとUIの作成
まずはクエスト画面を構成するゲームオブジェクトと個々のクエストの内容を表示するUIを作成していきます。
ヒエラルキー上で右クリック→Create Emptyで空オブジェクトを作成し、名前をQuestManagementとします。
その子要素に右クリック→UI→Canvasを選択し名前をQuestUIとします。
その子要素にUI→Panelを選択し名前をBackgroundとします。
Backgroundの子要素にはUI→Panelを選択し名前をQuest0とします。
Quest0の子要素にUI→Panelを2つ作り、名前をTitlePanel、InformationPanelとします。
TitlePanelの子要素にUI→Toggle、InformationPanelの子要素にUI→Textを作ります。
Toggleを選択しトグルの設定を変更します。
↑のようにInteractableのチェックを外し、TransitionをNoneにします。
これで手動でチェックボックスを操作する事が出来なくなります。
Quest0を選択した状態でCtrl+Dでコピーを4つ作成し、最後の番号を増やした名前にします。
最後にページ移動ボタン領域を作成します。
Backgroundの子要素にUI→Panelを選択し名前をButtonPanelとします。
その子要素にUI→Buttonを2つ作成し名前をPrevとNextにします。
PrevとNextの子要素にはTextがあるので表示されるテキストをPrevとNextに変更しておきます。
↑が作成したクエスト関連のヒエラルキーになります。
Quest1~Quest4まではQuest0をコピーして名前の最後の数字を変えただけで同じです。
これでクエスト関連のゲームオブジェクトは出来ました。
BackgroundゲームオブジェクトのインスペクタでAdd Component→Layout→Vertical Layout Groupを設定します。
これでQuest0~Quest4、ButtonPanelのパネルが縦に並ぶようになります。
↑のようにコンポーネントを追加しました。
最終的に出来たクエストの確認画面は
↑のようになっています。Questの子要素のLabelのテキストで初期タイトル、Informationのテキストで初期内容を設定出来ます(後でスクリプトで操作するので特に意味はありませんが・・・)。
クエスト情報を保存するQuestクラスを作成する
クエストはそれぞれ情報を持っています。
名前や内容、どうすればクリア出来るか?といった情報です。
これらはクエスト用のクラスを作成しそこに情報を保持出来るようにし、クエストを管理するスクリプトからアクセスして個別のクエスト情報を得られるようにしましょう。
今回はクエストの名前と内容を保持出来るようにします。
Questスクリプトをクラスの形で作成します。
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 | using UnityEngine; using System.Collections; public class Quest : object { // クエストのタイトル private string title; // クエストの内容 private string information; // クエストクラスのコンストラクタ public Quest(string title = "タイトルなし", string info = "内容なし") { this.title = title; this.information = info; } // クエストのタイトルを返す public string GetTitle() { return title; } // クエスト情報を返す public string GetInformation() { return information; } } |
Questクラスは単純にインスタンス化した時にタイトルと内容を保持し、それを返すメソッドが定義されているだけです。
Questクラスはobjectクラスを継承して作成しておきます。
このQuestクラスはゲームオブジェクトに取り付けて使用するものではなく他のスクリプトからこのクラスのインスタンスを生成し使用するものです。
クエストを管理するQuestManagementスクリプトを作成する
個々のクエストクラスは出来ましたが、そのクエストクラスからインスタンスを生成し全クエストを管理するようなスクリプトが必要です。
その為QuestManagementゲームオブジェクトに新しくQuestManagementスクリプトを作成し取りつけます。
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 | using UnityEngine; using System.Collections; using UnityEngine.UI; public class QuestManagement : MonoBehaviour { // クエストリスト private Quest[] questList; // 全クエストの数 [SerializeField] private int totalQuestNum = 50; // クエストが終了しているかどうか private bool[] questIsDone; // クエスト画面UI private GameObject questUI; // 表示する個数 private int num; // クエストインスタンスを入れる変数 private Transform[] questInstanceList; // 現在開いているページ private int nowPage; // アクティブになったら現在のページを初期化 void OnEnable() { nowPage = 0; } void Start () { // クエスト関連のオブジェクト領域確保 questList = new Quest[totalQuestNum]; questIsDone = new bool[totalQuestNum]; // クエストUIのゲームオブジェクトを変数に入れる questUI = transform.Find("QuestUI").gameObject; // サンプルの説明文を設定しクエストインスタンスを大量生産 // 本来は一つ一つクエストの説明文を記述して作成する for(var i = 0; i < totalQuestNum; i++) { questList[i] = new Quest("タイトル" + i, "テストの説明" + i); } // それぞれのクエスト情報を表示(確認の為) for(var i = 0; i < totalQuestNum; i++) { Debug.Log(questList[i].GetTitle() + ":" + questList[i].GetInformation()); } nowPage = 0; // クエストを表示する個数を子要素の数から1引いて(ButtonPanel分)計算 num = transform.Find ("QuestUI/Background").childCount - 1; } void Update () { // Qキーを押したらクエスト画面を開く if(Input.GetKeyDown("q")) { // ShowQuestスクリプトのShow関数を呼び出し情報を書き換える Show(0); questUI.SetActive(!questUI.activeSelf); } } // クエスト終了をセット public void SetQuestFlag(int num) { questIsDone[num] = true; } // クエストが終了しているかどうか public bool IsQuestFlag(int num) { return questIsDone[num]; } // クエストを返す public Quest GetQuest(int num) { return questList[num]; } // トータルクエスト数を返す public int GetTotalQuestNum() { return totalQuestNum; } public void Show(int pageNum) { // 現在のページに受け取った引数を設定 nowPage = pageNum; // 表示する分のインスタンスを確保 questInstanceList = new Transform[num]; for(var i = 0; i < num; i++) { // クエスト表示パネルのTransform情報を探す questInstanceList[i] = transform.Find("QuestUI/Background/Quest" + i); // セットするコンポーネントを取得 var toggleCom = questInstanceList[i].Find("TitlePanel/Toggle").GetComponent<Toggle>(); var toggleTextCom = questInstanceList [i].Find ("TitlePanel/Toggle/Label").GetComponent<Text> (); var informationTextCom = questInstanceList [i].Find ("InformationPanel/Information").GetComponent<Text> (); // クエスト番号を変数に入れる var questNum = (pageNum * num) + i; // クエストがある場合 if(questNum < GetTotalQuestNum()) { // クエストの情報を表示ページと表示数を使って計算し取得する var check = IsQuestFlag(questNum); var title = GetQuest(questNum).GetTitle(); var info = GetQuest(questNum).GetInformation(); // 取得した情報をUIに反映させる toggleCom.isOn = check; toggleTextCom.text = title; informationTextCom.text = info; // クエストがない場合 } else { // クエスト情報がない為、空文字をセット toggleCom.isOn = false; toggleTextCom.text = ""; informationTextCom.text = ""; } } } // クエスト数とページ数から最大のページを計算しそれより下の場合は次のページへ public void NextPage() { // 表示すべきクエストがまだある時 if(GetTotalQuestNum() - ((nowPage + 1) * num) > 0) { Show(nowPage + 1); } } // 現在のページが0ページ目でなければ前のページを表示 public void PrevPage() { if(nowPage != 0) { Show(nowPage - 1); } } } |
クエストはこのQuestManagementスクリプトで全て生成します。
クエストが完了しているかどうかもこのスクリプトで管理する為、クエスト数分のbool型の配列を用意しています。
今回はそれぞれのクエストを細かく作るのが大変なのでfor文を使ってクエストクラスを大量生産してます・・・(^_^;)
今回のクエスト画面はQキーを押したら開き、再度押したら閉じるという単純なものにしました。
OnEnableメソッドはこのスクリプトが設定されているゲームオブジェクトがアクティブになった時に呼ばれるので、クエスト画面が開いた時に実行されます。
クエスト画面が開いた時は常に最初のページを表示させる為、ここでnowPageを0に初期化しています。
実際にQuest0~Quest4にそれぞれのクエストの情報を入れているのはShowメソッドになります。
引数で指定している数値は何ページ目を開くかの数字です。
スクリプトでは0を渡しているのでクエストの最初のページが開きます。
QuestManagementで全クエストのリストを保持しているので、それに関する値を得られるようにゲッターを用意しています。
例えばクエストの0番目が終了しているかどうかはIsQuestFlag(0)と呼び出すわけですね。
クエストのUIを見て頂くとわかりますが、1度に表示するクエストは5個です。
そこでクエスト画面を開いた時は最初の5個を表示し、Nextボタンを押したら次の5個を表示、というようにしなければいけません。
Showメソッドでは1ページに表示するクエストの個数を表すnum数分繰り返し、クエストの情報をテキストに表示しています。
このnumに設定する数値は作成したクエスト個別のUIの数と一致させます。
このスクリプトのゲームオブジェクトの子要素の数からButtonPanelの分を引いて表示するクエスト数とします。
今回の場合はQuest0~Quest4の5つを作成したのでnumに5が入ります。
Showメソッドを細かく見てみる
Showメソッドを細かく見てみましょう。
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 | public void Show(int pageNum) { // 現在のページに受け取った引数を設定 nowPage = pageNum; // 表示する分のインスタンスを確保 questInstanceList = new Transform[num]; for(var i = 0; i < num; i++) { // クエスト表示パネルのTransform情報を探す questInstanceList[i] = transform.Find("QuestUI/Background/Quest" + i); // セットするコンポーネントを取得 var toggleCom = questInstanceList[i].Find("TitlePanel/Toggle").GetComponent<Toggle>(); var toggleTextCom = questInstanceList [i].Find ("TitlePanel/Toggle/Label").GetComponent<Text> (); var informationTextCom = questInstanceList [i].Find ("InformationPanel/Information").GetComponent<Text> (); // クエスト番号を変数に入れる var questNum = (pageNum * num) + i; // クエストがある場合 if(questNum < GetTotalQuestNum()) { // クエストの情報を表示ページと表示数を使って計算し取得する var check = IsQuestFlag(questNum); var title = GetQuest(questNum).GetTitle(); var info = GetQuest(questNum).GetInformation(); // 取得した情報をUIに反映させる toggleCom.isOn = check; toggleTextCom.text = title; informationTextCom.text = info; // クエストがない場合 } else { // クエスト情報がない為、空文字をセット toggleCom.isOn = false; toggleTextCom.text = ""; informationTextCom.text = ""; } } } |
引数として受け取るのは何ページ目か?という情報です。
表示するクエスト情報分のTransform配列を確保します。
pageNum(ページ情報)にnum(1ページで表示するクエストの個数)をかけ、i番目を足すと全クエストからの番号を得られます。
そのクエスト番号を使ってクエストのタイトルとクエストの内容情報を取得し一時変数に入れます。
QuestManagementスクリプトで管理している『クエストが終了しているかどうか?』のフラグを取得し、ToggleのisOnにその値を入れています。
こうすることでクエストが終了しているものはタイトル横のチェックボックスにチェックが入ります。
それぞれの子要素からテキストUIコンポーネントを取得し、そこにさきほど取得した情報を表示します。
1度に表示するクエストは5個ですが全クエストが3しかなかった場合等に残りの2つにも何らかの情報を表示しなければいけません。
その為にクエストの番号と全クエストの数とを比較しクエスト番号が全クエスト数を超えた場合は空文字等を表示するようにしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | // クエスト数とページ数から最大のページを計算しそれより下の場合は次のページへ public void NextPage() { // 表示すべきクエストがまだある時 if(GetTotalQuestNum() - ((nowPage + 1) * num) > 0) { Show(nowPage + 1); } } // 現在のページが0ページ目でなければ前のページを表示 public void PrevPage() { if(nowPage != 0) { Show(nowPage - 1); } } |
NextPageとPrevPageメソッドはNextボタンとPrevボタンをそれぞれ押した時に実行するメソッドです。
次のページや前のページに表示するクエストがない場合は表示しないように制限を加えて表示するようにしています。
クエスト関連の設定を行う
クエスト関連のスクリプトが出来たのでそれぞれ設定していきましょう。
まずはQuestManagementゲームオブジェクトに設定したQuestManagementスクリプトです。
↑のように全クエスト数を50に設定します。
次にPrevとNextボタンのOn Click()に設定をします。
↑のようにPrevボタンのOn Click()のゲームオブジェクトにQuestManagementゲームオブジェクトをドラッグ&ドロップし、実行するメソッドにQuestManagementスクリプトのPrevPageメソッドを指定します。
Nextボタンも同じように設定し実行するメソッドはNextPageメソッドにします。
クエスト画面は最初表示されていると困るのでQuestUIゲームオブジェクトのインスペクタのチェックを外しておきます。
クエストが完了するサンプルの作成
クエスト画面の実装は出来ましたが、クエストが完了した時にどうなるのかがわかりません。
そこでそこに行くと設定したクエストが完了する領域を作成します。
↑のようにヒエラルキー上で右クリック→3D Object→Cubeを3つ作成しそれぞれの名前の後にクエストの番号を付けておきます。
この番号は特に使うわけでなくわかりやすいかなと思って付けただけです(スクリプトで使う番号+1になっています)。
コライダのIs Triggerにチェックを入れ物理的に当たらないようにします。
CubeそれぞれにはCompleteQuestスクリプトを作成し取りつけ、QuestManagementスクリプトと自身のクエスト番号をインスペクタで設定します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | using UnityEngine; using System.Collections; public class CompleteQuest : MonoBehaviour { // クエストの管理スクリプト [SerializeField] private QuestManagement questManagement = null; // このクエストの番号 [SerializeField] private int num; void OnTriggerEnter(Collider col) { // 領域に侵入したのがPlayerタグを持つものでこのクエストが終わっていなければクエスト成功にする if(col.tag == "Player" && !questManagement.IsQuestFlag(num)) { questManagement.SetQuestFlag(num); Debug.Log(questManagement.GetQuest(num).GetInformation()); } } } |
↑のように侵入してきたのがPlayerタグを持っており、かつこのクエストが完了していない場合はQuestManagementスクリプトのSetQuestFlagメソッドを使ってクエストを完了させます。
Cube3はクエスト2を完了させる領域として使用するので、インスペクタは
↑のようにCompleteQuestのNumに2を設定します。
実際のクエスト完了をさせる領域は↑のようになりました。
クエスト機能を確認する
これでクエスト機能が全て出来ました!
実際にキャラクターを動かしクエストを完了させる領域を周ってクエストを完了した後にクエスト画面を開いて確認してみます。
まずはQキーを押してクエスト画面を開きクエスト一覧が見れる事を確認し、その後クエスト1が完了するエリアに侵入しクエスト画面を開きます。
クエスト1に相当する最初のクエストのチェックが入りました。
次にクエスト10の領域に入りクエスト10にチェックが入っているかどうかを確認し、最後にクエスト2も確認してます。
今回の記事はクエストの管理機能を作ったんですが、それとは別にページ送りのUIも作成出来ましたね。
あ・・・最後に気付いたんですが、クエストUIに表示するものがない時はUI自体を非表示にしてしまえばいいかもしれませんね。
例えばQuest3とQuest4に表示するクエストがなければQuest3とQuest4を非表示にするとか。
ふむ・・・・・(-_-)。
クエストを一覧表示しスクロールして見れるようにする
ここまででクエスト機能ではクエストは5個毎にページを分けて見るようにしています。
そこでUIとスクリプトを少し改造して全部のクエスト一覧をスクロールして見れるようにしてみます。
ヒエラルキー上で右クリックからCreate Emptyを選択し、名前をQuestManagement2とします。
既に作っているQuestManagementの子要素のQuestUIを選択した状態でCtrl+Dキーを押して複製し、それをドラッグしてQuestManagement2の子要素に移動させます。
QuestManagementはインスペクタの名前の横のチェックを外し非アクティブにしておきます。
QuestUIの子要素のBackgroundのVertical Layout Groupを削除し、BackgroundのQuest0以外の全ての子要素を削除します。
Backgroundを選択した状態で右クリックからUI→Scroll Viewを選択します。
Scroll Viewと子要素のViewportのAnchor Presetsで横をstretch、縦をstretchとします。
Scroll Viewの子要素のContentを選択し、Add ComponentからLayout→Vertical Layout Groupを選択し取り付けます。
Anchor Presetsで横はstretchで縦はtopにし、Heightを2800にします(個々のクエストの高さとクエスト数によって変えてください)。
Vertical Layout GroupのLeft、Right、Top、Spacingを5にし、Control Child SizeのWidthにチェックを入れ子要素の個々のクエストの幅をコントロールします。
Child Force ExpandのWidthにチェックを入れ空き要素がある場合は強制で幅を広げます。
ここまでのQuestManagement2の階層は
上のようになります。
Quest0を選択し、名前をQuestPrefabとします。
QuestPrefabをContentの子要素に移動させます。
QuestPrefabのAnchor Presetsで横をleft、縦をtopにし、Heightを50にします。
ここまで出来たらQuestPrefabをAssetsエリア内にドラッグ&ドロップしてプレハブにします。
ヒエラルキーのQuestPrefabは削除します。
次にQuestManagementスクリプトをコピーしQuestManagement2スクリプトを作成しQuestManagement2に取り付けます。
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 UnityEngine; using System.Collections; using UnityEngine.UI; using System.Collections.Generic; using System.Linq; public class QuestManagement2 : MonoBehaviour { // クエストリスト private List<Quest> questList; // 全クエストの数 [SerializeField] private int totalQuestNum = 50; // クエストが終了しているかどうか private List<bool> questIsDone; // クエスト画面UI private GameObject questUI; // 表示する個数 private int num; // 一つ一つのクエスト情報を表示するプレハブ [SerializeField] private Transform questPrefab; // クエスト用UIのインスタンスを入れる private List<Transform> questUIInstanceList; // クエストを表示するUIのコンテンツ private Transform questContent; void Start () { questList = new List<Quest>(); questUIInstanceList = new List<Transform>(); questIsDone = Enumerable.Repeat(false, totalQuestNum).ToList<bool>(); // クエストUIのゲームオブジェクトを変数に入れる questUI = transform.Find("QuestUI").gameObject; // クエスト表示用UIのコンテンツ questContent = transform.Find("QuestUI/Background/Scroll View/Viewport/Content"); for(var i = 0; i < totalQuestNum; i++) { // クエスト用のUIプレハブを生成する var questUIInstance = Instantiate<Transform>(questPrefab, questContent); questUIInstanceList.Add(questUIInstance); // サンプルの説明文を設定しクエストインスタンスを大量生産 questList.Add(new Quest("タイトル" + i, "テストの説明" + i)); // セットするコンポーネントを取得 var toggleCom = questUIInstanceList[i].Find("TitlePanel/Toggle").GetComponent<Toggle>(); var toggleTextCom = questUIInstanceList[i].Find("TitlePanel/Toggle/Label").GetComponent<Text>(); var informationTextCom = questUIInstanceList[i].Find("InformationPanel/Information").GetComponent<Text>(); // クエストの情報を表示ページと表示数を使って計算し取得する var check = IsQuestFlag(i); var title = GetQuest(i).GetTitle(); var info = GetQuest(i).GetInformation(); // 取得した情報をUIに反映させる toggleCom.isOn = check; toggleTextCom.text = title; informationTextCom.text = info; } // それぞれのクエスト情報を表示(確認の為) for (var i = 0; i < totalQuestNum; i++) { Debug.Log(questList[i].GetTitle() + ":" + questList[i].GetInformation()); } } void Update () { // Qキーを押したらクエスト画面を開く if(Input.GetKeyDown("q")) { // クエストの状態をアップデート UpdateQuestData(); // ShowQuestスクリプトのShow関数を呼び出し情報を書き換える questUI.SetActive(!questUI.activeSelf); } } // クエストの状態をアップデート void UpdateQuestData() { for (int i = 0; i < questList.Count; i++) { questUIInstanceList[i].GetComponentInChildren<Toggle>().isOn = IsQuestFlag(i); } } // クエスト終了をセット public void SetQuestFlag(int num) { questIsDone[num] = true; } // クエストが終了しているかどうか public bool IsQuestFlag(int num) { return questIsDone[num]; } // クエストを返す public Quest GetQuest(int num) { return questList[num]; } // トータルクエスト数を返す public int GetTotalQuestNum() { return totalQuestNum; } } |
Startメソッドでは先ほど作成したQuestPrefabをクエストの数だけインスタンス化しデータを設定します。
1 2 3 | questIsDone = Enumerable.Repeat(false, totalQuestNum).ToList<bool>(); |
上はクエストが完了したかどうかを保持しているquestIsDoneのクエスト数分の初期値を設定しリストにしています。
Enumerable.Repeatの第1引数が設定する値で、第2引数が個数です。
QuestPrefabは1個のクエスト分のUIでそれをインスタンス化したものをquestUIInstanceListに保持します。
実際のクエストのデータを保持するのはquestListです。
クエスト画面を開いた時にUpdateQuestDataメソッドを呼び出し、クエストが完了しているかどうかのデータを更新します。
QuestManagement2のインスペクタのQuestManagement2スクリプトのQuestPrefabに先ほど作ったQuestPrefabを設定します。
最後にサンプルで作ったCubeに設定したCompleteQuestスクリプトでQuestManagementの部分をQuestManagement2に変更し、インスペクタで設定をし直します。
これで改造は完了です。
QuestUIのインスペクタで名前の横のチェックを外し最初は表示しないようにします。
実際に試してみると、
上のようになります。