【Unity】タワーディフェンス(8) 複数の敵データをScriptableObjectで管理【クソゲー制作】

タワーディフェンスを作る(8)

Unity初心者が2Dタワーディフェンスを制作しています。今回は複数の敵のデータをScriptableObjectで管理して、生成する処理を実装します。

環境
  • Mac mini (M1, 2020)
  • Unity 2022.3.36f1
目次

敵のデータをScriptableObjectで管理する

今のところゲーム内に敵は1種類しか出現しませんが、いろんな種類の敵を出現させたいですよね。それぞれの敵のオブジェクトを個別に作成するのでは効率が悪いので、データベースに敵のパラメータやアニメーションを登録して、1つのプレハブからいろんな敵を生成してしまいます。今回はScriptableObjectを使います。

ScriptableObjectとは

ScriptableObjectとはUnityで使用される特殊なオブジェクト型で、データベースのようにデータを管理・格納することができます。一度作成すれば複数のスクリプトやオブジェクト間でデータを共有できるので、メモリ効率が向上します。

EnemySettingスクリプトの作成

「Assets/Scripts」下に新しいC#スクリプトを作成します。名前を「EnemySetting」にします。

EnemySettingスクリプトの内容は次のとおりです。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System; // [Serializable]を使うために必要な宣言

[CreateAssetMenu(fileName = "EnemySetting", menuName = "ScriptableObject/Enemy Setting")]
public class EnemySetting : ScriptableObject
{
    public List<EnemyData> enemyDataList = new List<EnemyData>();

    [Serializable]
    public class EnemyData
    {
        public string id;   // ID
        public string name; // 名前
        public int maxHp;   // 最大HP
        public float speed; // 移動速度
    }
}

このスクリプトはScriptableObjectを使って複数の敵データを管理するためのクラスです。

[CreateAssetMenu]属性は、Unityのメニューに新しいScriptableObjectを作成するための項目を追加します。fileNameは作成されるファイルの名前、menuNameはメニューに表示される項目名です。

public class EnemySetting : ScriptableObjectでクラスを宣言しています。MonoBehaviourではなくScriptableObjectを継承します。

enemyDataListは複数の敵のデータを格納するリストです。

[Serializable] 属性を付与するとEnemyData クラスがUnityのインスペクタに表示されるようになります。

EnemyDataクラスは個々の敵のデータを保持するクラスです。現在のところ、ID、名前、最大HP、移動速度を入れています。ゲームの開発を進めていくと項目の数が増えていきそうです。

ScriptableObjectのアセットを作成

STEP

「Assets」下に「Data」という名前のフォルダを作成します。これから作成するScriptableObjectを入れるためのフォルダです。

STEP

「Assets/Data」フォルダで右クリックして「Create > ScriptableObject > Enemy Setting」を選択してScriptableObjectを作成します。

タワーディフェンス90
STEP

「Assets/Data」フォルダの下に「EnemySetting」というアセットが作成されました。

タワーディフェンス91

敵のデータの登録

STEP

「Assets/Data/EnemySetting」を選択して、インスペクターで「Enemy Data List」を開くと「List is empty」となっています。

タワーディフェンス92
STEP

「+」をクリックして敵の情報を追加します。

タワーディフェンス93

以上でScriptableObjectに複数の敵データを格納することができました。わーい。

ScriptableObjectのデータを反映して敵を生成する

敵のプレハブにEnemySettingのデータを反映させて敵を生成する処理を書いていきます。これによって、1つのプレハブから異なる敵を生成できるようになります。

DBManagerスクリプトの作成

STEP

「Assets/Scripts」下に新しいC#スクリプトを作成します。名前を「DBManager」にします。

DBManagerスクリプトの内容は次のとおりです。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DBManager : MonoBehaviour
{
    public static DBManager instance;
    public EnemySetting enemySetting;

    void Awake()
    {
        if (instance == null)
        {
            instance = this;
            DontDestroyOnLoad(gameObject);
        }
        else
        {
            Destroy(gameObject);
        }
    }
}

