【Unity】RPGを作るチュートリアルその46 選択ウィンドウ全体を制御するクラス

【Unity】RPGを作るチュートリアルその46 選択ウィンドウ全体を制御するクラス

シンプルなRPGをUnityで作るチュートリアルシリーズの46回目です。

第45回では魔法やアイテムの選択ウィンドウの制御、ウィンドウ内のUIの制御を行うクラスのうち、UI部分の制御を実装しました。

今回は引き続き選択ウィンドウの実装として、ウィンドウ全体を制御するクラスを作成していきます。

 

 

制作環境

MacBook Pro 2023 Apple M2 Max

Unity6 (6000.0.30f1) Silicon

 

作業内容と順序

シンプルなRPGを作る上でどんな作業が必要か、どんな順番で作っていくと良さそうか、別ページで検討しました。基本的にこの流れに沿って進めていきます。

 

チュートリアルの一覧

このシリーズ全体の一覧は以下のページにまとめています。

 

前回の内容

前回は魔法やアイテムの選択ウィンドウの制御、ウィンドウ内のUIの制御を行うクラスのうち、UI部分の制御を実装しました。

 

UIを制御するクラス

戦闘画面のUIとして、5つのグループを作成しました。

  • ステータス表示のUI
  • 敵キャラクターの名前表示のUI
  • コマンドのUI
  • 選択ウィンドウのUI ◀︎今回はここ
  • メッセージ表示のUI

これらのUIに関しては、ひとつのまとまりをウィンドウとみなして、そのウィンドウ自体を制御するクラスと、ウィンドウ内のUIを制御するクラスの2つをセットにして実装していきます。

今回は選択ウィンドウのUIやウィンドウを制御するクラスを作っていきましょう。また、UIの内部で項目のゲームオブジェクトを制御するクラスについても作っていきたいと思います。

ウィンドウの制御クラス

↓ 指示

UIの制御クラス

↓ 指示

項目の制御クラス

のような流れになっています。

各項目の制御クラスとUIの制御クラスは前回作成したので、今回はウィンドウの制御クラスを作成していきます。ウィンドウの制御に関しては、コマンドで魔法を選んだ時、アイテムを選んだ時、でそれぞれ参照するデータが異なるので、それぞれの領域の処理に分けるためクラスを作っておきたいと思います。

選択ウィンドウの制御クラス …… カーソルの移動や決定ボタン、キャンセルボタンが押された時の動作を中心に実装

選択ウィンドウの魔法担当クラス …… コマンドで魔法を選んだ時に表示する魔法データの管理

選択ウィンドウのアイテム担当クラス …… コマンドでアイテムを選んだ時に表示する魔法データの管理

コマンドに応じた具体的な処理内容を別のクラスに分けておくことで、選択が必要なコマンドが増えた際にも改修する範囲や影響範囲が少なくて済みそうです。

 

選択ウィンドウの魔法担当クラスの作成

まずは選択ウィンドウの魔法担当クラスから作成していきます。このクラスでは、

  • キャラクターが覚えている魔法の情報の保持
  • 項目が選択できるかの確認
  • 魔法の数から最大ページ数を計算
  • ページ内の項目に魔法の名前などをセット
  • 選択された魔法データの取得

といった機能を入れていきます。

Projectウィンドウから「Assets/Scripts/Battle/UI」のフォルダを開き、MonoBehaviourのスクリプトを作成します。名前は [SelectionWindowMagicController] にしました。

スクリプトファイルの作成
スクリプトファイルの作成

 

作成した「SelectionWindowMagicController」の中身は以下のように記載しました。

「_magicIdDictionary」のフィールドは選択項目の位置と、魔法IDを対応させた辞書です。ページ内の位置の0から3までに対して、どの魔法が表示されているかを保持します。

「_characterMagicList」のリストでは、コマンドを選択中のキャラクターが覚えている魔法を保持します。といっても今回の仕様ではキャラクターは主人公ひとりなので、その魔法を保持するようになっています。

