i-school - 2Dタワーディフェンス 発展1
 味方のキャラの各データについては、スクリプタブル・オブジェクトを作成して、そちらにあるデータを利用してゲーム内に反映していく機能を実装しました。

 これを同じように、敵キャラについても新しくスクリプタブル・オブジェクトを作成して、敵ごとのデータをデータベースであるスクリプタブル・オブジェクトから参照できるようにしていきます。

 以下の内容で順番に実装を進めていきます。

発展1 −エネミー用のデータベース作成−
 1.エネミー用のデータベースとして利用するスクリプタブル・オブジェクトを作成する準備を行う −EnemyType スクリプトと EnemyDataSO スクリプトを作成するー
 2.EnemyDataSO スクリプトを利用して EnemyDataSO スクリプタブル・オブジェクトを作成し、エネミーのデータを登録する



 新しい学習内容は、以下の通りです。

 ・スクリプタブル・オブジェクトの作成とデータの登録(復習)
 ・クラス内に別のクラスを作成する(入れ子クラス)
 ・List の初期化(復習)
 ・[System.Serializable(シリアライザブル)]属性(復習)
 ・入れ子クラスを外部のクラスで宣言する方法



1.エネミー用のデータベースとして利用するスクリプタブル・オブジェクトを作成する準備を行う −EnemyType スクリプトと EnemyDataSO スクリプトを作成するー

1.設計


 手順を示しますが、よろしければ以前学習したことを振り返りながら、是非、自分で実装に挑戦してみてください。

 エネミーやアイテムなどのデータなどを扱う場合に、複数のデータを1つのまとまりとして管理できるデータベースのようなものがあると扱いが楽になります。

 Unityにはスクリプタブル・オブジェクトという機能(こちらはアセットとしてデータベースを扱う方法)がありますので、今回はこの機能を利用していきます。

 スクリプタブル・オブジェクトを作成するためには、専用のスクリプトを作成する必要があります。その作成方法を復習しながら、自分で作成していく学習を行います

 今回作成するスクリプタブル・オブジェクトはエネミーのデータを管理する目的で作成を行います。
そのため、スクリプト内には、エネミーのデータをまとめるための EnemyData クラスを用意します

 EnemyData クラスは、エネミー1体分のデータを1つにまとめている情報群です。
現在は Hp や攻撃力といった値を個別に用意していますが、これを1つのデータ群としてまとめて管理するためのクラスになります。

 この EnemyData クラスは、ゲームに登場するエネミーの数だけ用意することになりますので、それを管理するために List 機能を利用します。

 この2つの情報を管理して完成するのがスクリプタブル・オブジェクトになります。
どのような構成になっているかはスクリプト作成後に説明をしていますので、そちらをしっかりと学習してください。

 まずは最初にスクリプタブル・オブジェクトを作成するために必要なエネミーのタイプを登録する EnemyType を enum にて作成してから、
スクリプタブル・オブジェクト用のスクリプトを作成します。


2.EnemyType スクリプトを作成する


 enum (イーナム) を利用して、エネミーの種類を事前に登録し、この情報をエネミーの持つ情報として EnemyData クラス内に設定できるようにします。
enum のみでスクリプトを作成する場合、using の宣言や、MonoBehaviour(モノビヘイビア) クラスの継承は不要です
そしてどのスクリプトからでも変数の代入なしで利用可能になります。

 enum ではゲーム内に登場させたい種類の情報を、列挙子(れっきょし)という形で種類を作成できます。
今回は、エネミーの種類、という情報を EnemyType という名前で作成し、その中にエネミーの種類を登録しておきます。
これは追加可能な情報ですので、先々にエネミーの種類が増えても対応できます

EnemyType.cs

 <= クリックすると開きます


 スクリプトを作成したらセーブします。

 3つ以上の情報を管理する場合には、enum でその種類を登録しておくことをおすすめします
enum を利用する場合、その登録してある列挙子からしか情報を指定できませんので、
例えば、文字列と異なり、指定に際して打ち間違えが発生しませんので、不備の値が入ることも防ぐことが出来ます。

 ゲームの内容に応じた enum を考えて作成して運用します