このスクリプトはゲーム内でScriptabeObjectを一元管理するためのシングルトンクラスとして機能します。これによって、ゲーム全体で1つだけ存在するデータベース管理クラスを作成できます。

public static DBManager instanceで、DBManagerクラスのインスタンスを変数として保持します。

public EnemySetting enemySettingで、敵データが入っているEnemySettingを参照します。

Awake()メソッド内では、初回のインスタンス生成時には現在のDBManagerオブジェクトをinstanceに設定し、このオブジェクトをシーン遷移後も破棄しないように設定しています。また、すでにDBManagerが存在する場合は新しく生成されたDBManagerを破棄します。

STEP

ヒエラルキーで右クリック「Create Empty」を選択して、空のオブジェクトを作成します。名前を「DBManager」にします。

DBManagerオブジェクトにDBManagerスクリプトをドラッグ&ドロップしてアタッチします。

STEP

DBManagerオブジェクトのインスペクターの「DB Manager (Script) > Enemy Setting」「Assets/Data/EnemySetting」をドラッグ&ドロップしてアサインします。

EnemyControllerスクリプトの修正

これまで敵のパラメータはEnemyControllerスクリプトで管理していました。これをEnemySettingを参照するように変更します。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using DG.Tweening; // DOTweenを使うために必要な宣言
using System.Linq; // Linqを使うために必要な宣言

public class EnemyController : MonoBehaviour
{
    [SerializeField, Header("移動速度")]
    private float speed;
    [SerializeField, Header("最大HP")]
    private int maxHp;
    [SerializeField, Header("HP")]
    private int hp;
    private Tween tween; // DOPathメソッドの処理を代入しておく変数
    private Vector3[] path; // pathDataから取得した座標を格納するための配列
    private Animator animator; // Animatorコンポーネントの取得
    // ここから 追加
    private GameManager gameManager;
    public EnemySetting.EnemyData enemyData;
    //ここまで

    // ここから 削除
    //void Start()
    //{
    //    hp = maxHp; // 敵のHPを設定
    //    TryGetComponent(out animator); // Animatorコンポーネントを取得して代入
    //}
    //ここまで

    // ここから メソッド追加
    /// <summary>
    /// 敵データを初期化
    /// </summary>
    public void InitializeEnemy(PathData selectedPath, GameManager gameManager, EnemySetting.EnemyData enemyData)
    {
        this.enemyData = enemyData; // EnemyDataを代入
        speed = this.enemyData.speed; // 移動速度を設定
        maxHp = this.enemyData.maxHp; // 最大HPを設定
        this.gameManager = gameManager;
        hp = maxHp; // 現在のHPを設定
        TryGetComponent(out animator); // Animatorコンポーネントを取得して代入
        path = selectedPath.pathArray.Select(x => x.position).ToArray(); // 経路を取得
        float totalDistance = CalculatePathLength(path); // 経路の総距離を計算
        float moveDuration = totalDistance / enemyData.speed; // 移動時間を計算 (距離 ÷ 速度)
        // 経路に沿って移動する処理をtween変数に代入
        tween = transform.DOPath(path, moveDuration)
                         .SetEase(Ease.Linear)
                         .OnWaypointChange(x => ChangeWalkingAnimation(x));
        Debug.Log($"生成された敵: {enemyData.name}, HP: {hp}, 速度: {speed}");
    }
    //ここまで

