DOTSでマスターデータ管理

概要

csvファイル(マスターデータ)を読み込み、ゲームに反映させる、ということをUnity DOTSで行う方法をまとめました。

本記事の執筆の際に、特に次のサイトを参考にさせて頂きました。

www.f-sp.com

環境

  • Unity 2019.3.0f5
  • Entities 0.5.0 preview.17

実装

BlobAssetを使います。

次の手順に従います。

  1. アセットデータを表す構造体を定義する
  2. アロケータでアセットデータを組み立てる
  3. アセットデータの参照を取得してコンポーネントに記憶する
  4. 参照を利用してアセットデータにアクセスする

(参考 : 【Unity】 ECS まとめ(後編) - エフアンダーバー)

複数のCubeを次の表に従って配置してみます。

Position
1
2
3
4
5
6
7
8
9
10

マスターデータの準備

マスターデータとなるcsvファイルをResourcesフォルダ内に置きます。

アセットデータを表す構造体を定義する

using Unity.Entities;
using Unity.Mathematics;

public struct PositionBlobAsset
{
    public BlobArray<float3> PositionBlobArray;
}

SpawnerData

using Unity.Entities;

/// <summary>
/// CubeをSpawnする際に使うデータ
/// </summary>
public struct CubeSpawnerData : IComponentData
{
    /// <summary>
    /// 生成するPrefabのEntity
    /// </summary>
    public Entity CubeEntity;
    
    /// <summary>
    /// 読み込むデータの行数
    /// </summary>
    public int DataLength;
    
    public BlobAssetReference<PositionBlobAsset> PosBlobAssetReference;
}

Authoring

CubeSpawnerオブジェクトを作成し、ConvertToEntityCubeSpawnerAuthoringをアタッチします。

using System.Collections.Generic;
using System.Linq;
using Unity.Collections;
using Unity.Entities;
using UnityEngine;

public class CubeSpawnerAuthoring : MonoBehaviour, IConvertGameObjectToEntity, IDeclareReferencedPrefabs
{
    [SerializeField] private GameObject cubePrefab;
    
    // csvファイルの(Resources以下の)パス。拡張子の`.csv`は省略する
    private const string FilePath = "MasterData";
    // データを格納する配列のリスト
    private List<string[]> _dataArrayList;
    // 全データの行数    
    private int _dataListLength;
    // ラベル名
    private const string PosLabel = "Position";
    
    private BlobAssetReference<PositionBlobAsset> _posAssetReference;
    
    private void Awake()
    {
        // csvファイルを読みこんでArrayListにコピー
        _dataArrayList = ReadCsv.GetDataArray(FilePath).ToList();
        _dataListLength = _dataArrayList.Count;
        
        // Builderを作成
        var posBlobBuilder = new BlobBuilder(Allocator.TempJob);
        // Rootの参照を取得
        ref var posRoot = ref posBlobBuilder.ConstructRoot<PositionBlobAsset>();
        var posBuilderArray = posBlobBuilder.Allocate(ref posRoot.PositionBlobArray, _dataListLength);
        // 取得したい列が何列目かを示すIndexを取得
        var posLabelIndex = ReadCsv.GetLabelIndex(FilePath, PosLabel);

        for (var i = 0; i < _dataListLength; i++)
        {
            posBuilderArray[i] = float.Parse(_dataArrayList[i][posLabelIndex]);
        }
            
        _posAssetReference = posBlobBuilder.CreateBlobAssetReference<PositionBlobAsset>(Allocator.Persistent);
        posBlobBuilder.Dispose();
    }

    public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    {
        dstManager.AddComponentData(entity, new CubeSpawnerData()
        {
            CubeEntity = conversionSystem.GetPrimaryEntity(cubePrefab),
            DataLength = _dataListLength,
            PosBlobAssetReference = _posAssetReference
        });
    }

    public void DeclareReferencedPrefabs(List<GameObject> referencedPrefabs)
    {
        referencedPrefabs.Add(cubePrefab);
    }
}

ReadCsvクラスのコードを見る

using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEngine;

/// <summary>
/// CSVファイルからデータを読みこむクラス
/// </summary>
public static class ReadCsv
{
    /// <summary>
    /// CSVファイルからデータを読み込み、データが格納されたリストを返す
    /// </summary>
    /// <param name="filePath">CSVファイルの拡張子を除いたファイル名</param>
    public static IEnumerable<string[]> GetDataArray(string filePath)
    {
        var dataArrayList = new List<string[]>();
        var csvFile = Resources.Load(filePath) as TextAsset;
        Debug.Assert(csvFile != null, nameof(csvFile) + " != null");
        
        using (var reader = new StringReader(csvFile.text))    
        {
            // 1行目はラベルなので何もしない
            reader.ReadLine();

            while (reader.Peek() > -1)
            {
                var line = reader.ReadLine();
                Debug.Assert(line != null, nameof(line) + " != null");
                
                var elements = line.Split(',').ToArray();
                dataArrayList.Add(elements);
            }
        }

        return dataArrayList;
    }

    /// <summary>
    /// ラベル名から、そのラベルに対応する列のindexを返す
    /// </summary>
    /// <param name="filePath">CSVファイルの拡張子を除いたファイル名</param>
    /// <param name="labelName">ラベル名</param>
    public static int GetLabelIndex(string filePath, string labelName)
    {
        var csvFile = Resources.Load(filePath) as TextAsset;
        Debug.Assert(csvFile != null, nameof(csvFile) + " != null");
        
        using (var reader = new StringReader(csvFile.text))
        {
            var labelLine = reader.ReadLine();

            Debug.Assert(labelLine != null, nameof(labelLine) + " != null");
            
            var index = labelLine.Split(',').ToList().IndexOf(labelName);

            return index;
        }
    }
}

関連 : https://alberto-hojo.hatenablog.com/entry/2019/12/21/141011

SpawnSystem

using Unity.Entities;

/// <summary>
/// CubeをSpawnするタイミングで用いるタグコンポーネント
/// </summary>
public struct SpawnCubeTag : IComponentData
{
}
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Transforms;

/// <summary>
/// CubeをSpawnするSystem
/// </summary>
public class SpawnCubeSystem : JobComponentSystem
{
    private EntityCommandBufferSystem _entityCommandBufferSystem;

    protected override void OnCreate()
    {
        _entityCommandBufferSystem = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
    }

    [BurstCompile]
    private struct SpawnCubeJob : IJobForEachWithEntity<CubeSpawnerData, SpawnCubeTag>
    {
        public EntityCommandBuffer.Concurrent Concurrent;
        
        public void Execute(Entity entity, int index, ref CubeSpawnerData data, [ReadOnly] ref SpawnCubeTag tag)
        {
            ref var positionBlobArray = ref data.PosBlobAssetReference.Value.PositionBlobArray;

            for (var i = 0; i < data.DataLength; i++)
            {
                var cubeEntity = Concurrent.Instantiate(index, data.CubeEntity);
                Concurrent.SetComponent(index, cubeEntity,
                    new Translation() {Value = math.float3(0.0f, 0.0f, positionBlobArray[i])});
            }
            
            Concurrent.RemoveComponent<SpawnCubeTag>(index, entity);
        }
    }
    
    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        var jobHandle = new SpawnCubeJob()
        {
            Concurrent = _entityCommandBufferSystem.CreateCommandBuffer().ToConcurrent()
        }.Schedule(this, inputDeps);
        
        _entityCommandBufferSystem.AddJobHandleForProducer(jobHandle);

        return jobHandle;
    }
}

リポジトリはこちら

参考