i-school - 2Dまるばつゲーム(三目並べ) 手順5
 プレファブにした Grid ゲームオブジェクトを、スクリプトにより制御を行えるように、専用のスクリプトを作成します。


<実装動画>
動画ファイルへのリンク

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

手順5 ーGrid 用のクラスの作成ー
 9.Grid ゲームオブジェクト制御用のスクリプトを作成する
10.Grid の生成方法を修正する



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

 ・enum だけのスクリプト・ファイルの作成
 ・プロパティ
 ・ラムダ式によるプロパティの記述方法
 ・ゲームオブジェクトをスクリプトで制御する考え方
 ・GameObject 型ではなくて自作クラスでプレファブをインスタンスする方法



9.Grid ゲームオブジェクト制御用のスクリプトを作成する

1.設計


 Grid ゲームオブジェクトは、マス目1つを表現するためのゲームオブジェクトです。
クリックに反応できるように Button コンポーネントがアタッチされており、
子オブジェクトの方には、○×の表示が行えるように Text コンポーネントがアタッチされています。

 これらのコンポーネントは独立しているため、Grid ゲームオブジェクトはこれらを管理はしていますが、
実際にそれらのコンポーネントを制御するためには、それらの情報を取得しないと制御出来ません。

 こういった複数のコンポーネントを制御し、かつ、ゲームオブジェクトに対して特別な役割を与えるには
専用のスクリプトを1つ作成して、それをゲームオブジェクトにアタッチし、スクリプトがこれらの情報を束ねて制御を行えるようにする方法があります。

 各コンポーネントやゲームオブジェクト全体をスクリプトから制御を行うためには、スクリプトはこれらの情報を「知っている」、
つまり、変数を用意して情報を取得できるようにしておくことが重要です。

 実際に作成する際に再度説明を行いますが、そのスクリプトで管理を行いたい情報の1つとして、Grid に持ち主の情報を管理させることを検討します。

 持ち主としては、プレイヤーかコンピュータか、ということになりますが、ゲーム開始時には「持ち主なし」という状態も必要になります。
そのため、例えば、bool 型の変数によっては、2つしか状態の保持ができないため、この管理には不向きということになります。

 2つ以上の状態を持つ変数を扱いたい場合には、enum(イニューム、イーナム)を作成しておくと管理が楽になりますので、まずはそちらを先に作成します。


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


 Grid の所有者の種類を登録しておくための enum の作成方法を提示します。

 enum (イニューム、イーナム) を利用して、所有者の種類を事前に登録し、この情報を Grid の持つ情報として次の手順で作成する Grid 用のスクリプト内に設定できるようにします。

 enum のみでスクリプトを作成する場合、using の宣言や、MonoBehaviour(モノビヘイビア) クラスの継承は不要です
そしてどのスクリプトからでも型として利用可能になります。

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

 今回はプレイヤー、コンピュータ、持ち主なし、ともう1つ、引き分けという状態も用意しておきます。
これは、この所有者の状態情報を管理用としてだけではなく、勝敗の判定にも利用できるようにするためです。



GridOwnerType.cs

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


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



 このスクリプトはゲームオブジェクトにアタッチできませんので、作成した時点で、すべてのスクリプト内で利用可能になります。

 先ほどもお伝えしたように、3つ以上の状態情報を管理する場合には、enum でその種類を登録しておくことをおすすめします

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

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



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

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

<enum の列挙子のキャスト>
int  value = (int)GridOwnerType.Player;

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

<数字の代入の例(今回この方式は利用しません)>
GridOwnerType.cs
public enum GridOwnerType {
  None = 10,
    Player = 100,
    Opponent = 20,
    Draw = -5
}

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


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


 Grid ゲームオブジェクトに、Grid としての役割を与えるためのスクリプトを作成します。

 具体的に管理する情報としては、先ほどの GridOwnerType による所有者の情報Grid の通し番号を設定し、何番目の Grid であるかを判別できるようにします。
また、Button コンポーネントの制御による Grid ゲームオブジェクトのボタンを押した際の処理、
Text コンポーネントの制御による Grid 上に○×を表示させる処理についても作成します。

 これらの Grid の役割に沿った制御を1つのスクリプトにまとめておくことにより、Grid ゲームオブジェクトの役割が明確になるとともに、
このゲームオブジェクトの制御を行いたい場合には、このスクリプト内に変数やメソッドを記述していけばいい、という道筋を作ることにもなります。



 先ほどからお伝えしているように、ゲームオブジェクトには、これらの情報を有することが出来ません。