プレイヤーの状態用(毒、混乱、痺れとか)、アイテムの種類(消耗品、武器、防具、など)、
ゲームの状態管理(ゲーム開始前、ゲーム中、ゲーム終了)など、非常に応用が利く機能です。



 なお enum では各列挙子に自動的に整数の番号が与えられます一番上から 0 で連番になっています
今回の場合であれば、Normal_0 には 0、Boss には 2 の数字が与えられています。

 この番号は見えない情報ですが、列挙子を int 型にキャストを行うことで取得して利用出来ます
下記の例の場合、enumValue には 1 が代入されます。

<enum の列挙子のキャスト>
int  enumValue = (int)EnemyType.Elite;

 また、列挙子の宣言時に数字を指定して代入することも可能です。その場合には連番ではなく、指定した数値を取得出来ます。

<数字の代入の例(今回この方式は利用しません)>
EnemyType.cs
public enum EnemyType {
    Normal= 10,
    Elite = 5,
    Boss = 100
}

 上記のように代入されている場合には、列挙子を int 型にキャストすると、代入してある値が取得出来ます。
今回は数字の代入は行っていませんので一番上の列挙子には 0 から順番に採番されています。


3.EnemyDataSO スクリプトを作成する


 スクリプタブル・オブジェクトを作成するために必要な EnemyDataSO スクリプトを作成します。
スクリプタブル・オブジェクト専用の ScriptableObject クラスを継承し、[CreateAssetMenu] 属性を記述することで作成可能になります。
 
 スクリプタブル・オブジェクトでは、指定したデータを複数のデータとしてまとめて管理することが出来ます。
そのため、データベースとしての役割を果たすことが可能になっています。

 今回指定して管理したいデータはエネミーのデータです。
そのため、スクリプタブル・オブジェクト内に必要な情報は以下の2つです。

 1.エネミー1体単位でのデータ(Hp、攻撃力、画像などのデータ群)を扱うクラス
 2.エネミーのデータをまとめる List(リスト)

 前回スクリプタブル・オブジェクトを作成したときには【1.エネミー1体単位でのデータ(Hp、攻撃力、画像などのデータ群)を扱うクラス】= CharaData.cs、
【2.エネミーのデータをまとめる List(リスト)】 = CharaDataSO.cs と2つのクラスをそれぞれ作成していました。

 今回はこれを1つのクラス内に一緒に記述する方法を学習します。

 まずは最初にスクリプトを記述してから、内容を確認しましょう。
なお、必要に応じて変数に Header 属性を利用しておくことで、インスペクターで項目を設定する際にわかりやすくなります。



EnemyDataSO.cs

 <= クリックすると開きます


 スクリプトを作成したらセーブします。


4.EnemyDataSO スクリプトの構造について


 処理の内容について、順番に確認していきます。

 1.エネミー1体単位でのデータ(Hp、攻撃力、画像などのデータ群)
 2.エネミーのデータをまとめる List(リスト)


1.エネミー1体単位でのデータ(Hp、攻撃力、画像などのデータ群)

 EnemyController スクリプトにて管理していたエネミーの Hp や攻撃力を個別の変数ではなく、
1つのデータ単位として管理できるように、 EnemyData クラスとして作成して、こちらにて管理を行うようにします。
利点は、1つの EnemyData クラス内には1体分のエネミーの全データが登録できることです。
そのため、Hp 用、攻撃力用というように変数を個別に作る必要はなく、EnemyData の Hp、EnemyData の攻撃力という形で EnemyData を参照して利用できる部分です。
例えば、EnemyData.hp と記述すれば、それはその EnemyData クラスに登録されている hp の値を参照することになります。

 ここからはピリオドによる参照処理が増えていきますので、しっかりと処理を読み解いていきましょう

    [Serializable]
    public class EnemyData {
      public string enemyName;      // エネミーの名前
      public int enemyNo;        // エネミーの通し番号
      public int hp;           // エネミーのHp
      public int attackPower;         // エネミーの攻撃力
      public int moveSpeed;       // エネミーの移動速度
      public EnemyType enemyType;      // エネミーのタイプ

      [Header("アイテムドロップ率")]
      public int itemDropRate;

      // TODO 他にもあれば追加
    }

 このようにエネミー1体分に必要になるデータをクラスとしてまとめておくことで管理と利用が容易になります
