【Unity】RPGを作るチュートリアルその81 ゲームの進行フラグの管理機能を実装

【Unity】RPGを作るチュートリアルその81 ゲームの進行フラグの管理機能を実装

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

第80回ではイベント機能について実装の方針をまとめました。

今回はイベントの発生条件などで使用するフラグを管理するための機能を実装していきます。

 

 

制作環境

MacBook Pro 2023 Apple M2 Max

Unity6 (6000.0.30f1) Silicon

 

作業内容と順序

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

 

チュートリアルの一覧

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

 

前回の内容

前回はイベント機能について実装の方針をまとめました。

 

フラグの管理機能

イベントの実行においては、ゲーム内でどんな状況になっているかに応じて処理を分けていきます。例えば、宝箱からアイテムを入手したら次は入手できないようにする、特定のボスを倒したかどうかで処理を分ける、なんて感じです。この状態を保持する変数は多くの場合bool値で表現され、「フラグ」と呼ばれています。

フラグによって実行するイベントを切り替えるため、フラグの状態を一括で管理するクラスを用意して、そこを参照するようにしていきます。

フラグの状態を保持するにあたって、フラグ名と状態をセットで保持するクラスも用意します。フラグ管理のクラスにて、フラグの状態クラスのリストを作って、フラグ名で状態を取得できるようにします。この情報はセーブファイルに保存するため、[Serializable]の属性をつけておきます。

また、フラグ名の一覧を開発者側で確認できるように、フラグ名を定義するクラスも作成します。このクラスを使ってフラグ名の一覧データをScriptableObjectの形で出力し、ゲームの実行中にフラグ名の一覧をリストにセットできるようにします。定数クラスからの値の取得はリフレクションを使う予定で、ゲームの実行中にリフレクションを使うことによるエラーや速度低下を避けるため、間にScriptableObjectを挟むようにしたいと思います。この機能はエディタ拡張を使って実装予定です。なるべく拡張しない方向で考えていましたが、負担が大きくなさそうなところでは数を絞って導入していきます。

ゲーム開始時に全てのフラグ名が分かっている状態にしておくことで、ゲームの実行中にフラグの状態を確認したり、状態を切り替えるデバッグ機能を作りやすくなります。

ゲームの実行中のフラグの状態についてはセーブファイルに保存するので、その部分も実装していきます。対応するクラス自体は作ってあるので、実際の処理を追記する形になります。

 

フラグの管理機能の実装

上記の方針に従って、フラグの管理機能を作っていきましょう。実装する順番としては、

  • フラグの状態を保持するクラス
  • フラグを管理するクラス
  • フラグ名を定義するクラス
  • フラグ名一覧を保持するScriptableObject
  • 定義したフラグ名をScriptableObjectとして保存するエディタ拡張のクラス

で進めていきたいと思います。

 

フラグの状態を保持するクラス

まずはフラグの状態を保持するクラスから作成していきます。このクラスでは、フラグ名と状態をペアで保持するようにします。

Projectウィンドウの「Assets/Scripts」のフォルダを開き、新しくフォルダを作成します。名前は [Flag] にしました。

フォルダの作成
フォルダの作成

 

作成した「Flag」のフォルダに移動し、空のスクリプトファイルを作成します。名前は [FlagState] にしました。

フラグの状態を保持するクラス
フラグの状態を保持するクラス

 

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

フラグの名前と状態をセットで保持するようにしています。フラグの状態をファイルに保存するため、[Serializable]を忘れずにつけておきます。

 

フラグを管理するクラス

次にフラグを管理するクラスを作成します。このクラスでは、上の「FlagState」をリストとして保持します。また、指定した名前のフラグの状態を返すメソッド、フラグの状態をセットするメソッドも作成します。

Projectウィンドウから「Assets/Scripts/Flag」のフォルダにて、MonoBehaviourのスクリプトファイルを作成します。名前は [FlagManager] にしました。

フラグを管理するクラス
フラグを管理するクラス

 

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

フラグ名の定義データのScriptableObjectに対する参照や、フラグの状態を保持するクラスへのリストをフィールドとして用意しました。「FlagNameData」のクラスは後ほど定義していきます。

ScriptableObjectについては、他の定義データと同様にAddressablesからロードする形にしています。ロードが完了したと想定される5フレーム目に、フラグリストの初期化処理を行います。この初期化処理では、ScriptableObjectに記載されたフラグ名をリストにセットするようにしています。

GetFlagState()のメソッドではフラグ名を指定することで、その状態を取得します。SetFlagState()のメソッドでは指定したフラグの状態をセットします。