    // ここから メソッド削除
    /// <summary>
    /// 経路情報を初期化
    /// </summary>
    //public void InitializePath(PathData pathData)
    //{
    //    // 経路を取得
    //    path = pathData.pathArray.Select(x => x.position).ToArray();
    //    // 経路の総距離を計算
    //    float totalDistance = CalculatePathLength(path);
    //    // 移動時間を計算 (距離 ÷ 速度)
    //    // (1)ここから
    //    float moveDuration = totalDistance / speed;
    //    // 経路に沿って移動する処理をtween変数に代入
    //    tween = transform.DOPath(path, moveDuration)
    //                     .SetEase(Ease.Linear)
    //                     .OnWaypointChange(x => ChangeWalkingAnimation(x));
    //}
    //ここまで

    /// <summary>
    /// 経路の総距離を計算
    /// </summary>
    /// <param name="path">経路座標の配列</param>
    /// <returns>経路の総距離</returns>
    private float CalculatePathLength(Vector3[] path)
    {
        // ...省略...
    }

    /// <summary>
    /// 敵の進行方向を取得してアニメを変更
    /// </summary>
    private void ChangeWalkingAnimation(int index)
    {
        // ...省略...

    }

    /// <summary>
    /// ダメージ計算
    /// </summary>
    /// <param name="amount"></param>
    public void CalcDamage(int amount)
    {
        // ...省略...
    }

    /// <summary>
    /// 敵の破壊
    /// </summary>
    public void DestroyEnemy()
    {
        // ...省略...
    }
}

Start()メソッドを削除して、新しくInitializeEnemy()メソッドを追加しました。

InitializeEnemy()メソッドでは、敵のパラメータを設定したり、アニメーションを設定したり、移動経路を設定したりしています。メソッド化することで外部から呼び出すことができるようになりました。

InitializePath()メソッドはInitializeEnemy()に統合したので削除します。

EnemySpawnerスクリプトの修正

ランダムな敵を生成するように修正します。最終的にはウェーブごとに指定した敵を生成したいのですが、まだウェーブを実装していないので、とりあえず。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class EnemySpawner : MonoBehaviour
{
    [SerializeField]
    private EnemyController enemyPrefab; // 敵のプレハブ
    [SerializeField]
    private PathData[] pathDataArray; // 移動経路情報の配列
    [SerializeField]
    private GameManager gameManager;

    /// <summary>
    /// 敵の生成管理
    /// </summary>
    /// <returns></returns>
    public IEnumerator ManageSpawning()
    {
        // ...省略...
    }

    /// <summary>
    /// 敵の生成
    /// </summary>
    public void Spawn()
    {
        // ランダムな経路を選択
        PathData selectedPath = pathDataArray[Random.Range(0, pathDataArray.Length)];
        // 経路のスタート地点にプレハブから敵を生成
        EnemyController enemyController = Instantiate(enemyPrefab, selectedPath.positionStart.position, Quaternion.identity);
        // ここから 追加と削除
        // ランダムな敵を選択
        int enemyId = Random.Range(0, DBManager.instance.enemySetting.enemyDataList.Count);
        // 経路情報を初期化
        //enemyController.InitializePath(selectedPath);
        // 敵データの初期化
        enemyController.InitializeEnemy(selectedPath, gameManager, DBManager.instance.enemySetting.enemyDataList[enemyId]);
        //ここまで
    }
}

34行目でEnemyDataからランダムに1つのデータを選択しています。

36行目、InitializeEnemy()に統合したのでInitializePath()は削除します。

38行目、InitializeEnemy()で敵データを初期化しています。

動作確認

ゲームを再生して動作を確認します。

Consoleタブを見ると2種類の敵(すらいむ、すね〜く)がランダムに出現しているのがわかります。敵のアニメーションを変更する処理をまだ実装していないので、見た目はすべてスライムです。

敵のアニメーションを切り替える

敵の移動アニメーションをScriptableObjectに登録して、それぞれの敵のアニメーションが正しく表示されるようにします。

敵の移動アニメーションを作成

