[Unity] Project Tinyでのミニゲーム作成 - プレハブ・インスタンス作成

# はじめに
Project Tinyでミニゲームを作成時に使用した機能やトラブルを説明していきます。
今回はゲーム中のプレハブ・インスタンス作成の実装についてです。
リポジトリは[こちら](https://github.com/chocolate-ice-cream/TinyRun)。


### 実装方法

このゲームではランダムで障害物を生成する際に使用しています。
簡単に説明するとプレハブとして扱う障害物用のEntityをいくつか用意し
EntityManager.Instantiateでインスタンス作成しています。
あとは障害物が画面外にでたら削除するという、特に難しいことは行っていません。
ただ、この実装になるまでに紆余曲折あったのでそこらへんを説明していきます。

### サンプルの"Spawn and Destroy"

PackageManagerから取得できるサンプルに"Spaen and Destroy"というものがあります。
[+]ボタンをクリックすると戦闘機が倍に増えていくシンプルな内容です。![SpawnAndDestroy.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490737/2e3eced1-7b1f-a3f8-7c66-12c8d21ccfbb.gif)

最初はこれを参考に実装しようとしていたため、なかなか混乱することになりました…。

### "Spawn and Destroy"の実装

Hierarchyに戦闘機のEntityが一つだけ配置してあるシーンを用意し、
そのシーンをSceneService.LoadSceneAsyncでロードすることでインスタンスの作成を行っています。
削除の場合はSceneService.UnloadAllSceneInstancesで同一のシーンのインスタンスを全削除しています。
この方法の場合、インスタンス作成はいいんですが削除する時に同一のシーンのものが削除されてしまうのが
都合が悪かったため他の方法を探すことにしました。

### EntityManagerでのインスタンス作成

検索したらすぐ出てきました…。
感覚的にもGameObejctのInstatiateと同じように使えるし、やりたいことを実現できるのでこちらで実装することにしました。
ただ、ここでもプレハブの扱いでちょっとつまづきました。

### Prefabコンポーネント

ECSではPrefabコンポーネントがあり、これを使って実装を試しました。
Prefabコンポーネントについての詳細はF_さんの[こちらの記事](https://www.f-sp.com/entry/2019/04/19/175923#Disabled%E3%81%A8Prefab)がわかりやすいです。

クエリ作成のオプションでEntityQueryOptions.IncludePrefabを指定し
Prefabを含むようにしてみたが、何故かうまく取得出来ませんでした…。
Entityのコンポーネント設定や色々試してみましたが、結局うまくいかず最終的に次で紹介する方法で落ち着きました。

### 最終的な実装

障害物用のObstacleコンポーネントを用意し、その中にプレハブかどうか判定するboolを用意しました。

```C#:Obstacle.cs
public struct Obstacle : IComponentData {
    public bool IsPrefab;
}
```

HierarchyにObstacleコンポーネントを設定しIsPrefabをtrueにしたEntityをいくつか用意。
インスタンス管理を行うシステムでIsPrefabがtrueのEntityをプレハブとしてNativeArrayに入れておき、
インスタンス作成時にIsPrefabをfalseにすることで同じObstacleコンポーネントでも
プレハブかインスタンスかを判別できるようにしています。

```C#:SpawnAndDestroyObstacleSystem.cs
var prefabs = new NativeArray<Entity>(4, Allocator.Temp);
var count = 0;

// プレハブの取得
Entities.ForEach((Entity obstacleEntity, ref Obstacle obstacleComponent) => {
    if (obstacleComponent.IsPrefab) {
        prefabs[count] = obstacleEntity;
        ++count;
    }
});

// インスタンス作成
var spawnObstacle = EntityManager.Instantiate(prefabs[_random.NextInt(prefabs.Length)]);

var translation = EntityManager.GetComponentData<Translation>(spawnObstacle);
if (existObstacleCount == 0) {
    translation.Value = lastObstaclePos;
}
else {
    var speedScale = 1.0f;
    Entities.ForEach((ref GameState game) => {
        speedScale = game.SpeedScale;
    });
    var offset = 0.0f;
    if(speedScale < 1.75f) {
        offset = _random.NextFloat(7.5f, 10.0f);
    }
    else {
        offset = _random.NextFloat(10.0f, 12.5f);
    }

    translation.Value = new float3(lastObstaclePos.x + offset, 0.0f, 0.0f);
}
EntityManager.SetComponentData(spawnObstacle, translation);

// 座標の設定等
var scroll = EntityManager.GetComponentData<Scroll>(spawnObstacle);
var startPos = translation.Value;
scroll.StartPosition = new float2(startPos.x, startPos.y);
scroll.Distance = 100.0f + startPos.x;
scroll.Enable = true;
lastObstaclePos = startPos;
EntityManager.SetComponentData(spawnObstacle, scroll);

var obstacle = EntityManager.GetComponentData<Obstacle>(spawnObstacle);
// IsPrefabをfalseにしてインスタンスしたEntityとして扱う
obstacle.IsPrefab = false;
EntityManager.SetComponentData(spawnObstacle, obstacle);

prefabs.Dispose();
```