i-school - スクリプトから生成したプレハブのアサイン外れの対処方法

目的


 ゲーム開発では、複数のシーンやレベルが存在することがあります。
シーン内のゲームオブジェクトは、そのシーンに存在するため、プレハブを使ってもシーン内のゲームオブジェクトと直接関連付けることができます。
しかし、シーン外のゲームオブジェクトには、直接関連付けができません。

 最初からヒエラルキーウインドウにあるゲームオブジェクト同士はアサインをしたりして、他のゲームオブジェクトの情報の関連付けが出来ます。
ですが、ゲーム実行後に生成されたゲームオブジェクトは、ヒエラルキーウインドウにあるゲームオブジェクトとは事前に関連付けが出来ません。

 つまり、プレハブになっているゲームオブジェクトを使って、ゲーム実行後にスクリプトから動的に生成したプレハブのクローンのゲームオブジェクトは、
シーンビューにあるゲームオブジェクトとは直接関連付けられないため、スクリプトを使って設定を行う必要があるということです。


プレハブの仕様


 まず、プレハブの仕様を理解しておくことが大前提です。

 プレハブとは、あらかじめ作成したオブジェクトの設定やコンポーネントを保存し、その設定をもとに同じものを簡単に作成できるものです。

 大変便利な機能ですが、プレハブにしたときに、ヒエラルキーウインドウにあるゲームオブジェクトのアサインが外れてしまいます
これはプレハブの仕様ですので、回避する方法はありません。

 そのようになってしまう、ということを理解した上で、ではどうするのか、を考えていきます。


問題点


 ヒエラルキーウインドウにあるゲームオブジェクト同士は問題なく、アサインしておくことが出来ます。
以下の例では、GameManager に Cinemachine のゲームオブジェクトのスクリプトの情報がアサインされています。

 これはお互いにヒエラルキーウインドウにあるゲームオブジェクト同士であるためです。






 問題はプレハブです。

 先ほども説明した通り、プレハブにしたゲームオブジェクトは、シーンビューにある(ヒエラルキーウインドウにある)ゲームオブジェクトの情報を失います
これは、シーンビューとプレハブは異なる場所で管理されているため、関連付けをすることができなくなるためです。

 下記のケースではプレイヤー用キャラのゲームオブジェクトをプレハブにした場合です。
ヒエラルキーウインドウにあるゲームオブジェクトの情報が取得できないので、Camera のゲームオブジェクトのアサインが外れています。


<アサインしているゲームオブジェクトがヒエラルキーウインドウにあるゲームオブジェクトの場合>


  ↓

<プレハブにすると、ヒエラルキーウインドウにあるゲームオブジェクトのアサインが外れてしまう>





 これは逆の場合も同じで、プレハブにしたゲームオブジェクトをシーンビューから削除したとき、
ヒエラルキーウインドウにあるゲームオブジェクトの方からもプレハブのゲームオブジェクトのアサインが外れてしまいます

 下記のケースでは、Cinemachine の Follow の対象がなくなり、スクリプトの方からもアサインが外れています。


<本来>





 ↓


<Follow からプレハブのゲームオブジェクトのアサインが外れてしまう>



<スクリプトのアサインからプレハブのゲームオブジェクトの情報が外れてしまう>




 プレハブはSceneビューに配置されたゲームオブジェクトとは別に生成されるため、生成されたプレイヤーキャラクターがカメラの追従対象になるように、プログラム上で設定する必要があります。初心者に対しては、プレハブがSceneビューに配置されたゲームオブジェクトとは別に存在するという概念や、オブジェクトの生成と設定の仕方をわかりやすく説明することが必要です。


問題点解消のためのポイント


 今回は、プレハブのアサイン外れとヒエラルキーウインドウにあるゲームオブジェクトのアサイン外れの解消を行います。

 GameManager、プレイヤー用キャラ、カメラの3つのゲームオブジェクトがあり、それぞれに役割が設定されている想定です。
このうち、プレイヤー用キャラのみがプレハブになっており、GameManager とカメラのゲームオブジェクトはシーンビューにあります。


1.<メソッドの引数を活用する>


 生成処理を行うスクリプト内において、続けて、初期設定用のメソッドを実行し、必要となる情報を引数で渡すことにより、この症状を解消できます。
つまり、初期設定を行う場合、そのスクリプト内で情報を探すのではなく、外部のクラスから情報の提供を受けてそれをそのまま設定する、という手法です。


// プレハブから生成

// PlayerController に CameraController の情報を渡す
playerCharaPrefab.SetUpPlayer(cameraController);
 
   ↓

    [SerializeField]
    private CameraControllerFromCinemachine cinemachineCamera;   // ← このアサインが外れている

    private CinemachinePostProcessing postProcessing;


    /// <summary>
    /// 初期設定
    /// </summary>
    /// <param name="camera"></param>
    public void SetUpPlayer(CameraControllerFromCinemachine camera) {  // ← ここの引数に外部のクラスからアサインに必要な情報を受け取る

        // メソッドの引数で受け取った情報を代入 => PlayerController が Find メソッドなどを使って情報を探さなくても、外れているアサインに情報を取得出来る
        cinemachineCamera = camera;

        // メソッドの引数で受け取った情報を使う => PlayerController が Find メソッドなどを使って情報を探さなくても済むようになる
        camera.TryGetComponent(out postProcessing);

        (省略)
    }

 このようにメソッドの引数を活用することで、合理的、効率的な処理が構築できます


2.<GameObject型以外のインスタンスの方法>


 Instantiate メソッドではクラスの情報を利用して生成することが出来るため、GameObject 型ではなく、生成するゲームオブジェクトにアタッチしているクラスの型で生成します。