IsValidIndex()のメソッドでは、引数のインデックスが「_characterMagicList」のリストに対して有効かを確認しています。選択ウィンドウで扱いたいインデックスとしては、ページ内の位置を保持するページ内インデックスと、リストに対応するインデックスです。このうちリストに対応するインデックスを引数として指定するようにします。

IsValidSelection()のメソッドでは、選択した項目が有効かどうかを確認します。魔法の場合はリスト内に存在することと、消費MPに対して現在のMPが足りているかを確認します。

GetMaxPageNum()のメソッドでは魔法の一覧に応じて、必要なページ数を計算しています。CeilToIntは小数点以下の切り上げをしてくれるメソッドで、たとえば魔法が9個あったとしたら、9 / 4 = 2.25となりますが、切り上げ処理で3となって、最大で3ページとなります。最後のページは1項目だけ表示されるイメージです。

SetCharacterMagic()では対象のキャラクターが覚えている魔法の一覧をキャラクターのステータスから取得しています。取得した結果は「_characterMagicList」のリストに格納しています。

SetPageMagic()のメソッドではページ内の魔法をセットしています。指定されたページに対応する4つまでの魔法の情報を、選択項目にセットしていきます。

CanSelectMagic()のメソッドでは引数の魔法データが選択可能か判断します。消費MPに対して現在のMPが足りているかの判断ですね。

GetMagicData()では指定したインデックスの魔法のデータを返すようにしています。最終的に決定ボタンが押された時などに取得して、次の処理に渡していく流れを想定しています。

 

選択ウィンドウのアイテム担当クラスの作成

続いて選択ウィンドウのアイテム担当クラスを作成していきます。このクラスでは、

  • ページ内のアイテム情報の保持
  • 項目が選択できるかの確認
  • アイテムの数から最大ページ数を計算
  • ページ内の項目にアイテムの名前などをセット
  • 選択されたアイテム情報データの取得

といった機能を入れていきます。魔法がアイテムになっただけで、ほとんど同じような機能を実装していきます。

Projectウィンドウから「Assets/Scripts/Battle/UI」のフォルダを開き、MonoBehaviourのスクリプトを作成します。名前は [SelectionWindowItemController] にしました。

スクリプトファイルの作成
スクリプトファイルの作成

 

作成した「SelectionWindowItemController」の中身は以下のように記載しました。

ざっと眺めていただくとお気づきかもしれませんが、先ほど作成した「SelectionWindowMagicController」とほとんど同じ処理が実装されています。

違う点としては、所持アイテムの一覧は「CharacterStatusManager」で保持していることから、そちらのリストを参照している点です。アイテムが使用できるかどうかの判定は、アイテム情報の所持数を確認しています。

 

 

選択ウィンドウの制御クラスの作成

次に選択ウィンドウの制御クラスを作成していきます。このクラスでは、

  • BattleManagerやUIControllerなどへの参照の保持
  • 魔法担当クラスやアイテム担当クラスへの参照の保持
  • 選択ウィンドウ内の状態の保持
  • セットアップ用のメソッド
  • アイテム選択のキー入力検知メソッド
  • 選択の検証メソッド
  • キー入力に対応したインデックスとカーソルの操作メソッド
  • ウィンドウの表示/非表示を切り替えるメソッド

といった機能を入れていきます。このクラスは「IBattleWindowController」のインタフェースを実装するので、そちらのメソッドも含まれています。魔法用の処理、アイテム用の処理と分割しましたが、それでもボリュームたっぷりになりました。

Projectウィンドウから「Assets/Scripts/Battle/UI」のフォルダを開き、MonoBehaviourのスクリプトを作成します。名前は [SelectionWindowController] にしました。

スクリプトファイルの作成(3回目)
スクリプトファイルの作成(3回目)

 

作成した「SelectionWindowController」の中身は以下のように記載しました。とても長いので、全文を先に紹介しつつ、個別に説明を加えていきたいと思います。

 

