Edited at

【Unity2019.1】Entity Comonent Systemで自前Worldを作成する方法【ECS】

初刀工です。


問題提起

昔私はECSで移動するビルボード風スプライトを同時に114514体表示するという記事を書きました。

その中で性能比較のために複数のWorldを構築しました。

ですが、当時のECSはversion0.0.12-preview8です。

そしてこの記事を執筆する時点でECSは今やversion0.0.12-preview29にまで育っています。

APIもかなり跡形も無く変わってきています。諸行無常。

故に今、志ある方の為に(入門記事を全面的に書き直す暇も無いので)パッチ的に記事を書きます。


環境


  • Unity2019.1.0b9


    • ECS version 0.0.12-preview.29



  • Windows 10 Home(x64)


    • Intel Core i7-8750H CPU @2.20GHz

    • RAM 16.00GB

    • Intel UHD Graphics 630

    • DirectXバージョン 12




解決策

UNITY_DISABLE_AUTOMATIC_SYSTEM_BOOTSTRAPをPlayer Settings>Other Settings>Configuration>Scripting Define Symbolsに設定してあげましょう。それが一番性能ロスなく無駄を省けます。

そして適当なMonoBehaviourに次のようなコードを書きます。

using UnityEngine;

using Unity.Entities;

public sealed class ECSManager : MonoBehaviour
{
void Start()
{
// もしもPure ECSではなくGameObjectEntityなどを使用してHybrid ECSを使いたいと言うならば、Hookする必要がある
// ただ、その場合Unity.Entities.GameObjectArrayInjectionHookインスタンスが必要だが、internalなクラスであるのでリフレクションが必須となる
// 参考までにここに書いておく
// 前回の記事ではGameObjectEntityを使用していたがGameObjectEntityは今や[Obsolete]となってしまった
// というわけで同一アセンブリにありさえすればなんでもよいのでUnity.Entities.Hybridから適当に長生きしそうな型を選ぶことにする
// var assembly = Assembly.GetAssembly(typeof());
// #pragma warning disable 0618
// InjectionHookSupport.RegisterHook(Activator.CreateInstance(assembly.GetType("Unity.Entities.GameObjectArrayInjectionHook")) as InjectionHook);
// InjectionHookSupport.RegisterHook(Activator.CreateInstance(assembly.GetType("Unity.Entities.TransformAccessArrayInjectionHook")) as InjectionHook);
// InjectionHookSupport.RegisterHook(Activator.CreateInstance(assembly.GetType("Unity.Entities.ComponentArrayInjectionHook")) as InjectionHook);
// #pragma warning restore 0618
var world = new World("NEW WORLD");
World.Active = world;

// void ScriptBehaviourUpdateOrder.UpdatePlayerLoop(World world)
// 上のAPIを利用するためには全てのSystemが下の3種類のSystemGroupに所属せねばならぬ
InitializationSystemGroup initializationSystemGroup = world.GetOrCreateManager<InitializationSystemGroup>();
SimulationSystemGroup simulationSystemGroup = world.GetOrCreateManager<SimulationSystemGroup>();
PresentationSystemGroup presentationSystemGroup = world.GetOrCreateManager<PresentationSystemGroup>();

// このようにしてSystemはSystemGroupの管理下におかれるようになったのだ!
simulationSystemGroup.AddSystemToUpdateList(world.GetOrCreateManager<HogeSystem>());
simulationSystemGroup.AddSystemToUpdateList(world.GetOrCreateManager<FugaSystem>());
simulationSystemGroup.AddSystemToUpdateList(world.GetOrCreateManager<PiyoSystem>());

// System相互の順序をSortする必要がある
// このAPIを使用することで我々はSystemの追加順を気にせずともよい
initializationSystemGroup.SortSystemUpdateList();
simulationSystemGroup.SortSystemUpdateList();
presentationSystemGroup.SortSystemUpdateList();

// UpdatePlayerLoopが以前はWorld[]を受け取っていたのが単一ワールドに限るようになった
// おそらく無駄アロケーションを嫌ったのだろう
// これについては後述するが、複数世界をこのAPIを用いて構築するのは不可能となった
ScriptBehaviourUpdateOrder.UpdatePlayerLoop(world);
}

void OnDestroy()
{
// これは以前と同じ
World.DisposeAllWorlds();
// ああ^^~ いいっすね~^^
// 文字列をねえ! アンマネージに管理する仕組みだねえ!
// Instanceはシングルトンパターンに従っているのでnullの時にアクセスすると内部でバッファを自動的に作成するゾ
WordStorage.Instance.Dispose();
WordStorage.Instance = null;
ScriptBehaviourUpdateOrder.UpdatePlayerLoop(null);
}
}