GetFlagStateList()ではフラグの状態リストをそのまま返します。このメソッドはデータをセーブする際に使用する想定です。

※次の回を書いているときに気付きましたが、Update()の中でフレームカウントを確認するつもりがフラグリストのカウントを確認していました。次回この部分の修正を行います。

 

フラグ名を定義するクラス

続いてフラグ名を定義するクラスを作成していきます。Projectウィンドウから「Assets/Scripts/Consts」のフォルダに移動し、空のスクリプトファイルを作成します。名前は [FlagNames] にしました。

フラグ名の定義
フラグ名の定義

 

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

使用する想定のフラグ名を定義していきます。後ほどエディタ拡張でリフレクションを使って値を取得するため、constで定義しています。

イベントを作成していく中で必要なフラグが出てきたら追加していきましょう。

 

フラグ名一覧を保持するScriptableObject

フラグ名一覧を保持するためのScriptableObjectも作成します。こちらは定義データとして作成するため、Addressablesにも登録予定です。

Projectウィンドウから「Assets/Scripts/Data」のフォルダに移動し、空のスクリプトファイルを作成します。名前は [FlagNameData] にしました。

フラグ名を保持するデータ
フラグ名を保持するデータ

 

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

このScriptableObjectはシンプルで、フラグ名の一覧を保持するだけです。ScriptableObjectのアセット自体はメニューから作れるようにしておきたいと思います。パスは他の定義データと同じようにしましょう。

 

定義したフラグ名をScriptableObjectとして保存するエディタ拡張のクラス

フラグ名の一覧をScriptableObjectにするため、エディタ拡張のクラスも作成していきます。ScriptableObjectのInspectorウィンドウにボタンを表示して、「FlagNames」で定義した名前を自動的に登録するようにします。

エディタ拡張のクラスを作成する際は「Editor」という名前のフォルダの下に配置します。このフォルダ名することでエディタ用のスクリプトファイルだと認識され、ビルド時に含まれないようになります。

 

Projectウィンドウから「Assets」のフォルダに移動し、新しいフォルダを作成します。名前は [Editor] にしました。

フォルダの作成
フォルダの作成

 

作成した「Editor」のフォルダに移動し、空のスクリプトファイルを作成します。名前は [FlagNameEditor] にしました。

エディタ拡張のスクリプト
エディタ拡張のスクリプト

 

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

エディタの機能を使うので、「using UnityEditor;」を追加しています。また、リフレクションを使うので「using System.Reflection;」も入れています。

「Editor」を継承することで、エディタの描画時の処理をoverrideすることができます。このクラス内ではOnInspectorGUI()のメソッドを上書きして、Inspectorウィンドウを描画するときの処理をカスタマイズしています。

また、「CustomEditor」の属性をつけることで、対象となるクラスを指定できます。ここでは「FlagNameData」が対象なので、属性の引数で指定しています。

var data = target as FlagNameData;」の行では、表示しているUnityのObjectを「FlagNameData」にキャストしています。表示しているObjectは、Projectウィンドウで選択中の「FlagNameData」のScriptableObjectです。

GUILayout.Button()はボタンを表示するメソッドで、このボタンを押すことでフラグ名の定義をリストにする処理を呼びます。ボタンが押されたタイミングで戻り値がTrueとなり、if文による分岐で処理が実行されるようになっています。

GUILayout.Space()はスペースを表示するメソッドです。

DrawDefaultInspector()は通常のInspectorウィンドウを描画するメソッドです。規模が大きくなるとリストが長くなり、その下にボタンを配置するとスクロールが必要になるため、通常の表示の上にボタンを表示するようにしています。

SetFlagNameList()のメソッドでは「FlagNames」のフィールドを読み取って、「FlagNameData」のリストに追加しています。typeof(FlagNames)で型を指定した後、GetFields()のメソッドでフィールドを取得しています。引数では取得するフィールドの条件を指定しています。戻り値が配列になるので、それをforeach文でループ処理して中身を確認しています。

FieldInfoはフィールドの情報を持っている型で、「IsLiteral」は値がコンパイル時に書き込まれるものであるかを返すプロパティです。定義なのでゲーム実行中に値が変わらないようにしたいことからconstでフィールドを作っているので、それが対象になるように「IsLiteral」の時にフラグ名リストに追加するようにしています。

EditorUtility.SetDirty()のメソッドは編集中のオブジェクトに変更があったことをマークするメソッドで、保存の対象になります。これにより、AssetDatabase.SaveAssets()のメソッドで変更が保存されます。

 

既存のクラスの変更

作成したクラスに合わせて、既存のクラスも変更を加えていきます。変更したいクラスは以下の通りです。

  • AddressablesLabels
  • SaveInfoFlag
  • SaveInfoFlagController

 