今のところ、スライムのアニメーションしかありません。新しい敵スネークのアニメーションを作成します。

  1. アニメ用の敵画像をまとめて選択してヒエラルキービューにドラッグ&ドロップします。
  2. 「Create New Animation」ウインドウが開くので「Assets/Animations」フォルダ下に名前を「Enemy1_*」にして保存します(*は left | right | up | down)。
  3. ヒエラルキーにオブジェクトが作成されるので、インスペクターの「Sorting Layer」「Object」に変更します。これでオブジェクトが最前面に表示されます。
  4. このオブジェクトのAnimationビューで「Samples」の値を2に変更します。これはアニメーションの速度の設定です。再生ボタンを押してアニメーションを確認しましょう。
  5. ヒエラルキーのオブジェクトは削除します。
  6. 「Assets/Animations」フォルダに新しく作成されたAnimator Controllerも削除します。最初の1つ(スライムのAnimator Controller)は使うので残しておきます。

Animation Clipには「Enemy0_*」「Enemy1_*」のように通し番号をつけました。この番号はScriptableObjectのidに対応しています。

タワーディフェンス96

Animator Override Controllerを作成

Animator Override Controllerとは、既存のアニメーションをベースにして別のアニメーションで上書きする機能です。やってみよー。

STEP

「Assets/Animations」フォルダにひとつだけあるAnimator Controllerの名前を「EnemyCommon」に変更します。名前は何でもいいです。これがベースになるAnimator Controllerになります。

ちなみに、アイコンが タワーディフェンス95 なのがAnimator Controllerです。

STEP

Animator Override Controllerをそれぞれの敵に1つずつ作成します。

「Assets/Animations」で右クリック「Create > Animator Override Controller」を選択して、Animator Override Controllerを作成します。名前は「Enemy0」のように通し番号をつけます。

タワーディフェンス97
STEP

Enemy0Animator Override Controllerを選択して、インスペクターの「Controller」EnemyCommonをドラッグ&ドロップします。これがベースとなるAnimator Controllerです。

タワーディフェンス98
STEP

4方向のAnimation Clipを設定するための項目が表示されるので、それぞれにAnimation Clipをドラッグ&ドロップしてアサインします。

タワーディフェンス99

これで敵1体分のAnimator Override Controllerが完成です。敵の数だけSTEP2〜4の作業を繰り返します。

EnemySettingスクリプトの修正

STEP

EnemySettingスクリプトを修正して、EnemyDataに敵の移動アニメーションを追加します。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System; // [Serializable]を使うために必要な宣言

[CreateAssetMenu(fileName = "EnemySetting", menuName = "ScriptableObject/Enemy Setting")]
public class EnemySetting : ScriptableObject
{
    public List<EnemyData> enemyDataList = new List<EnemyData>();

    [Serializable]
    public class EnemyData
    {
        public string id;   // ID
        public string name; // 名前
        public int maxHp;   // 最大HP
        public float speed; // 移動速度
        // ここから 追加
        public AnimatorOverrideController overrideController; // 敵の移動アニメーション
        // ここまで
    }
}
STEP

「Assets > Data > EnemySetting」を選択して、インスペクターの「Override Controller」に先ほど作成したAnimator Override Controllerをドラッグ&ドロップしてアサインします。

タワーディフェンス100

EnemyControllerスクリプトの修正

EnemyControllerスクリプトを修正します。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using DG.Tweening; // DOTweenを使うために必要な宣言
using System.Linq; // Linqを使うために必要な宣言

public class EnemyController : MonoBehaviour
{
    [SerializeField, Header("移動速度")]
    private float speed;
    [SerializeField, Header("最大HP")]
    private int maxHp;
    [SerializeField, Header("HP")]
    private int hp;
    private Tween tween; // DOPathメソッドの処理を代入しておく変数
    private Vector3[] path; // pathDataから取得した座標を格納するための配列
    private Animator animator; // Animatorコンポーネントの取得
    private GameManager gameManager;
    public EnemySetting.EnemyData enemyData;