つまり、所有者の情報や、通し番号などの情報は持っていないため、管理できません。
Button コンポーネントなどはアタッチされていますが、利用するためには、その情報の取得が必要になります。

 よって、これらすべてをまかなうためのスクリプトを作成しておくことにより、管理と制御が行えるようになります。
実際には、作成したスクリプトと Grid ゲームオブジェクトとを紐づけ(アタッチ)して制御を行います。

 なお、スクリプトの名称は分かりやすさと役割に応じて検討してください。
今回であれば Grid という名称も候補になりえますが、Unity に最初から Grid というスクリプトが存在しているため、
同名スクリプトを作成すると競合してしまってエラーが発生します。

 こういったケースを回避する手段の1つとして、namespace という機能がプログラムには存在しています。
ですが今回は GridController という別の名称として作成し、同名による競合から回避しています。

 なお、自作したスクリプトやメソッドにはサマリーを記述する癖をつけておきましょう。
特にメソッドへのサマリーの記述は必須事項といえます。


GridController.cs

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


 TODO とは、今後処理を実装する予定の場所であり、処理の書き忘れを防止する意味も持ちます。
スクリプトを記述する際には、日本語で処理のコメントを TODO で最初に記述してからプログラムへと書き変えていくと、処理が作りやすくなります。


4.<プロパティ>


 プロパティとは、クラス外部から見るとメンバー変数のように振る舞いクラス内部から見るとメソッドのように振る舞う機能です。
そのため、実装状態(private修飾子のまま)を変更することなく、外部クラスへの参照・変更を行える機能であるため、扱いを覚えておくと非常に便利です。

 この特性より、プロパティを作成する場合、多くは private 修飾子の変数の情報を扱うために作成します
今回の場合は、GridController クラスの currentGridOwnerType 変数に対してのプロパティを作成します

<プロパティの作成>
    private GridOwnerType currentGridOwnerType;


  /// <summary>
    /// currentGridOwnerType のプロパティ
    /// </summary>
    public GridOwnerType CurrentGridOwnerType
    {
        get {
            return currentGridOwnerType;
        }
        set {
            currentGridOwnerType = value;
        } 
    }

 上記のようなプロパティを作成することで、private 修飾子で宣言している currentGridOwnerType 変数を
プロパティを利用することによって、外部クラスから参照出来るようにしています。
ただし現在は、デバッグ作業の効率化のため、SerializeField 属性を付与して、private である情報をインスペクターに表示しています。

 private 修飾子にて宣言フィールドで宣言した変数については、外部のクラスからは参照・変更を行うことが出来ません
このとき参照を行いたい場合には、変数の修飾子を public 修飾子に変更して対応するのではなく
プロパティの持つ get キーワードを利用して、戻り値を利用して private 修飾子の変数を外部クラスに参照させることで対応することが出来ます。

 こういった機能を知っているかどうかで設計部分が大きく変わります
色々な機能を知っていれば、例えば、今回はプロパティを使おうか、どうしようか、という選択肢が増えますが、知らなければ選択肢自体が生まれません。

 設計の引き出しを広げる上でも、新しい技術を覚えること、常に学習する意欲と向上心を持つことがプログラム学習のポイントです



 プロパティを利用した設計により、public ではない変数を外部クラスで利用できるようにします。
外部からプロパティを呼び出して戻り値を参照する処理のことをゲッター(getter)と呼びます。

  get {
      return currentGridOwnerType;
  }



 同様に、private 修飾子の値を外部クラスより変更したい場合には、プロパティを仲介する手法を使って書き換える処理を実装出来ます。
こちらの処理は set キーワードを利用して処理を記述します。そのため、この処理をセッター(setter)と呼びます。

 set キーワード内には呼び出し元から value の値で情報が届いていますので、value の値を利用することで private 修飾子の変数に代入処理を行うことが出来ます。

  set {
      currentGridOwnerType = value;
  }

 この処理を外部のクラスから実行する場合には、参照する場合と同じで「クラスの変数.プロパティ名」と記述することで値の書き換えが可能です。

 get、set キーワードはどちらか片方だけでも記述できます。また、記述する順番も任意です。set から書き始めることも出来ます。
またプロパティ内部に条件式を用意して、その結果に合わせて処理を変更する記述も出来ます


参考サイト
未確認飛行 C 様
プロパティ
https://ufcpp.net/study/csharp/oo_property.html

FEnetインフラ様 テックブログ
C#のプロパティを使いこなそう!さまざまな実装方法を紹介
https://www.fenet.jp/infla/column/technology/c%E3%...


5.<ラムダ式によるプロパティの記述方法>


 ラムダ式による記述はプロパティについても利用することが出来ます。