項目が選択された場合、ウィンドウ内でキャンセルボタンが押された場合のそれぞれで「BattleManager」の処理を呼び出すようにしています。この処理は後ほど実装するため、今はコンパイルエラーが出る状態になります。

 

フィールド

フィールドに関する部分です。

Inspectorウィンドウから参照をアサインするのは、UIコントローラと選択ウィンドウの各パーツ(魔法、アイテム)です。「BattleManager」に対しては選択された項目の情報を渡したいので、初期化時に渡される参照を保持するようにします。

選択ウィンドウではカーソルを移動させて項目を選択します。選択した項目が論理的にリストの何番目であるのかを「_selectedIndex」のフィールドで保持します。また、現在表示中のページ数についても保持するようにします。

「_canSelect」のフィールドでは項目を選択できるかどうかの状態を保持します。戦闘中のフェーズによる制御だけだと、連打できてしまう可能性もあったため、明示的にフラグを用意して制御するようにしました。

 

初期化とキー入力

初期化のメソッドとキー入力のメソッド部分です。

SetUpController()のメソッドは「IBattleWindowController」のインタフェースで実装を強制されるメソッドです。項目の選択後に「BattleManager」への通知を行いたいので、引数で渡された参照をフィールドに渡します。

Update()からはSelectItem()を呼んで、その中でキー入力を検知します。選択ウィンドウの場合は、上下左右の矢印キー、決定ボタン、キャンセルボタン、といった感じで多くのキー入力を検知する必要があるので、それぞれに対応した処理を呼び出すようにしています。

 

インデックスの検証

キー入力で操作されたインデックスが有効な値かどうかを確認する部分です。

魔法、アイテムのそれぞれでインデックスを確認する先のデータリストが異なるため、コマンドウィンドウで選択されたコマンドに応じて確認先のコントローラを切り替えています。

IsValidIndex()の方はデータリストに対してインデックスが有効な範囲かどうかの確認で、IsValidSelection()の方は選択された項目が実行可能かどうかも確認するようにしています。

 

キー入力の左右の対応

キー入力のうち、左右にカーソルを移動させる処理です。

右キーを入力した際に、現在のカーソルの位置が左側の場合は移動処理を行い、右側の場合は処理を行いません。カーソルが左にいるか、右にいるかの判断はIsLeftColumn()の中で行います。インデックスは0から開始するため、インデックスの値を2で割って余りが0なら左側、1なら右側と判断します。

左キーを入力した場合は、カーソルが右側にいる場合に処理を行います。

 

キー入力の上下の対応

キー入力のうち、上下にカーソルを移動させる処理です。

こちらも左右の時と似たような感じで、カーソルの現在いる位置に応じて処理を分けています。例えば上の行にいる場合、前のページが存在するなら前のページを表示、存在しなければ移動しない、といった動きになります。

上の行かどうかの判定は若干力技で、インデックスを4で割った余りを求めて、1以下だったら上の行であると判断します。インデックスでみると、上の行の左から右に向かって0, 1となっていて、下の行の左から右に向かって2, 3と割り振ってあることから、1以下なら上の行であると判断できます。

最大ページ数の取得はコマンド選択しているのが魔法とアイテムで呼び出し先を変えています。

 

インデックスに応じたカーソルの表示

キー入力後、インデックスに対応する位置にカーソルを表示する処理です。

こちらはデータに対応するインデックスを4で割った余りを求め、ページ内のインデックスを計算しています。ページ内の対応する項目のカーソルを表示するため、UIの制御クラスにページ内インデックスを渡します。

 

ウィンドウ内の項目をセットアップする処理

ウィンドウ自体のセットアップや、ページ内の項目を表示する処理です。

ウィンドウ自体をセットアップするSetUpWindow()のメソッドを用意します。こちらはインタフェースで定義されるコントローラ自体のセットアップとは別のタイミングで呼ばれる想定です。というのは、選択ウィンドウではキャンセルして前のフェーズに戻るケースがあるため、選択ウィンドウを表示するタイミングでウィンドウの内容をセットできるメソッドを用意しています。