AddressablesLabelsの変更

フラグ名の定義データをロードできるようにしたいので、Addressablesのラベル名の定義を追加します。

既存の「Map」のフィールドの下に「Flag」のフィールドを追加しました。

 

SaveInfoFlagの変更

「SaveInfoFlag」のクラスではフラグの状態を保持するクラスのリストを追加します。クラス自体はセーブ機能の実装の際に作っておいたので、その中身を記載していきます。

 

SaveInfoFlagControllerの変更

「SaveInfoFlagController」のクラスでは、「FlagManager」から現在の状態を取得するメソッドと、ロードしたセーブの状態を「FlagManager」にセットするメソッドを追加します。こちらも以前作っておいたクラスの中身を記載していきます。

GetSaveInfoFlag()のメソッドでは「FlagManager」からフラグの状態リストを取得してセーブ用のクラスにセットします。

SetSaveInfoFlag()のメソッドでは逆に、セーブ用のクラスからフラグの状態リストを取得してセットします。このメソッドが呼ばれた場合はまずフラグの状態リストを初期化して、そこからセーブデータのリストを読み取って状態をセットしていきます。これはフラグ名の変更があった場合に、定義データとセーブデータとの不整合が発生する可能性があるためです。なるべくなら定義データ側ですでに存在しているフィールドは削除しないのが望ましいのですが、万一削除されても対応できる余地を残しています。

 

定義データをAddressablesに追加

フラグ名の定義データを追加したので、Addressablesのグループに追加しましょう。まずは定義データから作成していきます。Projectウィンドウから「Assets/Data」のフォルダに移動し、[Create] -> [Scriptable Object] -> [SimpleRpg] -> [FlagData] を選択します。

ScriptableObjectの作成
ScriptableObjectの作成

 

「Addressables Groups」のウィンドウを表示し、新しくグループを追加します。名前は [Flag] にしました。

Addressablesのグループを追加
Addressablesのグループを追加

 

Projectウィンドウから先ほど作成した「FlagData」をドラッグ&ドロップし、コンテキストメニューの [Simplify Addressable Name] を選択して名前を省略表示します。また、ラベルのプルダウンから「Manage Labels…」を選択し、 [Flag] のラベルを追加後、ラベルとして選択します。

アセットの追加とラベルの追加
アセットの追加とラベルの追加

 

フラグ名の定義をリスト化

続いて、フラグ名の定義クラスからScriptableObjectのリストを作成します。「FlagData」のファイルを選択し、Inspectorウィンドウから [フラグ名データリストの作成] のボタンをクリックして「Flag Names」のリストに値が入ることを確認します。画像ではボタンを押した後にリストに追加された状態になっています。

一覧を作成するボタン
一覧を作成するボタン

 

もしボタンが表示されていない場合は、コンソールのメッセージを確認しつつ、「FlagNameEditor」のクラスでCustomEditorの属性がついているかどうかなどからチェックしてみてください。

 

スクリプトのアタッチ

ゲームの実行中にフラグ関連の機能を使えるように、スクリプトをアタッチしていきます。

スクリプトをアタッチするためのゲームオブジェクトを作成します。Hierarchyウィンドウから「Managers」の下に空のゲームオブジェクトを作成します。名前は [FlagManager] にしました。

フラグ管理用のゲームオブジェクト
フラグ管理用のゲームオブジェクト

 

作成した「FlagManager」を選択した状態でInspectorウィンドウにて「FlagManager」のスクリプトをアタッチします。

スクリプトのアタッチ
スクリプトのアタッチ

 

Hierarchyウィンドウから「SaveInfoFlagController」のゲームオブジェクトを選択し、Inspectorウィンドウから先ほど作成した「FlagManager」のゲームオブジェクトをアサインします。

ゲームオブジェクトのアサイン
ゲームオブジェクトのアサイン

 

今回は動作確認は行いませんが、ゲームを実行してAddressablesのロード周りでエラーが出ていないことを確認しておくと安心です。Addressablesのラベルの定義の追加漏れや、グループやファイルの追加漏れを検知することができます。

 

今回のブランチ

 

まとめ

今回はイベントの発生条件などで使用するフラグを管理するための機能を実装しました。デバッグ時にフラグ名を確認できるように定義クラスからScriptableObjectへデータを追加するエディタ拡張も作成したので、これもうまく使っていきたいと思います。

次回はゲームの実行中にフラグの状態を出力する機能、フラグの状態を切り替える機能を実装したいと思います。

     

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

CTA-IMAGE

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


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


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