Alright. I think the best way to expose my use case is to detail out exactly what I am doing and what is working for me right now that I am worried is going to be completely shut down in the near future. Keep in mind, I do expect my solution to evolve and grow in complexity as I challenge it to more use cases.
So first off, I do use an ICustomBootstrap. Right now it looks like this:
using System;
using System.Collections.Generic;
using Latios;
using Unity.Collections;
using Unity.Entities;
public class LatiosBootstrap : ICustomBootstrap
{
public bool Initialize(string defaultWorldName)
{
var world = new LatiosWorld(defaultWorldName);
World.DefaultGameObjectInjectionWorld = world;
var initializationSystemGroup = world.GetExistingSystem<InitializationSystemGroup>();
var simulationSystemGroup = world.GetExistingSystem<SimulationSystemGroup>();
var presentationSystemGroup = world.GetExistingSystem<PresentationSystemGroup>();
var systems = new List<Type>(DefaultWorldInitialization.GetAllSystems(WorldSystemFilterFlags.Default));
systems.RemoveSwapBack(typeof(InitializationSystemGroup));
systems.RemoveSwapBack(typeof(SimulationSystemGroup));
systems.RemoveSwapBack(typeof(PresentationSystemGroup));
BootstrapTools.InjectUnitySystems(systems, world, simulationSystemGroup);
BootstrapTools.InjectRootSuperSystems(systems, world, simulationSystemGroup);
initializationSystemGroup.SortSystemUpdateList();
simulationSystemGroup.SortSystems();
presentationSystemGroup.SortSystems();
ScriptBehaviourUpdateOrder.UpdatePlayerLoop(world);
return true;
}
}
The first thing I am doing is creating a custom subclass instance of World that has a few additional features. This World’s constructor creates LatiosWorldInitializationSystemGroup, and the default SimulationSystemGroup and PresentationSystemGroup.
LatiosWorldInitializationSystemGroup has been broken and patched with the last two Entities releases, so the code has deteriorated a bit.
using System.Collections.Generic;
using System.Linq;
using Unity.Entities;
namespace Latios.Systems
{
public class LatiosWorldInitializationSystemGroup : InitializationSystemGroup
{
private SceneManagerSystem m_sceneManager;
private MergeGlobalsSystem m_mergeGlobals;
private DestroyEntitiesOnSceneChangeSystem m_destroySystem;
private ManagedComponentsReactiveSystemGroup m_cleanupGroup;
private LatiosSyncPointGroup m_syncGroup;
private BeginInitializationEntityCommandBufferSystem m_beginECB;
private EndInitializationEntityCommandBufferSystem m_endECB;
protected override void OnCreate()
{
UseLegacySortOrder = true;
base.OnCreate();
m_sceneManager = World.CreateSystem<SceneManagerSystem>();
m_mergeGlobals = World.CreateSystem<MergeGlobalsSystem>();
m_destroySystem = World.CreateSystem<DestroyEntitiesOnSceneChangeSystem>();
m_cleanupGroup = World.CreateSystem<ManagedComponentsReactiveSystemGroup>();
m_syncGroup = World.GetOrCreateSystem<LatiosSyncPointGroup>();
}
public override void SortSystemUpdateList()
{
//m_destroySystem does not have a normal update, so don't add it to the list.
//Remove from list to add back later.
m_beginECB = World.GetExistingSystem<BeginInitializationEntityCommandBufferSystem>();
m_endECB = World.GetExistingSystem<EndInitializationEntityCommandBufferSystem>();
RemoveSystemFromUpdateList(m_beginECB);
RemoveSystemFromUpdateList(m_endECB);
RemoveSystemFromUpdateList(m_sceneManager);
RemoveSystemFromUpdateList(m_mergeGlobals);
RemoveSystemFromUpdateList(m_cleanupGroup);
RemoveSystemFromUpdateList(m_syncGroup);
base.SortSystemUpdateList();
var systems = Systems.ToList();
systems.Remove(m_beginECB);
systems.Remove(m_endECB);
// Re-insert built-in systems to construct the final list
var finalSystemList = new List<ComponentSystemBase>(5 + systems.Count);
finalSystemList.Add(m_beginECB);
finalSystemList.Add(m_sceneManager);
//Todo: MergeGlobals and CleanupGroup need to happen after scene loads and patches but before user code.
//However, there has to be a cleaner way to do this that doesn't make so many assumptions
int index;
for (index = systems.Count - 1; index >= 0; index--)
{
if (systems[index].GetType().Namespace.Contains("Unity"))
break;
}
foreach (var s in systems)
{
finalSystemList.Add(s);
if (index == 0)
{
finalSystemList.Add(m_mergeGlobals);
finalSystemList.Add(m_cleanupGroup);
finalSystemList.Add(m_syncGroup);
}
index--;
}
finalSystemList.Add(m_endECB);
var systemsToUpdate = m_systemsToUpdate;
systemsToUpdate.Clear();
/*foreach (var s in Systems)
{
RemoveSystemFromUpdateList(s);
}
base.SortSystemUpdateList();*/
foreach (var s in finalSystemList)
{
AddSystemToUpdateList(s);
}
}
protected override void OnUpdate()
{
foreach (var sys in m_systemsToUpdate)
{
sys.Update();
}
}
}
[UpdateInGroup(typeof(LatiosWorldInitializationSystemGroup))]
public class LatiosSyncPointGroup : ComponentSystemGroup
{
}
}
There are a couple of specific rules:
- BeginInitializationEntityCommandBufferSystem must always run first. This is my preferred ECB system for pretty much everything.
- SceneManagerSystem must run immediately afterwards, before any SubScene logic. This system is responsible for changing the actual scene (not the subscene).
- DestroyEntitiesOnSceneChangeSystem must not be run in a ComponentSystemGroup. It reacts to scene changes, and usually ends up running inside the SceneManagerSystem’s callstack.
- ManagedComponentsReactiveSystemGroup contains a bunch of concrete instances of reactive generic systems which maintain an Entity-NativeContainer mapping. It must run after all entities are populated from subscenes but before any other custom user code.
- LatiosSyncPointGroup was a group I had to add to get things to work in 0.10.0. Aside from the first frame of a scene, all sync points happen inside the InitializationSystemGroup.
The resulting system order looks like this:

After creating the world, back in the bootstrap, I get all systems and remove the Initialization, Simulation, and Presentation systems from the list. Then I make these two calls to a custom static class:
BootstrapTools.InjectUnitySystems(systems, world, simulationSystemGroup);
BootstrapTools.InjectRootSuperSystems(systems, world, simulationSystemGroup);
The first call injects all Unity systems into the systems groups based on attributes. It also reports that CompanionGameObjectUpdateTransformSystem, EditorCompanionGameObjectUpdateSystem, and CompanionGameObjectUpdateSystem are missing namespaces.
The second call injects top level user ComponentSystemGroups. These groups also use attribute-based ordering, and their sole purpose is to position user code points relative to existing Unity systems. I only do this because Unity’s systems use attribute ordering, but the complexity hasn’t gotten out of hand yet. I have a few other injection algorithms based on different criteria, though I haven’t needed them personally yet.
Now what happens in the RootSuperSystem is the interesting part. Here’s what that looks like:
using System;
using System.ComponentModel;
using Unity.Entities;
namespace Latios
{
public abstract class RootSuperSystem : SuperSystem
{
protected override void OnUpdate()
{
if (ShouldUpdateSystem())
base.OnUpdate();
}
}
public abstract class SuperSystem : ComponentSystemGroup, ILatiosSystem
{
public LatiosWorld latiosWorld { get; private set; }
public ManagedEntity sceneGlobalEntity => latiosWorld.sceneGlobalEntity;
public ManagedEntity worldGlobalEntity => latiosWorld.worldGlobalEntity;
public FluentQuery Fluent => this.Fluent();
public virtual bool ShouldUpdateSystem()
{
return Enabled;
}
[EditorBrowsable(EditorBrowsableState.Never)]
protected sealed override void OnCreate()
{
base.OnCreate();
if (World is LatiosWorld lWorld)
{
latiosWorld = lWorld;
}
else
{
throw new InvalidOperationException("The current world is not of type LatiosWorld required for Latios framework functionality.");
}
CreateSystems();
}
[EditorBrowsable(EditorBrowsableState.Never)]
protected override void OnUpdate()
{
foreach (var sys in Systems)
{
try
{
if (sys is ILatiosSystem latiosSys)
{
if (latiosSys.ShouldUpdateSystem())
{
sys.Enabled = true;
sys.Update();
}
else
{
sys.Enabled = false;
}
}
else
{
sys.Update();
}
}
catch (Exception e)
{
UnityEngine.Debug.LogException(e);
}
if (World.QuitUpdate)
break;
}
}
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed override void SortSystemUpdateList()
{
// Do nothing.
}
public EntityQuery GetEntityQuery(EntityQueryDesc desc) => GetEntityQuery(new EntityQueryDesc[] { desc });
#region API
protected abstract void CreateSystems();
public ComponentSystemBase GetOrCreateAndAddSystem(Type type)
{
var system = World.GetOrCreateSystem(type);
AddSystemToUpdateList(system);
return system;
}
public T GetOrCreateAndAddSystem<T>() where T : ComponentSystemBase
{
var system = World.GetOrCreateSystem<T>();
AddSystemToUpdateList(system);
return system;
}
public void SortSystemsUsingAttributes()
{
base.SortSystemUpdateList();
}
#endregion API
}
}
A SuperSystem defines an abstract class method called CreateSystems, and a utility method called GetOrCreateAndAddSystem which is usually called from inside CreateSystems implementations. The order of the systems in the group is the order this method gets called, so I manually register and order a new system with a single line of code. I can also inject an existing system into several different groups. If the system was already created, it gets reused rather than create a new instance. This all works for me because I heavily decouple my systems such that the only thing they depend on are ECB systems which have already been created by this point.
So RootSuperSystems create instances of SuperSystems, which in turn create instances of more SuperSystems and SubSystems (custom SystemBase). This leads to a top-down hierarchical creation of all my systems.
Some example snippets from my game:
[UpdateInGroup(typeof(Latios.Systems.LatiosSyncPointGroup))]
public class LsssInitializationRootSuperSystem : RootSuperSystem
{
protected override void CreateSystems()
{
GetOrCreateAndAddSystem<GameplaySyncPointSuperSystem>();
GetOrCreateAndAddSystem<TransformSystemGroup>();
GetOrCreateAndAddSystem<CompanionGameObjectUpdateTransformSystem>(); //Todo: Namespace
}
}
[UpdateInGroup(typeof(SimulationSystemGroup))]
[UpdateBefore(typeof(TransformSystemGroup))]
public class LsssPreTransformRootSuperSystem : RootSuperSystem
{
protected override void CreateSystems()
{
GetOrCreateAndAddSystem<PlayerInGameSuperSystem>();
GetOrCreateAndAddSystem<AdvanceGameplayMotionSuperSystem>();
}
}
[UpdateInGroup(typeof(SimulationSystemGroup))]
[UpdateAfter(typeof(TransformSystemGroup))]
public class LsssPostTransformRootSuperSystem : RootSuperSystem
{
protected override void CreateSystems()
{
GetOrCreateAndAddSystem<UpdateTransformSpatialQueriesSuperSystem>();
GetOrCreateAndAddSystem<AiSuperSystem>();
GetOrCreateAndAddSystem<ProcessGameplayEventsSuperSystem>();
GetOrCreateAndAddSystem<GraphicsTransformsSuperSystem>();
}
}
public class ProcessGameplayEventsSuperSystem : SuperSystem
{
protected override void CreateSystems()
{
GetOrCreateAndAddSystem<ShipVsBulletDamageSystem>();
GetOrCreateAndAddSystem<ShipVsShipDamageSystem>();
GetOrCreateAndAddSystem<ShipVsExplosionDamageSystem>();
GetOrCreateAndAddSystem<ShipVsWallDamageSystem>();
GetOrCreateAndAddSystem<BulletVsWallSystem>();
GetOrCreateAndAddSystem<CheckSpawnPointIsSafeSystem>();
GetOrCreateAndAddSystem<FireGunsSystem>();
GetOrCreateAndAddSystem<TravelThroughWormholeSystem>();
GetOrCreateAndAddSystem<UpdateTimeToLiveSystem>();
GetOrCreateAndAddSystem<DestroyShipsWithNoHealthSystem>();
GetOrCreateAndAddSystem<SpawnShipsDequeueSystem>();
}
}
Now the other aspect that I don’t have implemented is mod support. Mods would most likely be inserted into a few select ComponentSystemGroups (some customized subclass of SuperSystem in my case). Now unlike my core game code, I don’t have any knowledge of what order mods need to run in at compile time. Some mods will have optional compatibility support for other mods, and will provide a set of rules for their system order when those other mods are present. So I would need to read the rules from all the mod files, compute a new system order, and be able to set that to the ComponentSystemGroup. It is not a big deal if that really just becomes adding each system one-by-one from the pre-generated order, as long as some API exists that allows me to completely clear out all systems in a group.
Kudos if you made it through all that!