そうすることにより、GetComponent メソッドを使わなくてもすぐにクラスのメソッドに命令を実行できます。



 Instantiate メソッドには戻り値があり、クローンの生成を行うとともに、生成を行った型を戻り値として左辺へ戻します。
そのため、GameObject 型でクローンの生成を行うと、GameObject 型が戻り値として戻されます。

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


<GameObject型でのインスタンス処理>
    [SerializeField]
    private GameObject playerCharaPrefab;

    [SerializeField] 
    private Transform startTran;


    // Player用のキャラの生成
    GameObject playerChara = Instantiate(playerCharaPrefab, startTran.position, Quaternion.identity);

    // PlayerController スクリプトを取得
    PlayerController playerController = playerChara.GetComponent<PlayerController>();

    // PlayerController の設定を行う
    playerController.SetUpPlayer([設定したい情報を渡す]);

   ↓


<クラスでのインスタンス処理>
    [SerializeField]
    private PlayerController playerCharaPrefab;  // GameObject 型ではなくクラスでアサインしておく


    // Player用のキャラの生成
    PlayerController playerChara = Instantiate(playerCharaPrefab, startTran.position, Quaternion.identity);  // PlayerController 型でインスタンス化される

    // PlayerController の設定を行う
    playerController.SetUpPlayer([設定したい情報を渡す]);  // GetComponent の処理が省略できる


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

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

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

 この使い方は Unity のマニュアルにも記載されています。
読んでおくとよいでしょう。


参考サイト
Unity 公式マニュアル
Object.Instantiate


3.<処理の起点を1箇所にする>


 Startメソッドでの初期設定が必要な複数のクラスがある場合、それぞれのクラスでStartメソッドを呼び出すことで初期設定を行うことができます。
しかし、GameManagerといった一つのクラスに初期設定をまとめることで、初期設定の一貫性を保ちやすくなり、コードの保守性や拡張性が高くなるという利点があります。

 イメージとしては、GameManager の Start メソッドがゲーム内の処理の起点となり、この地点からトップダウン型の命令系統を構築する形です。
このように処理の起点を1箇所にすることで、各クラスが Start メソッドを使ってそれぞれ動き出す方式ではなく、この起点の部分から順番に処理を実行させていく方式になります。

 そのため、GameManager 以外の各クラスでは Start メソッドではなく、GameManager から実行してもらうための初期設定用のメソッドを用意しておき、
そこに対して命令を受けることで Start メソッドのように利用する方式です。

 処理の流れが可視化できるため、より設計者の想定している処理の流れで各クラスの制御を行うことが出来るようになります。


スクリプトから生成したプレハブに対して、ヒエラルキーウインドウにあるゲームオブジェクトを動的設定する


 今までの説明を念頭に置き、スクリプトのサンプルを提示します。

 生成されたプレハブにヒエラルキーウィンドウ内のゲームオブジェクトがアサインされていない場合は、スクリプトを使って動的に設定する必要があります。
具体的には、プレハブを生成するスクリプトの中で、生成されたプレハブにアクセスして、必要なコンポーネントや変数に対して値を設定することができます。



 この処理は、GameManager、PlayerController、CameraControllerの3つのクラスから構成されています。

 GameManagerは、プレイヤーキャラクターのセットアップ、カメラのターゲットを設定します。

 PlayerControllerは、プレイヤーキャラクターの移動、ダッシュ、攻撃などを制御し、CinemachineポストプロセスのVignetteを制御します。

 CameraControllerは、Cinemachineを使用してプレイヤーを追跡し、カメラの視野を制御します。

 GameManager は他の2つのクラスに命令を出す起点となるため、前提として他のクラスが必要になります。
そのため、GameManager 以外のクラスから作成を行います。

 PlayerController クラスも CameraController クラスの情報が必要となっているため、
作成順は、CameraController → PlayerController → GameManager の順になります。



CameraControllerFromCinemachine.cs

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





PlayerController.cs

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




 最後に GameManager を作成し、ここから、先ほどの2つのクラスに対して命令を実行します。
そのため、今回の設定においては、Start メソッドはこのクラスにしかない状態、ここが処理の起点となる場所です。

 ここではプレイヤー用キャラのプレハブ、生成位置、カメラのゲームオブジェクトなどの情報をアサインしておきます。


GameManager.cs

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



<解説>


 GameManagerの Startメソッドが起点となって、他の2つのクラスの初期設定用のメソッドを実行しています。

 最初にプレイヤー用キャラのプレハブをインスタンスし、その後、そのクローンされたゲームオブジェクトにアタッチされている
PlayerController クラスの SetUpPlayer()メソッドを実行し、引数に CameraController の情報を渡すことで、プレイヤーのカメラとの設定を実行し、アサイン外れを解消しています。

 同様に CameraController の SetUpTarget() メソッドが実行され、生成されたプレイヤー用キャラの情報を渡すことで、アサイン外れを解消しています。


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


 実際に想定している挙動になるか確認します。
デバッグを行う際には、どのような挙動が正しいのかを理解してから行うようにしてください。
実行して動いたから OK ではなく、どういう処理になっているので、このように動いている、という論理的な考え方で捉えるようにしてください。

 まずはヒエラルキーウインドウにあるゲームオブジェクトに対して、生成されたプレハブの情報が正常にアサインされるかを確認します。


<実行前のインスペクター画像>



<実行後のインスペクター画像>



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



 同じように生成されたプレハブのゲームオブジェクトのアサインも確認します。
プレハブの際には外れていた、ヒエラルキーウインドウにあるゲームオブジェクトのアサインが行われていれば正常に動作しています。


<実行前(プレハブ)>



<実行後(生成されたプレハブ)>




 このような手法を用いることにより、プレハブになったときのアサイン外れを解消することが出来ます。