SystemGroup

SystemGroupとはUnityのPlayerLoopの各段階にべったりくっついている存在です。

各SystemGroupはPlayerLoopのUpdateやLateUpdate, FixedUpdateなどのうちのどれか1つに所属します。

そしてSystemGroupに所属する(Job)ComponentSystemをそのPlayerLoopでUpdateするのです。

故に全ての(Job)ComponentSystemはSystemGroupに所属すべきである、とされています。

[UpdateInGroup(typeof(T))] where T : SystemGroupの指定を各ComponentSystemに付与することでデフォルトワールドではSystemをSystemGroupに所属させます。

え? そんな属性書いたことないですって?

その場合はデフォルトワールドはSimulationSystemGroup(Update相当)にそのComponentSystemを所属させます。

そしてかつてとは異なりBarrierSystemは消滅しました。

代わりに各SystemGroupはBegin/End〇〇EntityCommandBufferSystemをprivate fieldとして保持するようになりました。

EntityCommandBufferについては割愛します。

覚えるべきことはデフォルトワールドを作らずWorld自作するならば、SystemGroupにSystemを登録するのが楽(事実上一択)ということです。


void ScriptBehaviourUpdateOrder.UpdatePlayerLoop(World world)

さてさて、私の記事の傾向としては関数の内部まで踏み込むことが多いので踏み込んでいきましょう。

public static void UpdatePlayerLoop(World world)

{
// ここ重要
// デフォルトループ取るのでぇ!このメソッドを使うと必ずPlayerLoopが上書きされまぁす!
// UniRx.Asyncとの食い合わせが最悪の所以
var playerLoop = PlayerLoop.GetDefaultPlayerLoop();
if (world != null)
{
// Insert the root-level systems into the appropriate PlayerLoopSystem subsystems:
for (var i = 0; i < playerLoop.subSystemList.Length; ++i)
{
int subsystemListLength = playerLoop.subSystemList[i].subSystemList.Length;
if (playerLoop.subSystemList[i].type == typeof(Update))
{
var newSubsystemList = new PlayerLoopSystem[subsystemListLength + 1];
for (var j = 0; j < subsystemListLength; ++j)
newSubsystemList[j] = playerLoop.subSystemList[i].subSystemList[j];
InsertManagerIntoSubsystemList(newSubsystemList,
subsystemListLength + 0, world.GetOrCreateManager<SimulationSystemGroup>());
playerLoop.subSystemList[i].subSystemList = newSubsystemList;
}
else if (playerLoop.subSystemList[i].type == typeof(PreLateUpdate))
{
var newSubsystemList = new PlayerLoopSystem[subsystemListLength + 1];
for (var j = 0; j < subsystemListLength; ++j)
newSubsystemList[j] = playerLoop.subSystemList[i].subSystemList[j];
InsertManagerIntoSubsystemList(newSubsystemList,
subsystemListLength + 0, world.GetOrCreateManager<PresentationSystemGroup>());
playerLoop.subSystemList[i].subSystemList = newSubsystemList;
}
else if (playerLoop.subSystemList[i].type == typeof(Initialization))
{
var newSubsystemList = new PlayerLoopSystem[subsystemListLength + 1];
for (var j = 0; j < subsystemListLength; ++j)
newSubsystemList[j] = playerLoop.subSystemList[i].subSystemList[j];
InsertManagerIntoSubsystemList(newSubsystemList,
subsystemListLength + 0, world.GetOrCreateManager<InitializationSystemGroup>());
playerLoop.subSystemList[i].subSystemList = newSubsystemList;
}
}
}

PlayerLoop.SetPlayerLoop(playerLoop);
currentPlayerLoop = playerLoop;
}