    /// <summary>
    /// 敵データを初期化
    /// </summary>
    public void InitializeEnemy(PathData selectedPath, GameManager gameManager, EnemySetting.EnemyData enemyData)
    {
        this.enemyData = enemyData; // EnemyDataを代入
        speed = this.enemyData.speed; // 移動速度を設定
        maxHp = this.enemyData.maxHp; // 最大HPを設定
        this.gameManager = gameManager;
        hp = maxHp; // 現在のHPを設定
        // ここから 修正
        //TryGetComponent(out animator); // Animatorコンポーネントを取得して代入
        if (TryGetComponent(out animator)) // Animatorコンポーネントを取得して代入
        {
            // Animatorコンポーネントが取得できたら、アニメーションの上書きをする
            SetUpAnimation();
        }
        // ここまで
        path = selectedPath.pathArray.Select(x => x.position).ToArray(); // 経路を取得
        float totalDistance = CalculatePathLength(path); // 経路の総距離を計算
        float moveDuration = totalDistance / enemyData.speed; // 移動時間を計算 (距離 ÷ 速度)
        // 経路に沿って移動する処理をtween変数に代入
        tween = transform.DOPath(path, moveDuration)
                         .SetEase(Ease.Linear)
                         .OnWaypointChange(x => ChangeWalkingAnimation(x));
        Debug.Log($"生成された敵: {enemyData.name}, HP: {hp}, 速度: {speed}");
    }

    /// <summary>
    /// 経路の総距離を計算
    /// </summary>
    /// <param name="path">経路座標の配列</param>
    /// <returns>経路の総距離</returns>
    private float CalculatePathLength(Vector3[] path)
    {
        // ...省略...
    }

    /// <summary>
    /// 敵の進行方向を取得してアニメを変更
    /// </summary>
    private void ChangeWalkingAnimation(int index)
    {
        // ...省略...

    }

    /// <summary>
    /// ダメージ計算
    /// </summary>
    /// <param name="amount"></param>
    public void CalcDamage(int amount)
    {
        // ...省略...
    }

    /// <summary>
    /// 敵の破壊
    /// </summary>
    public void DestroyEnemy()
    {
        // ...省略...
    }

    // ここから メソッドを追加
    /// <summary>
    /// アニメーションを変更
    /// </summary>
    private void SetUpAnimation()
    {
        if (enemyData.overrideController != null) // アニメーション用のデータがあれば
        {
            animator.runtimeAnimatorController = enemyData.overrideController; // アニメーションを上書きする
        }
    }
    // ここまで
}

32〜37行目、Animatorコンポーネントが取得できた場合にSetUpAnimation()メソッドを実行するように処理を修正しました。

SetUpAnimation()では、AnimatorコンポーネントのruntimeAnimatorControllerプロパティを上書きしてアニメーションを変更しています。

動作確認

動作確認をします。

スライムはスライムのアニメーション、スネークはスネークのアニメーションで表示されるようになりました。

コライダーの修正

前回、EnemyオブジェクトにBox Collider 2Dを追加して、スライムの形に合わせてコライダーの大きさを調整しました。しかしながら、それを変更する必要があります。

というのは、1つのEnemyオブジェクトのプレハブを使い回していろんな敵を生成することにしたので、敵の形が変わってしまうわけです。

敵ごとにコライダーの形を変更するのも面倒なので、どんな形の敵にも対応できるざっくりとしたコライダーを設定したいと思います。

「Assets > Prefabs > Enemy」をダブルクリックしてプレハブの編集モードに入ります。Box Collider 2Dを削除して、Circle Collider 2Dを追加します。

タワーディフェンス102

これで、どんな形の敵にも対応できるのではないでしょうか。

さいごに

今回は初めてScriptableObjectを使ってみました。いろんな場面で利用できそうです。

でわでわ

タワーディフェンスを作る(8)

この記事が気に入ったら
いいね または フォローしてね!

シェアしてね

コメント

コメントする

目次