InitializeSelect()では選択状態を初期化しています。もしカーソルの位置を記憶する場合は、魔法とアイテムそれぞれで位置を記憶して表示する仕組みを入れておくと良いかと思います。

SetPageElement()では魔法とアイテムでそれぞれ対応するメソッドを呼び出します。また、ページ数に応じて、前のページ、次のページがあることを示すページャーの表示を切り替えています。

 

決定ボタン、キャンセルボタンが押された時の処理

決定ボタンやキャンセルボタンが押された時の処理です。

OnPressedConfirmButton()は決定ボタンが押された時の処理で、コマンドに応じて分けています。魔法の場合は魔法のIDを、アイテムの場合はアイテムのIDを「BattleManager」に伝えて、ウィンドウを非表示にしています。

OnPressedCancelButton()はキャンセルボタンが押された時の処理で、「BattleManager」にキャンセルボタンが押されたことを伝えて、ウィンドウを非表示にしています。

 

その他3つのメソッドは書いてあるそのままなので説明は省略します。

 

既存のスクリプトの変更

BattleWindowManagerの変更

ウィンドウを追加したので、戦闘画面のウィンドウを管理するクラスに「SelectionWindowController」に関する処理を追加していきます。

今回も、

  • フィールドの追加
  • リストに追加するコントローラの追加
  • 参照を取得するメソッドの追加

を行いました。

 

BattleManagerの変更

「SelectionWindowController」から情報を渡してもらうため、「BattleManager」も修正していきます。

  • 魔法またはアイテムのコマンドが選択されたら選択ウィンドウを開く
  • 選択ウィンドウで選択された結果を受け取る
  • 選択ウィンドウのキャンセル通知を受け取る

といった部分を追加していきましょう。

今回選択ウィンドウを開くにあたっては、コルーチンを使うので冒頭に「using System.Collections;」を追加しています。

 

以前追加したHandleCommand()のメソッドの中身を変更しつつ、それ以降にメソッドを追加しています。説明はコードの後に。

HandleCommand()では、選択したコマンドに応じて処理を分けています。選択ウィンドウを表示する対象の魔法とアイテムの場合は選択ウィンドウの処理へ進み、攻撃と逃げるの場合は、コマンド選択をさせないようにフェーズを変更して処理を止めています。

ShowSelectionWindow()では、選択ウィンドウを表示するためのコルーチンを起動しています。コルーチンを使うのは1フレーム遅延させたいためなのですが、なぜ遅延させたいかというと、コマンド入力が完了したフレームと同じフレームで選択ウィンドウで決定キーが押せる状態にしてしまうと、選択ウィンドウ側でも決定キーが押されたものとみなされるためです。Input.GetKeyDownなどは、そのフレーム内でキー入力があったかどうかを検知するため、コマンド入力のために決定キーを押す→選択ウィンドウのUpdate()のタイミングで入力のif文を確認→同一フレームで入力があったので選択ウィンドウでも決定キーが押されたと判断、といった流れになります。そのため、コルーチンを使ってフレームをずらすことで、同一フレーム内で2つの画面で入力を検知してしまうことを防いでいます。

ShowSelectionWindowProcess()のコルーチンの中では、戦闘のフェーズを変更した後にウィンドウのセットアップ処理を呼んでいます。

OnItemSelected()では引数で渡されたIDをもとに、主人公の行動を設定していく処理に進みます。今回の時点ではコンソールにメッセージを表示するだけに留めておきましょう。

OnItemCanceled()では選択ウィンドウでキャンセルボタンが押された旨を通知してもらって、戦闘のフェーズをコマンド入力に戻しています。コマンド入力画面ではキャンセルボタンの検知をしていないため、同一フレームでも問題なく処理を分けられます。

 

スクリプトのアタッチ

スクリプトを保存したら、ゲームオブジェクトを作成してスクリプトをアタッチしましょう。

 