上のコードが何をやっているのかですって?

引数のworldがnullならPlayerLoopをデフォルトのそれにしています。

そしてworldがnullでないならば、Initialization, Update, PreLateUpdateの各PlayerLoop段階に対応するSystemGroup(InitializationSystemGroup, SimulationSystemGroup, PresentationSystemGroup)を追加しています。

PlayerLoopの書き換えぐらいならこのコードを参考にすれば十分我々も出来るでしょうね。


SystemGroup.SortSystemUpdateList()

[UpdateBefore][UpdateAfter]の順序付けを解釈して一列にソートするのは我々人間には面倒臭すぎる作業です。

それを肩代わりしてくれるのは本当にありがたいですね。

public class PresentationSystemGroup : ComponentSystemGroup

{
private BeginPresentationEntityCommandBufferSystem m_BeginEntityCommandBufferSystem;
private EndPresentationEntityCommandBufferSystem m_EndEntityCommandBufferSystem;
public override void SortSystemUpdateList()
{
// 旧BarrierSystem, 現EntityBufferSystem以外をSortしてから初めと終わりに付け足すようにしている
// Extract list of systems to sort (excluding built-in systems that are inserted at fixed points)
var toSort = new List<ComponentSystemBase>(m_systemsToUpdate.Count - 2);
foreach (var s in m_systemsToUpdate)
{
if (s is BeginPresentationEntityCommandBufferSystem ||
s is EndPresentationEntityCommandBufferSystem)
{
continue;
}
toSort.Add(s);
}
m_systemsToUpdate = toSort;
base.SortSystemUpdateList();
// Re-insert built-in systems to construct the final list
var finalSystemList = new List<ComponentSystemBase>(1 + m_systemsToUpdate.Count + 1);
finalSystemList.Add(m_BeginEntityCommandBufferSystem);
foreach (var s in m_systemsToUpdate)
finalSystemList.Add(s);
finalSystemList.Add(m_EndEntityCommandBufferSystem);
m_systemsToUpdate = finalSystemList;
}
}

デフォルトの他の2つのSystemGroup(InitializationSystemGroup, SimulationSystemGroup)はいずれもEntityCommandBufferを抱えているので大体似たようなコードです。

さて、では大本のComponentSystemGroupのSortSystemUpdateListを覗いてみましょう。

すごく長い上に別に読み飛ばしても構わないのでたたみます。

public virtual void SortSystemUpdateList()