<プロパティの書式の一例>
    public GridOwnerType CurrentGridOwnerType
    {
        get { 
                 return currentGridOwnerType ; 
        }    
        set { 
                 currentGridOwnerType = value;
        }    
    }


<ラムダ式によるプロパティ>
    public GridOwnerType CurrentGridOwnerType
    {
        get => currentGridOwnerType;
        set => currentGridOwnerType = value;
    }

 get 部分については return を省略することが出来ます。

 注意点としては、プロパティでラムダ式にできるのは1行分だけです。
そのため、例えば set 内に2行以上の処理がある場合にはラムダ式での記述はできません。


6.Grid ゲームオブジェクトのプレファブに GridController スクリプトをアタッチして設定を行う


 プレファブの Grid ゲームオブジェクトを選択してインスペクターの上部にある Open Prefab を選択し、プレファブ編集モードに切り替えます。

 先ほど作成した GridController スクリプトをドラッグアンドドロップしてアタッチしてください。

 インスペクターに GridController スクリプトが追加されますので、表示されている変数に必要な情報をアサインして登録します。

 アサインの手続きは、ゲームオブジェクトをドラッグアンドドロップすることでアサイン出来ます。

 btnGrid 変数には、Grid ゲームオブジェクト自体をドラッグアンドドロップしてアサインしてください。
自動的に Button コンポーネントの情報は変数に登録されます。

 txtGridOwnerIcon 変数には、Grid ゲームオブジェクトの子オブジェクトである txtGridOwner ゲームオブジェクトをドラッグアンドドロップしてアサインしてください。
こちらも自動的に Text コンポーネントの情報が変数に登録されます。

 currentGridOwnerType 変数には設定は不要です。enum は初期値として、最初に登録した列挙子の情報が自動的に設定されますので、None になっていれば問題ありません。


インスペクター画像



 以上で設定は完了です。


10.Grid の生成方法を修正する

1.設計


 この手順では、ゲームオブジェクトの生成の方法のバリエーションについて学習を行います。

 Instasntiate メソッドの具体的な利用例になりますので、しっかりと学習し、活用できるようにしていきましょう。
この手法が習得できているかどうかにより、処理の記述の引き出しが増えますので、とても重要な学習になります。



2.GameManager スクリプトを修正する


 ここでは前回のサンプルとして用意した GameManager スクリプトを修正して作成しています。
自分で書いたスクリプトがある場合には、そちらを優先して修正ください。

 プレファブのクローンの生成には GameObject 型を利用していましたが、Instasntiate メソッドでは
ゲームオブジェクトにアタッチされているスクリプト(クラス)の情報を利用してプレファブのクローンを生成することも出来ます。

 そのため、変数の宣言を GameObject 型ではなく、GridController 型に変更します。
また、配列で管理していた型も GameObject 型ですが、こちらも GridController 型で管理を行うように変更します。


GameManager.cs

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



4.<GameObject 型ではなくて自作クラスでプレファブをインスタンスする方法>


 Instantiate メソッドには戻り値があり、その型は Object 型です。(GameObject 型ではありません)
Object 型とは、すべてのクラスに自動的にサポートされるクラス(型)です。これは、C# プログラムにおける基本クラスとなっています。

MicroSoft
Object クラス
https://docs.microsoft.com/ja-jp/dotnet/api/system...


 そのため、Unity において作成されているクラス(型)ーGameObject 型、Transform 型、Rigidbody 型などーは、すべてこの Object クラスの情報を有しています。
利用頻度の高い ToString メソッドも、この Object 型によって提供されているメソッドになります。
 


 Instantiate メソッドでは第1引数に指定した Object 型のクローンの生成を行うとともに、生成を行った型を戻り値として左辺へ戻します
そのため、GameObject 型でクローンの生成を行うと、GameObject 型が戻り値として戻されます。
GameObject 型は Object 型の情報を有しているため、このように Object 型以外の型でも指定出来るようになっています。

 この機能は GameObject 型には限らないため、クローンの生成を行いたいゲームオブジェクトに、自作したスクリプト(クラス)がアタッチされている場合には
そのスクリプトを使って、クローンの生成を行うとともに、そのスクリプトの型を戻り値として戻すことが出来ます
この場合、左辺に用意する型もスクリプトの型を用意することで戻り値を受けとることが可能です。

 この場合も GameObject 型と同じで、自作したスクリプト(クラス)には Object 型の情報が含まれているので、Instantiate メソッドの第1引数として指定できるようになっています。

 クラスによる生成、という単語で見るとイメージが沸きにくいかもしれませんが、どちらの場合であっても、ゲームオブジェクトが生成されます。