SelectionWindowControllerのアタッチ

Hierarchyウィンドウから「BattleWindowManager」の子オブジェクトとして、空のゲームオブジェクトを作成します。名前は [SelectionWindowController] にしました。続けて「SelectionWindowController」の子オブジェクトとして、空のゲームオブジェクトを2つ作成し、それぞれ [SelectionWindowMagicController][SelectionWindowItemController] に名前を変更しました。

ゲームオブジェクトの作成 * 3
ゲームオブジェクトの作成 * 3

 

先に子オブジェクトの方からスクリプトをアタッチしていきます。「SelectionWindowMagicController」のゲームオブジェクトには[SelectionWindowMagicController]をアタッチします。

魔法担当
魔法担当

 

同様に「SelectionWindowItemController」のゲームオブジェクトには[SelectionWindowItemController]をアタッチします。

アイテム担当
アイテム担当

 

次に「SelectionWindowController」のゲームオブジェクトに戻って、[SelectionWindowController] のスクリプトをアタッチします。参照もそれぞれアサインしておきます。

アタッチと参照のアサイン
アタッチと参照のアサイン

 

BattleWindowManagerへの参照のアサイン

続いて「BattleWindowManager」のゲームオブジェクトにて、Inspectorウィンドウから「SelectionWindowController」への参照をアサインします。

ウィンドウへの参照のアサイン
ウィンドウへの参照のアサイン

 

動作確認

ここまでの設定を終えたら動作確認をしてみます。ゲームを実行して、「BattleTester」のスクリプトにて「Execute Battle」にチェックを入れて戦闘を開始します。戦闘開始後、魔法またはアイテムのコマンドを選択後、選択ウィンドウが表示されればOKです。

* 選択できる魔法がありません! *
* 選択できる魔法がありません! *

 

「BattleTester」のInspectorウィンドウで主人公のレベルを1にしている場合は、まだ魔法を習得していないので使える魔法がなく、キャンセルボタンを押すだけになります。また、同じく「BattleTester」のInspectorウィンドウでパーティの所持アイテムを設定していない場合はアイテム選択画面でキャンセルボタンを押すだけになります。

というわけで以下のように設定を行なって、再度ゲームを実行してみましょう。

テスト戦闘の設定
テスト戦闘の設定

 

主人公のレベルを上げることで、以下のように魔法が表示されるようになります。魔法の説明が説明テキストエリアに表示されることも一緒に確認しておきましょう。魔法を選択すると、コンソールに魔法のIDが表示されることも確認するとグッド。

魔法が表示された
魔法が表示された

 

チュートリアルの中では魔法数やアイテム数が少ないのですが、もし余力があれば、魔法の定義データ数を増やしたり、アイテムの定義データ数を増やしたりして、ページ送りも確認しておくとより拡張がしやすくなるかもしれません。一応カーソル移動やページ送りなどの動作は確認してあるのですが、もしバグを見つけたらこっそり教えてください。こっそり修正します。

ここまでの確認ができたら今回は完了です。

 

今回のブランチ

 

まとめ

今回は選択ウィンドウの実装として、ウィンドウ全体を制御するクラスを作成しました。いやー長くなってしまいました。区切りのポイントが難しかったのと、やることが多かったのとで今回は非常に長くなりましたね。お疲れ様でした。

次回はメッセージウィンドウに関する実装として、戦闘中のメッセージ定義を先にやっておきましょう。

     

ゲーム開発の攻略チャートを作りました!

CTA-IMAGE

「ゲームを作ってみたいけど、何から手を付けていいか分からない!」


そんなお悩みをお持ちの方向けに、todoがアプリをリリースした経験を中心に、ゲーム作りの手順や考慮すべき点をまとめたe-bookを作成しました。ゲーム作りはそれ自体がゲームのように楽しいプロセスなので、「攻略チャート」と名付けています。


ゲームを作り始めた時にぶつかる壁である「何をしたら良いのか分からない」という悩みを吹き飛ばしましょう!