Nested Systems?

Hi!

I’m new to DOTS (and Unity in general), and I’d like to port an existing pinball physics engine (written in C++) to ECS (also open source, BTW). Here’s an overview of how it works:

So ideally the rectangular boxes would be systems. The most-inner loop is executed at least once, but potentially multiple times. Now, for the outer loop I’ve found some hacky way to run a system component group at a fixed 1ms rate, but I’m kind of clueless how to model the inner loop.

I suppose I could just use one system for the outer loop, but each of the inner operations touches different component data, so I would have to retrieve all of these at once in the outer loop? Or is there a way to execute an entire system component group manually so the outer system drive the inner systems as a group as many times as necessary?

Or maybe I’m over-complicating things and there’s a more trivial way?

Any suggestions are appreciated!

You can update any system manually by calling Update on it (not to be confused with OnUpdate). In fact, this is what ComponentSystemGroup is doing in its default OnUpdate. You can override this behavior to do all sorts of goofy stuff, including updating systems multiple times.

Unity also has some delegate mechanism for running systems at fixed rates. However, I find their solution to be not very elegant and instead override OnUpdate to roll my own.

Thanks, that sounds like the way to go then. I could even update the outer system like that instead of relying on
FixedRateUtils.EnableFixedRateWithCatchUp which sends Unity into limbo one out of two times.

So I ended up with this:

/// <summary>
/// Main physics simulation system, executed once per frame.
/// </summary>
[UpdateBefore(typeof(TransformSystemGroup))]
public class VisualPinballSimulationSystemGroup : ComponentSystemGroup
{
    public double PhysicsDiffTime;

    public override IEnumerable<ComponentSystemBase> Systems => _systemsToUpdate;

    private long _nextPhysicsFrameTime;
    private long _currentPhysicsTime;

    private readonly List<ComponentSystemBase> _systemsToUpdate = new List<ComponentSystemBase>();
    private VisualPinballUpdateVelocitiesSystemGroup _velocitiesSystemGroup;
    private VisualPinballSimulatePhysicsCycleSystemGroup _cycleSystemGroup;
    private VisualPinballTransformSystemGroup _transformSystemGroup;

    protected override void OnCreate()
    {
        _velocitiesSystemGroup = World.DefaultGameObjectInjectionWorld.GetOrCreateSystem<VisualPinballUpdateVelocitiesSystemGroup>();
        _cycleSystemGroup = World.DefaultGameObjectInjectionWorld.GetOrCreateSystem<VisualPinballSimulatePhysicsCycleSystemGroup>();
        _transformSystemGroup = World.DefaultGameObjectInjectionWorld.GetOrCreateSystem<VisualPinballTransformSystemGroup>();

        _systemsToUpdate.Add(_velocitiesSystemGroup);
        _systemsToUpdate.Add(_cycleSystemGroup);
        _systemsToUpdate.Add(_transformSystemGroup);
    }

    protected override void OnUpdate()
    {
        const int startTimeUsec = 0;
        var initialTimeUsec = (long)(Time.ElapsedTime * 1000000);
        var curPhysicsFrameTime = _currentPhysicsTime == 0
            ? (long) (initialTimeUsec - Time.DeltaTime * 1000000)
            : _currentPhysicsTime;

        while (curPhysicsFrameTime < initialTimeUsec) {

            PhysicsDiffTime = (_nextPhysicsFrameTime - curPhysicsFrameTime) * (1.0 / PhysicsConstants.DefaultStepTime);

            // update velocities
            _velocitiesSystemGroup.Update();

            // simulate cycle
            _cycleSystemGroup.Update();

            // new cycle, on physics frame boundary
            curPhysicsFrameTime = _nextPhysicsFrameTime;

            // advance physics position
            _nextPhysicsFrameTime += PhysicsConstants.PhysicsStepTime;
        }
        _currentPhysicsTime = curPhysicsFrameTime;

        _transformSystemGroup.Update();
    }
}

The referenced system groups you see there are all tagged with
[DisableAutoCreation]. Furthermore, variables I need in other systems are retrieved like that, for example:

[DisableAutoCreation]
public class VisualPinballSimulatePhysicsCycleSystemGroup : ComponentSystemGroup
{
    public double DTime;
    protected override void OnUpdate()
    {
        var sim = World.DefaultGameObjectInjectionWorld.GetExistingSystem<VisualPinballSimulationSystemGroup>();
        DTime = sim.PhysicsDiffTime;
    }
}

Does this make sense? Are there any performance implications by doing it like that?

I guess what I’m particularly wondering: If I manually call .Update() on a system group whose children spawn jobs on worker threads, is that statement blocking, i.e. can I be sure all jobs have completed after .Update() is done?

The ECS automatic dependency tracking does not care what order systems run. As long as you don’t call Update from within a SystemBase and don’t schedule jobs from a ComponentSystemGroup, the Dependency property of every SystemBase should be correct. The ECS internals will never complete jobs unless they have to.

About your code:
World is a property of ComponentSystemGroups. Accessing DefaultGameObjectInjectionWorld could be accessing a different world than the one your ComponentSystemGroup lives in, which is typically not what you want. Other than that, if the code is doing what you want and you aren’t seeing major stalls in your Profiler Timeline, then you probably don’t need to worry about performance implications. I don’t see anything that is immediately obvious to me.

1 Like

Thanks a lot! Good to know about World, I didn’t know the systems have a World property I can use.

Hopefully last question: In the DOTS Pong tutorial, Mike doesn’t schedule anything at all, but runs all systems on the main thread. Is there any way common approach to find out what complexity of code is sufficient so a threaded execution is beneficial?

Common approach? Not really. I have determined on my machine that scheduling a job and completing it immediately has roughly a 30 microsecond overhead. But even if the work takes less than that, I often still schedule things as jobs so as to not force dependencies to complete.

Excellent info, cheers man!