{
#if !UNITY_CSHARP_TINY
lookupDictionary = null;
#endif
// Populate dictionary mapping systemType to system-and-before/after-types.
// This is clunky - it is easier to understand, and cleaner code, to
// just use a Dictionary<Type, SysAndDep>. However, Tiny doesn't currently have
// the ability to use Type as a key to a NativeHash, so we're stuck until that gets addressed.
//
// Likewise, this is important shared code. It can be done cleaner with 2 versions, but then...
// 2 sets of bugs and slightly different behavior will creep in.
//
var sysAndDep = new SysAndDep[m_systemsToUpdate.Count];

for(int i=0; i<m_systemsToUpdate.Count; ++i)
{
var sys = m_systemsToUpdate[i];
if (TypeManager.IsSystemAGroup(sys.GetType()))
{
(sys as ComponentSystemGroup).SortSystemUpdateList();
}
sysAndDep[i] = new SysAndDep
{
system = sys,
updateBefore = new List<Type>(),
nAfter = 0,
};
}
for(int i=0; i<m_systemsToUpdate.Count; ++i)
{
var sys = m_systemsToUpdate[i];
var before = TypeManager.GetSystemAttributes(sys.GetType(), typeof(UpdateBeforeAttribute));
var after = TypeManager.GetSystemAttributes(sys.GetType(), typeof(UpdateAfterAttribute));
foreach (var attr in before)
{
var dep = attr as UpdateBeforeAttribute;
int depIndex = LookupSysAndDep(dep.SystemType, sysAndDep);
if (depIndex < 0)
{
#if !UNITY_CSHARP_TINY
Debug.LogWarning("Ignoring invalid [UpdateBefore] dependency for " + sys.GetType() + ": " + dep.SystemType + " must be a member of the same ComponentSystemGroup.");
#else
Debug.LogWarning("WARNING: invalid [UpdateBefore] dependency:");
Debug.LogWarning(TypeManager.SystemName(sys.GetType()));
Debug.LogWarning(" depends on a non-sibling or non-ComponentSystem.");
#endif
continue;
}

sysAndDep[i].updateBefore.Add(dep.SystemType);
sysAndDep[depIndex].nAfter++;
}
foreach (var attr in after)
{
var dep = attr as UpdateAfterAttribute;
int depIndex = LookupSysAndDep(dep.SystemType, sysAndDep);
if (depIndex < 0)
{
#if !UNITY_CSHARP_TINY
Debug.LogWarning("Ignoring invalid [UpdateAfter] dependency for " + sys.GetType() + ": " + dep.SystemType + " must be a member of the same ComponentSystemGroup.");
#else
Debug.LogWarning("WARNING: invalid [UpdateAfter] dependency:");
Debug.LogWarning(TypeManager.SystemName(sys.GetType()));
Debug.LogWarning(" depends on a non-sibling or non-ComponentSystem.");
#endif
continue;
}
sysAndDep[depIndex].updateBefore.Add(sys.GetType());
sysAndDep[i].nAfter++;
}
}

// Clear the systems list and rebuild it in sorted order from the lookup table
var readySystems = new Heap<TypeHeapElement>(m_systemsToUpdate.Count);
m_systemsToUpdate.Clear();
for (int i = 0; i < sysAndDep.Length; ++i)
{
if (sysAndDep[i].nAfter == 0)
{
readySystems.Insert(new TypeHeapElement(i, sysAndDep[i].system.GetType()));
}
}
while (!readySystems.Empty)
{
int sysIndex = readySystems.Extract().unsortedIndex;
SysAndDep sd = sysAndDep[sysIndex];
Type sysType = sd.system.GetType();

sysAndDep[sysIndex] = new SysAndDep(); // "Remove()"
m_systemsToUpdate.Add(sd.system);
foreach (var beforeType in sd.updateBefore)
{
int beforeIndex = LookupSysAndDep(beforeType, sysAndDep);
if (beforeIndex < 0) throw new Exception("Bug in SortSystemUpdateList(), beforeIndex < 0");
if (sysAndDep[beforeIndex].nAfter <= 0) throw new Exception("Bug in SortSystemUpdateList(), nAfter <= 0");

sysAndDep[beforeIndex].nAfter--;
if (sysAndDep[beforeIndex].nAfter == 0)
{
readySystems.Insert(new TypeHeapElement(beforeIndex, sysAndDep[beforeIndex].system.GetType()));
}
}
}

for(int i=0; i<sysAndDep.Length; ++i)
{
if (sysAndDep[i].system != null)
{
// Since no System in the circular dependency would have ever been added
// to the heap, we should have values for everything in sysAndDep. Check,
// just in case.
#if ENABLE_UNITY_COLLECTIONS_CHECKS
var visitedSystems = new List<ComponentSystemBase>();
var startIndex = i;
var currentIndex = i;
while (true)
{
if (sysAndDep[currentIndex].system != null)
visitedSystems.Add(sysAndDep[currentIndex].system);

currentIndex = LookupSysAndDep(sysAndDep[currentIndex].updateBefore[0], sysAndDep);
if (currentIndex < 0 || currentIndex == startIndex || sysAndDep[currentIndex].system == null)
{
throw new CircularSystemDependencyException(visitedSystems);
}
}
#else
sysAndDep[i] = new SysAndDep();
#endif
}
}
}


私は読む気が起きませんでしたがいかがでしたか?(いずれ読みますが)

まあなんかいい感じに順序を整理してくれているとUTを信じましょう。


結論

ScriptBehaviourUpdateOrder.UpdatePlayerLoop(World world)を使う限り3つのSystemGroupに制限されますが、参考にして自作するのはそこまで難しくありませんし、やってみる価値はあると思います。


感想

急いで書いたので多分書き直します。