<GameObject型でのインスタンス処理>
  // 宣言
    [SerializeField]
    private GameObject gridPrefab;

    [SerializeField]
    private GameObject [] grids;


    // ループ文を利用した生成処理
   for (int i = 0; i < grids.Length; i++) {

     // GameObject 型でインスタンスするので、戻ってくる型も GameObject 型
        grids[i] = Instantiate(gridPrefab, gridSetTran, false);

        // GameObject にアタッチされている GridController スクリプトにアクセスして、SetUpGrid メソッドを実行する
    // GameObject 型なので、GetComponent メソッドによる GridController スクリプトの取得が必要
        grids[i].GetComponent<GridController>().SetUpGrid(i, this);
    }



<自作クラスでのインスタンス処理>
    [SerializeField]
    private GridController gridPrefab;

    [SerializeField]
    private GridController  [] grids;


    // ループ文を利用した生成処理
   for (int i = 0; i < grids.Length; i++) {

     // GridController  型でインスタンスするので、戻ってくる型も GridController 型
        grids[i] = Instantiate(gridPrefab, gridSetTran, false);

        // GameObject にアタッチされている GridController スクリプトにアクセスして、SetUpGrid メソッドを実行する
    // 戻り値になっている型が最初から GridController 型なので、GetComponent メソッドによる GridController スクリプトの取得が必要ない
    // つまり、必要な情報が利用できる状態で提供される形式になる
        grids[i].SetUpGrid(i, this);
    }


 違いとしては、プレファブとして登録する際の型や、インスタンス処理の際の左辺に用意する型が異なります

 そして最も大きな違いは、クラスの取得方法です。インスタンスされたゲームオブジェクトの持つクラスの情報を利用したい場合、
GameObject 型である場合には一度、GetComponetメソッドを利用して、操作を行いたいクラスの情報を取得する必要があります

 ですが下のケースの場合、クラスを利用してゲームオブジェクトが生成されるため、GameObject 型の場合に必要な GetComponentメソッドの処理が不要になります。
生成されるゲームオブジェクトに変わりはありません。

 下の自作クラスでインスタンス処理をした場合にはゲームオブジェクトのクローンを生成する部分は同じですが、
戻り値として GridController スクリプトを受け取っているため、GetComponent メソッドを実行せずとも、そのスクリプトの情報を自動的に戻り値から取得出来ています。

 このように GetComponent メソッド処理を省略する処理を書くことで、処理的に重い GetComponent 処理の負荷を減らすことが出来ます
もしも生成したゲームオブジェクトのクローンに対して、何か処理を行いたいような場合には、GameObject型だけではなく、自作クラスにて生成することも念頭に置いて設計しておきましょう。


5.GameManager ゲームオブジェクトの設定を行う


 GridPrefab 変数の型情報が GameObject 型から GridController 型に変更になっているため、アサインが外れていると思います。
こちらに、今までと同じように Grid ゲームオブジェクトのプレファブをドラッグアンドドロップしてアサインしてください。

 アサイン後の () の情報に注目しておいてください。今までは GameObject 型をアサインしていたので Grid(GameObject) となっていましたが、
今回は GridController 型の情報をアサインしているので、Grid(GridController) と情報が書き換わっています。

 インスペクターのアサイン情報の読み解き方もしっかりと覚えて置くようにしましょう。


インスペクター画像



6.ゲームを実行して動作を確認する


 処理が完成しましたので、最初から今回学習した内容を振り返り、処理の内容の理解を深めてください。

 今回はゲーム画面における処理には変更がないため、見た目的には何も変わりません。
そのため、ゲームを実行した際に、今までと同じように Grid のゲームオブジェクトが9個生成されれば問題ありません。
Console ビューを確認し、GridController スクリプト内の SetUpGrid メソッドに記述した Debug.Log メソッドが動作していることを確認しましょう。


Console ビュー




 合わせて、GameManager ゲームオブジェクトのインスペクターを確認し、GameObject 型ではなく、
GridController 型によって生成された Grid ゲームオブジェクトが Grids 配列に代入されているかを確認しておいてください。


GameManager ゲームオブジェクト インスペクター画像





 以上でこの手順は完成です。

 今回のように、ゲームや処理の見た目を変えることなく、プログラムの内容(中身)を精査して修正していくことをリファクタリングといいます。
これはバグやエラーの修正ではなくて、プログラム内容の向上を目的として行うことを指します。

 学習した内容はとても複雑なロジックになりますが、ゲームオブジェクトに自作したスクリプトをアタッチし、
そのスクリプトを利用して生成を行うという処理は、非常に多くのゲームで応用可能なパターンの1つです。

 次は 手順6 −クラス同士の繋がりの作成− です。