【Unity】ECSを使用しているプロジェクトでIL2CPPビルドする際の注意点

UnityのECS(EntityComponentSystem)に関する小ネタです。
**※注意点として記事中で触れるECSについてはPureECSを前提としたものとなります。(Hybridについては触れません)**

entitiesパッケージに含まれている既存のComponentSystem(`EndFrameTransformSystem`/`RenderingSystemBootstrap`)を用いるとした場合、IL2CPPビルド時にて注意点があるのでそちらについて解説していきます。

**先に結論から言うと「既存のComponentSystemはIL2CPPビルド時のCode Strippingの影響で削られてしまうので、正しくlink.xmlを設定しておく必要がある」と言ったお話になります。**
→ 「[▽ 解決方法](https://qiita.com/mao_/items/7f97fed4533838434aff#-%E8%A7%A3%E6%B1%BA%E6%96%B9%E6%B3%95)」の項目にて解説

とりあえずは現時点の物を動かす際の参考情報としてメモ序に書き残しておきます。

### ※注意点

- ECS自体はまだpreview段階ではあるので、将来的には実装が変わって解決される可能性もあります。あくまで現時点の参考情報と言うことで。
- **ECSのWorldについて、今回のサンプルでは自前World生成を前提としております。(`UNITY_DISABLE_AUTOMATIC_SYSTEM_BOOTSTRAP`を定義)**
    - ただ、見た感じだとDefault Worldの方でも同じ問題が発生した上で同じ手順で解決できたので、この資料自体は参考にはなるかもしれません。。(デフォルトについてはそこまで深くは検証しておらず。。)
- IL2CPPの動作確認環境としては「Standalone(Windows) + IL2CPP」で確認しております。

プロジェクト一式はGitHubにアップ済みです。
[mao-test-h/ECS-IL2CPP-Sample](https://github.com/mao-test-h/ECS-IL2CPP-Sample)



### ■ 実装/動作環境

- **Unity version**
    - Unity 2018.3.0f2
- **依存パッケージ**
    - "com.unity.entities": "0.0.12-preview.21"





# ▽ サンプルについて

サンプルとしては以下の様に大量のCubeが回っているだけのシンプルな物となります。

![sample1.gif](https://qiita-image-store.s3.amazonaws.com/0/80207/86858371-137e-3333-d564-4de05a8ae8fb.gif)

実装としてはTransform及びRenderingSystemは既存の物となる`EndFrameTransformSystem`と`RenderingSystemBootstrap`を用いており、回転処理のみ自前で用意したComponentSystem(`RotationSytstem`)で回してます。

以下にソースを記載。

```csharp:Bootstrap.cs
using UnityEngine;

using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
using Unity.Rendering;

using UnityRandom = UnityEngine.Random;

public sealed class Bootstrap : MonoBehaviour
{
#pragma warning disable 0649
    // ------------------------------
    #region // Private Members(Editable)

    [SerializeField] int _maxObjectNum = 10000;
    [SerializeField] float _boundSize = 128f;
    [SerializeField] MeshInstanceRenderer _meshInstanceRenderer;

    #endregion // Private Members(Editable)
#pragma warning restore 0649

    // ----------------------------------------------------
    #region // Unity Events

    /// <summary>
    /// MonoBehaviour.Start
    /// </summary>
    void Start()
    {
        // Create World & Systems.
        World.Active = new World("Sample World");
        var entityManager = World.Active.CreateManager<EntityManager>();
        // Built-in System
        World.Active.CreateManager(typeof(EndFrameTransformSystem));
        World.Active.CreateManager(typeof(RenderingSystemBootstrap));
        // Original System
        World.Active.CreateManager(typeof(RotationSytstem));
        ScriptBehaviourUpdateOrder.UpdatePlayerLoop(World.Active);

        // Create Archetype.
        var archetype = entityManager.CreateArchetype(
            ComponentType.Create<Prefab>(),
            ComponentType.Create<Position>(),
            ComponentType.Create<Rotation>(),
            ComponentType.Create<LocalToWorld>(),
            ComponentType.Create<MeshInstanceRenderer>());

        // Create Prefab Entities.
        var prefabEntity = entityManager.CreateEntity(archetype);
        entityManager.SetSharedComponentData(prefabEntity, this._meshInstanceRenderer);

        // Create Entities.
        for (int i = 0; i < _maxObjectNum; ++i)
        {
            var entity = entityManager.Instantiate(prefabEntity);
            entityManager.SetComponentData(entity, new Position { Value = this.GetRandomPosition() });
            entityManager.SetComponentData(entity, new Rotation { Value = UnityEngine.Random.rotation });
        }
    }

    /// <summary>
    /// MonoBehaviour.OnDestroy
    /// </summary>
    void OnDestroy()
    {
        World.DisposeAllWorlds();
    }

    #endregion // Unity Events

    // ----------------------------------------------------
    #region // Private Methods

    float3 GetRandomPosition()
    {
        float halfSize = this._boundSize / 2f;
        return new float3(
            UnityRandom.Range(-halfSize, halfSize),
            UnityRandom.Range(-halfSize, halfSize),
            UnityRandom.Range(-halfSize, halfSize));
    }

    #endregion // Private Methods
}
```

```csharp:RotationSystem.cs
using UnityEngine;

using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
using Unity.Jobs;
using Unity.Burst;

/// <summary>
/// 回転するだけのシステム
/// </summary>
public sealed class RotationSytstem : JobComponentSystem
{
    [BurstCompile]
    public struct RotationJob : IJobProcessComponentData<Rotation>
    {
        float _deltaTime;
        public RotationJob(float deltaTime) => this._deltaTime = deltaTime;
        public void Execute(ref Rotation rot)
        {
            rot.Value = math.mul(
                math.normalize(rot.Value),
                quaternion.AxisAngle(math.up(), 2f * this._deltaTime));
        }
    }
    protected override JobHandle OnUpdate(JobHandle inputDeps) => new RotationJob(Time.deltaTime).Schedule(this, inputDeps);
}
```


## ▼ 問題点について

こちら、Editor上では正しく再生されますが...IL2CPPビルドを行うと以下の様に何も表示されない上にエラーが発生する状況となります。
※Standalone(windows) + IL2CPPで確認

![ss.png](https://qiita-image-store.s3.amazonaws.com/0/80207/594bbf80-bd34-f282-8599-5ba2e40aa048.png)

エラーログの内容を掘り下げると、「MissingMethodException: Constructor on type 'Unity.Transforms.EndFrameTransformSystem' not found.」と怒られており、「コンストラクタが無い」と言われている事が確認できます。[^1]

[^1]: Default Worldで動かした場合には何故かエラーログが出力されていない様に見受けられたので注意。(他の要因で不具合が発生している..?)

```text:output_log.txt
MissingMethodException: Constructor on type 'Unity.Transforms.EndFrameTransformSystem' not found.
  at System.RuntimeType.CreateInstanceImpl (System.Reflection.BindingFlags bindingAttr, System.Reflection.Binder binder, System.Object[] args, System.Globalization.CultureInfo culture, System.Object[] activationAttributes, System.Threading.StackCrawlMark& stackMark) [0x00000] in <00000000000000000000000000000000>:0 
  at System.Activator.CreateInstance (System.Type type, System.Reflection.BindingFlags bindingAttr, System.Reflection.Binder binder, System.Object[] args, System.Globalization.CultureInfo culture, System.Object[] activationAttributes) [0x00000] in <00000000000000000000000000000000>:0 
  at System.Activator.CreateInstance (System.Type type, System.Object[] args) [0x00000] in <00000000000000000000000000000000>:0 
  at Unity.Entities.World.CreateManagerInternal (System.Type type, System.Object[] constructorArguments) [0x00000] in <00000000000000000000000000000000>:0 
  at MainContents.Bootstrap.Start () [0x00000] in <00000000000000000000000000000000>:0 
 
```





# ▽ 解決方法

一先ずは`EndFrameTransformSystem`と`RenderingSystemBootstrap`を用いるという前提であれば以下の`link.xml`を設定することで解決することが出来ます。
※link.xmlについては以下のドキュメントを参照。
[Integrated development environment (IDE) support IL2CPP を使ったマネージバイトコードストリップ](https://docs.unity3d.com/ja/current/Manual/IL2CPP-BytecodeStripping.html)

```xml:linl.xml
<linker>
    <assembly fullname="Unity.Transforms">
        <type fullname="Unity.Transforms.EndFrameTransformSystem" preserve="all"/>
    </assembly>
    <assembly fullname="Unity.Rendering.Hybrid">
        <type fullname="Unity.Rendering.RenderingSystemBootstrap" preserve="all"/>
        <type fullname="Unity.Rendering.MeshInstanceRendererSystem" preserve="all"/>
        <type fullname="Unity.Rendering.LODGroupSystem" preserve="all"/>
    </assembly>
</linker>
```

以下は設定後に再度Standalone(windows) + IL2CPPでビルド/実行したものです。

![sample2.gif](https://qiita-image-store.s3.amazonaws.com/0/80207/02f99921-817e-8cf5-7a66-ec47d213db12.gif)




# ▽ 参考/関連サイト

- [Integrated development environment (IDE) support IL2CPP を使ったマネージバイトコードストリップ](https://docs.unity3d.com/ja/current/Manual/IL2CPP-BytecodeStripping.html)