また、エネミーの情報を増やしたい(獲得できる Exp や属性など)場合には、この EnemyData クラス内に 型と変数を追記すれば、好きなだけ増やすことも出来ます。
この教材でも順番に EnemyData クラスに追記していきますので、それを確認するとわかりやすいでしょう。


2.エネミーのデータをまとめる List(リスト)

 EnemyData クラスにはエネミーの情報をまとめて登録できるようにしました。
このデータはエネミー1体分ですので、もしも複数のエネミーのデータを用意して登録したい場合、
この EnemyData クラスを複数用意して、それを管理するための変数が必要になります。

 こういった1つの同じデータ群をまとまったものをコレクションといいます。
C# にはコレクションを管理する方法として、Dictinary(ディクショナリー)List(リスト) があります。



 List クラスは <T> にジェネリック型(任意の型)を指定して、同じデータ型をまとめて管理するコレクション機能を持つクラスです。
配列と異なり、要素を自由に追加・削除できます。(要素数が可変する)
List はサイズ(長さ)が可変可能な配列のイメージです。

 List を利用する場合には配列と同様に初期化が可能ですが、Listでは初期化時に要素数の宣言が不要です

<配列の初期化>
  EnemyData[] enemyDatas = new EnemyData[3];       // <=  要素数の宣言が必要

<List の初期化>
  List<EnemyData> enemyDatasList = new List<EnemyData>();   // <=  要素数の宣言が不要

 そのため基本的には、予め要素数の確定しているデータを扱う場合には配列を、要素数が未確定であったり可変長であるデータについてはListを利用するように考えてください。


参考サイト
.net column様
【初期化の方法】C#で配列やリストを初期化するには?
https://www.fenet.jp/dotnet/column/language/713/



 public 修飾子にて List を宣言することで、インスペクター上でサイズの変更が可能です
例えばエネミーのデータを3体分作って登録したい場合には、インスペクターで List のサイズを 3 に設定すれば
EnemyData クラスが 3 つ、Element 0 〜 Element 2 として作成されますので、ここにエネミーのデータを1体ずつ、合計3体分登録することが出来ます。

<EnemyData クラスを扱う List>
    public List<EnemyData> enemyDatasList = new List<EnemyData>();


5.<クラス内に別のクラスを作成する(入れ子クラス)>


 EnemyDataSO クラスの宣言フィールドにおいて、別のクラスを作成しています。今回は EnemyData クラスです。
このようにC#では、1つの独立したクラス(ファイル)としてではなく、あるクラスの中に別のクラスを作成しても使用することができます。
このような構造を入れ子(ネスト)クラスと言います。

 特定のクラスでのみ使用することが確定しているような、使用範囲の狭いクラスであれば、
このように入れ子クラスにした方がスクリプト・ファイルが増えずに済みます。
 また設計上、ファイルにはしたくない(隠しておきたい)クラスを作成する場合にも用いられます。

 使用方法は他のクラスと同じです。参照する場合は、EnemyDataSO.EnemyData という書式で、入れ子クラスのあるクラスの後に、入れ子クラスを順番に記述します


6.[System.Serializable(シリアライザブル)]属性


 入れ子になっているクラスの1行上には上記の宣言があります。[ ]で宣言された設定値は「属性」と呼ばれる情報になり、特別な意味を持ちます。

 今回利用している[System.Serializable]属性は、Systemに含まれている設定値であり、こちらを宣言することで入れ子クラスの情報をインスペクターに表示することが出来ます。
これを書き忘れてしまうと、インスペクターに EnemyData が表示されず、データをインスペクターから登録することが出来ません
using System; を宣言している場合には [Serializable] とだけ記述すれば適用されます。
宣言していない場合には [System.Serializable] と記述する必要があります。


