Unity ECS スタディ - Scriptable Object を ECS で使用し、SO データを IComponent にパースします

この記事の英語は[Medium][1]個人ブログにも書いてあります。

この記事では、Unity ECS（Entity Component System）で Scriptable Object（SO）を使用して、SO データを IComponent に変換する方法について説明します。
基本を思い出してください：ECSのすべてはデータに関するものですので、「オブジェクト、継承、など」OOPに関連するすべてのものは（理論上）存在しないでしょう。そして、SOはOOPのものである（ある視点では）ので、誰かはこれをECSの内部で使用するのは奇妙だと思うかもしれません。
しかし、ゲームプレイデータを設定するためには、これは不可欠な部分です。OOPの世界でもECSの世界でも、それが必要です。
この記事では、SOを使用する非常にシンプルな方法を紹介しますが、もっと良い方法がたくさんあると確信しています。後で更新します。

## メモ：この記事を読むには、Unity ECS の基本知識が少なくとも必要です。

### Environment Setup:

* Unity Version: 2023.1.15f1

* ECS Version: 1.0.16

* Code: [https://gist.github.com/JVinceW/3f2cd97aab49b83f5e44da7542bcdd16](https://gist.github.com/JVinceW/3f2cd97aab49b83f5e44da7542bcdd16)

### シナリオ

この記事では、SOを使用して弾丸の基本的なプロパティを設定し、ファイアボタンを押したときに弾丸を発射します。この記事はプロジェクト全体をカバーしていないため、SOがどのように動作するかの一部分のみをカバーしています。

## Setup Step

 1. 弾丸の Scriptable Object（SO）の設定:

```csharp
    [CreateAssetMenu(fileName = "BulletConfiguration", menuName = "Project/Config/Bullet", order = 0)]
    public class BulletConfiguration : ScriptableObject
    {
      // Prefab that use for spawn bullet
      [SerializeField]
      private GameObject _bulletPrefab;
    
      // Speed of the bullet
      [SerializeField]
      private float _bulletSpeed;
    
      // the max distance square that bullet can move before removed
      [SerializeField]
      private float _bulletMaxDistanceSq;
    
      // This is a util method use for baking process.
      public void ConvertToUnManaged(ref BulletConfigurationComponentData data, IBaker baker)
      {
       data.BulletPrefab = baker.GetEntity(_bulletPrefab, TransformUsageFlags.Dynamic);
       data.BulletSpeed = _bulletSpeed;
       data.BulletMaxDistanceSq = _bulletMaxDistanceSq;
      }
    }
```


2. IComponentData から派生した BulletConfigurationComponentData 構造体を作成します。この構造体は BulletConfiguration SO のミラーとして機能します。

```csharp
    [BurstCompile]
    public struct BulletConfigurationComponentData : IComponentData
    {
      [ReadOnly] public Entity BulletPrefab;
      [ReadOnly] public float BulletSpeed;
      [ReadOnly] public float BulletMaxDistanceSq;
    }
```


2. 実際のSOへの参照を保持し、データをECSワールドに組み込むブリッジ MonoBehaviorを作成します。

```csharp
public class BrideAssetConfigAuthoring : MonoBehaviour
{
 [SerializeField]
 private BulletConfiguration _bulletConfiguration;

 private class BrideAssetConfigBaker : Baker<BrideAssetConfigAuthoring>
 {
  public override void Bake(BrideAssetConfigAuthoring authoring)
  {
   var entity = GetEntity(TransformUsageFlags.None);
   var playerConfiguration = new BulletConfigurationComponentData();

   // we will use the method that we already create before to parsing data into the component
   authoring._bulletConfiguration.ConvertToUnManaged(ref playerConfiguration, this);

   // Add configuration to entity
   AddComponent(entity, playerConfiguration);
  }
 }
}
```


ここまでで、基本的な設定は完了し、SOデータは使用準備が整いました。これをスポーン弾丸システム（別名FireBulletSystem）で使用します。SOはデータのみを保持すべきであると仮定するため、1つのインスタンスのみを持つべきだと考えています。そのため、エンティティを取得するために GetSingleton を使用します。

```csharp
public partial struct BulletFireSystem : ISystem
{
  [BurstCompile]
  public void OnCreate(ref SystemState state)
  {
   state.RequireForUpdate<PlayerFireBulletTag>();
   state.RequireForUpdate<BulletConfigurationComponentData>();
  }

  [BurstCompile]
  public void OnUpdate(ref SystemState state)
  {
   var ecb = new EntityCommandBuffer(Allocator.Temp);
   var configurationSingleton = SystemAPI.GetSingleton<BulletConfigurationComponentData>();
   foreach (var transform in SystemAPI.Query<LocalTransform>().WithAll<PlayerFireBulletTag>())
   {
    var projectileEntity = ecb.Instantiate(configurationSingleton.BulletPrefab);
    var projectileTransform =
     LocalTransform.FromPositionRotationScale(transform.Position, transform.Rotation, transform.Scale);
    
    // Set location for the entity
    ecb.SetComponent(projectileEntity, projectileTransform);
    
    // AddComponent
    ecb.AddComponent(projectileEntity, new BulletSpeed {
     Value = configurationSingleton.BulletSpeed
    });
    ecb.AddComponent(projectileEntity, new BulletMaxTravelDistanceSq {
     Value = configurationSingleton.BulletMaxDistanceSq
    });
   }
   ecb.Playback(state.EntityManager);
   ecb.Dispose();
  }
}
```

## 最後に

この方法を使えば、私の目標を達成できますが、試して解決したいことはまだたくさんあります。

* 制約: SOのデータは常に読み取り専用である必要があり、1つ作成して永遠に使用するべきです。このアプローチでは、データはルールが固定されていない場合に実行時に簡単に変更される可能性があります。実行時にBlobAssetにパースして読み取り専用にできるかどうかを検討しています。

* IComponentData は使えるか？それとも ISharedComponentData がより良い選択か？（まだ検討中）

しかし、個人的な感触では、SOをECSと組み合わせるためには、たくさんのものとたくさんのルールを作成する必要があります。プロセスを簡素化する方法を見つけるべきです。

  [1]: https://medium.com/@vincewang10a7/unity-ecs-study-use-scriptable-object-in-ecs-by-parsing-so-data-to-icomponent-73c5e7f228c9