今回はメニューの基礎部分の設計に挑戦しました。
これまで使用していた「Player」という言葉ですが、Unity上では複数の意味で使われることが分かりました。そのため、今回から、「ゲームをプレイする人」が操作するゲーム上のキャラクターを「操作キャラ」と呼ぶことにしました。また操作キャラの状態やコマンド表示を「操作キャラの状態表示」および「ゲームコマンド」および「メニュー」と呼ぶことにします。
設計課題
最初に、メニューを設計する時の課題についてまとめます。
メニューの追加に当たって検討しないといけないのは、以下の2点です。
1. 「ゲームコマンド」を表示している間は「操作キャラ」を動かさない。
2. 「ゲームコマンド」および「メニュー」も階層化が必要。
つまり、キーを受け付ける対象を、ゲームコマンド や メニュー が表示されている状態に応じて切り替える必要があります。これを実現するために、第12回で使用した Player Input
を 操作キャラ の外に置く必要があります。また、ゲームコマンド や メニュー など、複数の「ウィンドウ」のうち、どのウィンドウがキーを受け付ける対象として「選択されている」のかを切り替える必要があります。
状態に応じてどのオブジェクトにキーを渡すかを切り替える手段として、Unity では Event Systems
と RayCast
があります(2つはセットで使用します)。しかし、今回は Event Systems
と RayCast
の機能は使用しないことにしました。詳しくは別の記事にしますが、Input Manager/Input System
と Event Systems
ではキーを渡すタイミングに違いがあって、Input Manager/Input System
はキーを離した時のイベントを通知できますが、Event Systems
はキーを離した時のイベントが処理できません。そのため、キーの受信にEvent Systems
を使用すると、操作感の違いを埋める必要があります。やりようはあると思うのですが、時間がかかりそうだったので、一旦 Input System
だけで作ることにします。ただ、最低限の移植性は検討します。
※Event Systems
は Canvas
を始めて使う時に実装されます。ここでは、Event Systems
が提供するイベントハンドラを使ったキーの受信方法を採用しないという意味です。
参考(EventSystems と RayCast)
Event Systems
では、画面でマウスクリックをした時に、その座標上にどれだけのオブジェクトが重なっているかを、手前から奥へと走査する機能 RayCast
を使って、オブジェクトの重なりと、その結果どのオブジェクトが選択されたかを計算する機能があります。RayCast
はちょうど光線を画面の手前から奥へと発射するイメージで、この光線に当たったオブジェクトがどういう順番に、いくつ当たったか、どれが一番手前かを操作する機能です。この計算のために、RayCast
の対象となるオブジェクトにはすべて当たり判定(Collider
)を使用します。将来的にタッチパッド(iPhone、androidなど)に対応する時に、この機能が必要になるかもしれませんが、今の段階では冗長なので、今回はその「重なり」を表現する手段としてスタック(Stack
)を使用します。
キー入力を受け付けるオブジェクトを変更する
第12回で、「操作キャラ」に入力処理(Player Input
)を付けました。しかし、「ゲームコマンド」や「メニュー」を操作している間は、「操作キャラ」に入力処理があるのは都合が悪いことが分かりました。「操作キャラ」に Player Input
が付いていなくても、他のオブジェクトから Player Input
と同じタイミングで同じ処理を入れてあげれば、「操作キャラ」は操作性を損なわずに Player Input
だけを「操作キャラ」の外に分離できます。このPlayer Input
を引き受けるオブジェクトをEmpty
で実現します。
キーマップ
第12回で作成した、以下のActionMap(Input System)
を使用します。
キー入力を受け付けるオブジェクトを作成する
(1) Hierarchyウィンドウで右クリック > Create Empty
(2) オブジェクトの名称を変更(Hierarchyウィンドウでオブジェクト選択 > Inspectorウィンドウ上で名称を変更)
(ここでは Input UI
にしました。)
キー入力を受け付けるオブジェクトから操作キャラにアクセスさせる
ここは力技ですが、先ほどのEmpty
オブジェクトのキーイベント処理から、もともと操作キャラ側に実装されていたキーイベント処理を呼び出します。
キー入力側(コード)
キー入力用のコードは以下のようにします。
コード
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.InputSystem; public class InputUIManager2 : MonoBehaviour { // ターゲットとなる操作キャラ [SerializeField] GameObject player; private PlayerControl2 playercs; // キー入力 private Vector2 input; // Start is called before the first frame update void Start() { playercs = player.GetComponent<PlayerControl2>(); } // 新Input System (入力イベントで取得) void OnNavigate(InputValue value) { input = value.Get<Vector2>(); if(playercs != null){ playercs.Move(input); } } // Update is called once per frame void Update() { } }
操作キャラ側(コード)
操作キャラのコードは以下のように修正しました。
コード
using System.Collections; using System.Collections.Generic; using UnityEngine; public class PlayerControl2 : MonoBehaviour { // キー入力 private Vector2 input; // 移動 private float speed; private Rigidbody2D rigidBody; // アニメーション private Vector2 scale; private Animator animator; // Start is called before the first frame update void Start() { speed = 0.1f; rigidBody = GetComponent<Rigidbody2D>(); animator = GetComponent<Animator>(); } // Update is called once per frame void Update() { } public void Move(Vector2 v) { input = v; } void FixedUpdate() { // 移動 if (input == Vector2.zero){ return; } rigidBody.position += input * speed; // キャラクターの左右反転 scale = this.transform.localScale; if( input.x > 0 ){ scale.x = 1; } else if( input.x < 0 ){ scale.x = -1; } this.transform.localScale = scale; // アニメーション方向決定 animator.SetFloat("dirX", input.x); animator.SetFloat("dirY", input.y); } }
Player Input
からの入力部(OnMove()
)を、外部からの入力用のインタフェース(Move()
)に変更しました(Move関数は自作関数です)。引数にInput.Value
を使わずVector2
だけを受け取るようにしたことで、操作キャラ側にはInputSystem
のインクルード(using
)も不要になりました。
キー入力用のオブジェクトに操作キャラをリンクする
(1) Hierarchyウィンドウから、キー入力用のオブジェクト (Input UI
) を選択
(2) Inspectorウィンドウで、C#スクリプトの位置までスクロールする
キー入力側のコンパイルが完了していたら、操作キャラ側のコードをリンクできる状態になっています。
(3) Hierarchyウィンドウから、操作キャラのゲームオブジェクトを (2) にドラッグ&ドロップ
入力側が操作キャラをわざわざGameObject
で取得して、そこからGetComponent()
でC#スクリプトを取り出すのは、少し冗長に感じるかもしれません。これは、キー入力の拡張を検討した結果になります。
この部分を考えるために、Player Input
について少し調べました。 Player Input
は1人のゲームプレイヤーを代表していて、1人用のゲームでは1つだけ存在できます。2人以上のマルチプレイでは、Player Input
はプレイ人数分だけ作ります。その場合、Player Input
と 操作キャラ は複製できる必要があります。この時、例えば1Pの操作キャラと2Pの操作キャラは、見た目が違うかもしれませんし、もしかすると能力や属性も違うかもしれません。もし上記のようにしておけば、見た目が違う場合は、複製したゲームオブジェクトのAnimator
を変更したら済みます。能力や属性も違うのなら、C#スクリプトのクラス名を Interface
に変えると、入力部分を変更することなく、キャラを複製できます。これは、RPGでシーン毎に操作キャラを変える場合や、格闘ゲームで操作キャラを選択するなどの対応をする時に役に立ちます。
操作キャラの Player Input を消す
(1) Hierarchyウィンドウから、操作キャラのゲームオブジェクトを選択
(2) Inspectorウィンドウから、Player Input
を見つける
(3) Player Input
の名称が表示されている領域で右クリック > Remove Component
メニューを表示する
メニューの表示には、Canvas
> Panel
を使用します。そのうちに見栄えするデザインにしたいですが、今回は動作重視で行きます。メニューの仕様は以下の通りです。
第11回にある通り、基本的な操作は Cancel
と Submit
の2つのボタンにします。
フィールド
キー | 機能 |
---|---|
十字キー (WASD) | キャラクターを動かす |
Submit (E) | 足元または前方を調べる |
話しかける | |
アイテムを使用する | |
Cancel (Q) | メニューを表示する |
メニュー
キー | 機能 |
---|---|
十字キー (WASD) | コマンドやアイテムを選択する |
Submit (E) | メニューを一つ進める(確定) |
Cancel (Q) | メニューを一つ戻る(キャンセル) |
パネル順序管理の状態遷移
最小状態での順序は以下になります。
第11回の観察から、最小のRPG要素では、キャラクター1名、ステータスは3要素(体力、攻撃、速さ)、やり込み要素は道具のみのため、道具表示→使う選択(または装備する選択)→効果表示、の3層で表現できそうです。
ただ、今後やり込み要素を追及すると、この階層は深くなります。キャラクターの追加、ステータスの追加、保持する道具の最大値、戦局に影響する道具の細分化(魔法、技など)、その他の次元で必要なパラメータ、など。そのため、この階層を一旦抽象化します。(抽象化=目的の抽出)
パネルを追加する
(1) Hyerarchyウィンドウで右クリック > Create Empty
(2) Hyerarchyウィンドウで右クリック > Create
> UI
> Panel
で パネルを3枚作成
(3) パネルの名前は、それぞれ"ListItem", "QueryUse", "Description" としました。
先ほど追加したキー入力(InputUI
)と、今回追加したパネル。画像では余分なパネルも追加されています。
パネル順序管理(コード)
コード
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.InputSystem; public class InputUIManager : MonoBehaviour { // ターゲットとなる操作キャラ [SerializeField] PlayerControl2 player; // 一番最初のメニュー [SerializeField] Window root; // 一番最後のメニュー(ターミネータ) [SerializeField] Window terminate; // メニューのスタック private Stack<Window> windows; // キー入力 private Vector2 input; // Start is called before the first frame update void Start() { // 操作キャラの初期化 if(player == null){ player = GetComponent<PlayerControl2>(); } // スタックの初期化 windows = new Stack<Window>(); windows.Clear(); } void AddWindow(Window w) { Debug.Log("InputUIManager: AddWindow"); windows.Push( w ); w.SetActive( true ); } void RemoveWindow(Window w) { Debug.Log("InputUIManager: RemoveWindow"); w.SetActive( false ); windows.Pop(); } void ClearAllWindows() { Window w; Debug.Log("InputUIManager: ClearAllWindows"); while( windows.Contains(root) ){ w = windows.Pop(); w.SetActive(false); } } // 新Input System (入力イベントで取得) void OnNavigate(InputValue value) { input = value.Get<Vector2>(); Debug.Log("InputUIManager: " + input); if( windows.Contains(root) ){ // メニュー状態 Window w = windows.Peek(); w.Navigate(input); } else { // キャラ操作状態 if(player != null){ player.Move(input); } } } void OnSubmit() { Debug.Log("InputUIManager: OnSubmit"); if( windows.Contains(root) ){ // メニュー状態 Debug.Log("Window contains root"); Window w = windows.Peek(); if( w == terminate ){ ClearAllWindows(); } else { AddWindow( w.Next() ); } } else { // フィールドでのキャラ操作(未実装) Debug.Log("Window does not contains root"); } } void OnCancel() { Debug.Log("InputUIManager: OnCancel"); if( windows.Contains(root) ){ // メニュー状態 Window w = windows.Peek(); if( w == terminate ){ ClearAllWindows(); } else { RemoveWindow(w); } } else { // キャラ操作→メニュー(Root) AddWindow( root ); } } // Update is called once per frame void Update() { } }
コードが大きくなる時は、キー入力、キーを受けた時の処理、画面表示は3層に分けることが多いと思いますが、今の段階ではコードが小さいので、キー入力の中にキーを受けた時の処理までまとめて記述します。主に以下の点に注意して作りました。
1. パネルは何層あるか分からなくても良い作りにするので、最初(root
)、最後(terminate
)、中間(layer
)だけを意識します。
2. キャラを操作する状態か、パネルを操作する状態かを区別するのは、最初のスタックであるroot
があるかどうかで見分けます。また、最後のスタックであるterminate
がある場合、一旦すべてのスタックを開放します。(パネルの表示・非表示も忘れずに行います)
3. スタックはUnity C#で用意されたインタフェースです。スタックへの追加はPush()
、取り出しはPop()
、スタックを取り出さずに参照する処理はPeek()
を使います。
4. パネルの表示・非表示にはSetActive(bool)
、パネルの操作はInput Action Map
> UI
で定義されたインターフェースに似せたNavigate()
を定義しました。
5. パネルは何層あるか分からないので、次のパネルの取り出しは、パネル側の実装にします。パネル側に、Next()
インターフェースを実装します。
パネル(コード)
コード
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Window : MonoBehaviour { // 次のウィンドウ [SerializeField] Window windowNext; // Start is called before the first frame update void Start() { } public void Navigate(Vector2 value){ // リストの移動 } public void SetActive(bool b){ gameObject.SetActive(b); } public Window Next(){ // リスト要素毎にNextを持つ。その種類のWindowNextを返す。 // 今はwindowのテストのため、windowNextをそのまま返す return windowNext; } // Update is called once per frame void Update() { } }
今の段階で、パネル側はあまりすることが無く、表示・非表示の切り替えと次のパネルを渡す処理だけを実装しました。
パネルを関連させる
パネルの実装が終わると、Next
を要素として持たせることができるので、先ほどのListItem
、QueryUse
、Description
をその順に設定します。
InputUI
:root
= ListItem, terminate
= Description
ListItem
:next
= QueryUse
QueryUse
:next
= Description
Description
:next
= None
最後に、全てのパネルをデフォルトで非表示にします。
(1) Hierarchyウィンドウから、非表示にするパネルを選択
(2) Inspectorウィンドウで、パネル名の左のチェックを外します。
これで、道具などを表示をするため土台ができました。表示位置は次回以降に調整するため、今回は適当です。以下のキャプチャーでは、イメージを付けるために、パネルにテキストを入れています。
後記
新しいInput System
に関する記事がまだ少ないので、参考のために以下を残しておきます。
次にやること
アイテムのリスト化