2.EnemyDataSO スクリプタブル・オブジェクトを作成する

1.設計


 EnemyDataSO スクリプトを元に EnemyDataSO スクリプタブル・オブジェクトを作成します。
EnemyDataSO スクリプトに用意してある EnemyData 型の List である enemyDataList 変数がデータベースの役割を持っています。


2.<クラスのリスト化によるデータベース作成>


 EnemyData クラスは1つのデータ情報を扱うことが出来ます。今回であればエネミーの1体分の情報です。
そのため複数のエネミーの情報を扱う必要がある今回のような場合には、その分だけ EnemyData クラスを追加して作成しなければなりません

 それらを管理するために EnemyData 型の List を作り、まとめて管理を出来るようにしています。
ここで大切なことは、1つ1つの別の変数に個別に EnemyData が存在していたのではまとめて管理していることにはなりません
EnemyDataのリストとはすなわち、EnemyDataをまとめて扱っているデータの集合体になりますので、ここにデータベースとして役割を成立させることが出来ます


3.EnemyDataSO スクリプタブル・オブジェクトを作成する


 最初に、スクリプタブル・オブジェクトを管理するためのフォルダを作成しておきます。
Project 内で右クリックをしてメニューを開き、Datas フォルダを作成してください。
この中に作成されたスクリプタブル・オブジェクトを入れて管理します。



 Unity の左上のメニューより、Assets => Create => Create EnemyDataSO を選択します。
新しく EnemyDataSO というファイルが作成されます。名前はそのままで構いません。

 このアイコンの形が違うファイルがスクリプタブル・オブジェクトになります。
これはアセットとして取り扱われるようになる情報です。

 EnemyDataSO スクリプタブル・オブジェクトを Datas フォルダへ移動してください。
今後もスクリプタブル・オブジェクトを作成したら、Datas フォルダ内で管理するようにします。


<フォルダ管理>



 早速スクリプタブル・オブジェクトを活用して、エネミーのデータを登録していきましょう。


4.EnemyDataSO スクリプタブル・オブジェクトの設定を行う


 作成された EnemyDataSO スクリプタブル・オブジェクトを選択してインスペクターを確認します。
EnemyDataSO スクリプトにて宣言した EnemyDataList 変数がインスペクターに表示されて、 Sizeが 0 になっています。
これがスクリプタブル・オブジェクトの中身です。

 Size に任意の数を入力すると、同数の Element が作成されます。これが List で管理する EnemyData クラスの情報群になります。


インスペクター画像



 まずはエネミーのデータを3体分登録しておきたいと思います。
EnemyDataList 変数の Size を 3 に変更してください。Element 0 〜 2 が下に作成されます。

 Element とは List の要素(中身)のことです。
そのため、Element 1つが EnemyData 1つになります。Element の番号は 0 から始まります。

 以上のことから、1つの Element には1つの EnemyData クラスの内容を設定できるようになっています。
このとき、EnemyData クラスの上に [Serializable] 属性を宣言しているので、EnemyData クラスの内容がインスペクターに表示されています。
[Serializable] 属性を活用することによって、インスペクターからエネミー用の情報を1体ずつ、EnemyData 単位で登録出来るようになっています。



 下記の画像のように設定を行ってみてください。

インスペクター画像



 各数値は任意ですが、No の値だけは異なる番号で設定してください。できれば 0 から連番が理想です。
この番号はエネミー用の個体番号として利用する可能性がありますので、同じ番号を重複して設定してしまうと、同じ番号の個体が複数存在することになり、
番号によってエネミー用を特定することが出来なくなります。


5.<入れ子クラスを外部のクラスで宣言する方法>


 EnemyDataSO スクリプト内に入れ子クラスとして作成している EnemyData クラスを外部のクラスで用いる場合には、入れ子の親クラスを記述した上で宣言するようになります。

 下記のように書きます。

  EnemyDataSO.EnemyData enemyData;

  enemyData.enemyDatasList[index].power;

 これは次の手順から利用します。



 以上でこの手順は終了です。

 次は 発展2 −エネミー用のデータベースの利用− です。