Unity-DOTSで作る2D弾幕シューティングの例 - ①データの管理について

[Unity-DOTSで作る2D弾幕シューティングの例](https://qiita.com/inukai_anubis/items/21e96a59941adab96670)の一回目です。

# データの管理について

早速Dotsから脱線しますが、このゲームでは敵のステータスやスポーン情報、
効果音のファイル名などの情報を全部ScriptableObjectで持たせます。
改竄対策としてデータ類はAssetBundleの形にして、
Addressableを利用して要所で読み出しするような形をとっています。
（枠を作ってあるだけで、実際にリモートからAssetBundleをダウンロードする実装までは作れていないですが）

ScriptableObjectにするのはそれがおそらく一番読み込みが早かろうという理由からです。
例えば、敵のステータス情報は下記のような感じになります。
敵のスコアや耐久値、アニメーションのID値というようなものをここでデータとして持っておきます。
<img width="334" alt="スクリーンショット 2020-06-08 10.03.12.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651558/7919b82e-92f4-73e7-739e-311318e5d1d4.png">

が、このままだとデータが増えていったとき、インスペクターが地獄のように長くなりますし、
変更したいデータを探すのも大変になってくるので、
編集のしやすさのため元になるデータはSQLiteデータベースで管理しています。

さきほどの敵のデータは、DB上で下記のように設定してあります。
(データの編集はDB browser for SQLiteを使用しています)
<img width="928" alt="スクリーンショット 2020-06-08 10.10.57.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651558/c22961c4-16fd-afa6-3209-792a255e4160.png">

まあ縦に長いよりは見やすいような気がしないでもない。
これをEditorスクリプトでScriptableObjectに変換しますが、
その際のSQLiteDBの読み込みにはSQLiteUnityKitを使用しています。
あくまで編集用であって、このデータベースファイル自体をゲームアプリ内に持つわけではないので、他のものでもいいかもしれません。

データ変換を実行する契機になるボタンを持つScriptableObjectを下記のような感じで別に作っておき、
<img width="335" alt="スクリーンショット 2020-06-08 10.21.59.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651558/624ad2d5-b8bb-5c20-d322-16fcd0ae528b.png">
このボタンを押すと各テーブルが変換されて指定のパスに保存されるという感じです。
SQLiteからScriptableObjectに変換するコードは下記のような感じになっています。
ここでデータ変換を担う奴のことを、Creatorと呼んでいます。

1. Creatorが敵IDの一覧を取得し、各IDごとにステータス情報を拾って構造体配列を作る
2. その構造体配列を、ScriptableObjectとして保存する

```cs
public static class EnemyStatusCreator {

    public static void Create(string dir, string assetPath, SqliteDatabase db) {

        string q = " select enemy_id from enemy_status order by enemy_id ";
        var ids = db.ExecuteQuery(q);

        List<EnemyStatusStruct> enemyProfiles = new List<EnemyStatusStruct>();
        foreach (DataRow dr in ids.Rows)
        {
            string id = (dr["enemy_id"] as int?).ToString();
            enemyProfiles.Add(EnemyStatusDataModel.Get(id, db));
        }

        Directory.CreateDirectory(dir + assetPath);
        string filepath = assetPath + "/" + ScriptableResources.ENEMY_STATUS + ".asset";

        SEnemyStatus obj = AssetDatabase.LoadAssetAtPath(filepath, typeof(SEnemyStatus)) as SEnemyStatus;
        if (obj == null)
        {
            obj = ScriptableObject.CreateInstance<SEnemyStatus>();
            AssetDatabase.CreateAsset(obj, filepath);
        }

        obj.enemies = enemyProfiles.ToArray();

        EditorUtility.SetDirty(obj);
        AssetDatabase.Refresh();
    }
}
```

```cs
[System.Serializable]
public struct EnemyStatusStruct
{
    public int enemyId;
    public int moveAnimationId;
    public int explosionAnimationId;
    public int colliderSetId;
    public int[] componentIds;
    public int mass;
    public int durability;
    public int score;
    public float maxSpeed;
}
```
```cs
public static class EnemyStatusDataModel
{
    private const string SELECT = @"
SELECT
    a.enemy_id,
    a.move_animation_id,
    a.explosion_animation_id,
    a.collider_set_id,
    a.mass,
    a.durability,
    a.score,
    a.max_speed,
    b.component_id
FROM
    enemy_status a
    JOIN enemy_component b
    ON a.enemy_id = b.enemy_id
WHERE
    a.enemy_id = {0}
";

    public static EnemyStatusStruct Get(string id, SqliteDatabase db)
    {
        // 敵の設定値を取得する
        var q = string.Format(SELECT, id);
        var profile = db.ExecuteQuery(q);
        var enemyId = (int)profile.Rows[0]["enemy_id"];
        var moveAnimationId = (int)profile.Rows[0]["move_animation_id"];
        var explosionAnimationId = (int)profile.Rows[0]["explosion_animation_id"];
        var colliderSetId = (int)profile.Rows[0]["collider_set_id"];
        var mass = (int)profile.Rows[0]["mass"];
        var durability = (int)profile.Rows[0]["durability"];
        var score = (int)profile.Rows[0]["score"];
        var maxSpeed = (float)(double)profile.Rows[0]["max_speed"];

        // 敵ごとのコンポーネント一覧を取得する
        List<int> componentIds = new List<int>();
        foreach (DataRow r in profile.Rows)
        {
            componentIds.Add((int)r["component_id"]);
        }

        var result = new EnemyStatusStruct()
        {
            enemyId = enemyId,
            moveAnimationId = moveAnimationId,
            explosionAnimationId = explosionAnimationId,
            colliderSetId = colliderSetId,
            componentIds = componentIds.ToArray(),
            mass = mass,
            durability = durability,
            score = score,
            maxSpeed = maxSpeed,
        };


        return result;
    }
}
```

## ハマったところ

### クラッシュする

これをやってからしばらくデバッグ実行するとUnityがクラッシュする現象が多発して悩まされました。
原因はScriptableObjectの定義が別のファイルについでに書いてあったことでした。
わざわざこの定義のためにファイル用意するのめんどくせーなと思って一箇所にまとめていたのがよくなかったようです。
単独のファイルに下記のように定義を書くようにすると解消しました。

```cs
public class SEnemyStatus : ScriptableObject
{
    public EnemyStatusStruct[] enemies;
}
```

### ScriptableObjectの設定値が空になる（クリアされる）

これも頻発して何なんだと思いながらしばらく素直に再生成を繰り返していましたが、アセットデータベースへの配慮が足りていなかったようです。
DBファイルを更新してScriptableObjectを再生成する場合、生成したオブジェクトにDirtyフラグをつけておかないとこの事象が発生するようです。
この設定は、下記のように実行します。

```cs
EditorUtility.SetDirty(target)
```

下記が参考になりました。
[Scriptable Object resets after Unity is closed](https://answers.unity.com/questions/1028443/scriptable-object-resets-after-unity-is-closed.html)