Unity Physics package (DOTS) is not frame-rate independent?

Best solution

Let’s assume we need all the systems surrounding Unity Physics in SimulationSystemGroup needs to be with Unity Physics for Unity Physics to work completely like designed. Therefore, if we want Unity Physics to run on FixedUpdate, we need the entire SimulationSystemGroup to be ran every FixedUpdate, not every Update.

Now the question is how do we get SimulationSystemGroup to run every FixedUpdate? Calling SimulationSystemGroup’s .Update() every FixedUpdate would result in it running every Update, plus every FixedUpdate. There is no function to force a SimulationSystemGroup to stop running. Even then, forcing to stop would give no opportunity to update it manually, lol.

Then how does SimulationSystemGroup even update every Update? Is there some magical system or class calling its .Update() every Update? Nope. SimulationSystemGroup, PresentationSystemGroup, and InitializationSystemGroup are updated by coupling them into Unity’s PlayerLoop.

It turns out, one does not need to define UNITY_DISABLE_AUTOMATIC_SYSTEM_BOOTSTRAP_RUNTIME_WORLD in the scripting defines to override default world creation (see the World documentation for more about that define). All one needs for world creation overriding is any class/struct inheriting type ICustomBootstrap defined anywhere with a function override for public bool Initialize(string defaultWorldName). Just like for systems, usages of ICustomBootstrap is found via reflection (see 5argon’s article on worlds and system groups, and how reflection affects performance). ICustomBootstrap is found when AutomaticWorldBootstrap.cs calls DefaultWorldInitalization.Initalize, and in that function, CreateBootStrap is called, where in that function, types of ICustomBootstrap is found and used. AutomaticWorldBootstrap doesn’t call that DefaultWorldInitalization.Initalize if the disable world bootstrap defines are used.

Back to talking about making a ICustomBootstrap. A blank Initialize function does not initialize any systems into any world. ICustomBootstrap does not even provide any functions for finding and setting up Systems. I’ve found by studying DOTS Sample that a default world and setting up all the systems can be done using these functions:

using Unity.Entities;

public struct ADefaultBootstrap : ICustomBootstrap
{
    public bool Initialize(string defaultWorldName)
    {
        // Assembly reflection to find systems happens here
        var systems = DefaultWorldInitialization.GetAllSystems(WorldSystemFilterFlags.Default);

        var world = new World(defaultWorldName);
        World.DefaultGameObjectInjectionWorld = world;

        // The three root level groups are added into our blank world, and then the
        // systems are sorted into the three root level groups from associated [UpdateInGroup] attributes.
        // That is all done in this one function call.
        DefaultWorldInitialization.AddSystemsToRootLevelSystemGroups(world, systems);

        // Couple the three root level groups to Unity's PlayerLoop
        ScriptBehaviourUpdateOrder.UpdatePlayerLoop(world);

        return true;
    }
}

The call ScriptBehaviourUpdateOrder.UpdatePlayerLoop(world); is what gets SimulationSystemGroup to run every Update. Before understanding how SimulationSystemGroup can be added to the PlayerLoop, let’s look at the Entity Debugger, and check the option “Show Full Player Loop” (or look at the image below). Notice, the “root layer” is that of Unity’s update functions you can use in MonoBehaviours! And under groups Initialization, Update, and PreLateUpdate contains systems InitializationSystemGroup, SimulationSystemGroup, and PresentationSystemGroup respectively! Each of these loop points are actually children of the current instance of the PlayerLoop. Each of those loops are of type PlayerLoopSystem, and contain references to the systems that make Unity run. Unity then go through the PlayerLoopSystem instance set by PlayerLoop.SetPlayerLoop, and update the systems in order based on if the PlayerLoopSystem type is one of these structs. Trying to type out how this works makes me more confused, but I think y’all would get a better sense how the nested structure is set up based on this picture.

How ScriptBehaviourUpdateOrder.UpdatePlayerLoop(world); adds these groups to the PlayerLoop is using some array manipulation. You create a new PlayerLoopSystem of one size bigger than before, relocate all instances in the original PlayerLoopSystem to the new one, and then add the SimulationSystemGroup at the end of the new PlayerLoopSystem. So to move SimulationSystemGroup to FixedUpdate, it’d be easy as slightly modifying ScriptBehaviourUpdateOrder.UpdatePlayerLoop to say when playerLoop.subSystemList_.type == typeof(FixedUpdate), then smack it to the end of the FixedUpdate’s PlayerLoopSystem. This is a poor idea, because modifying any package doesn’t persist, and UpdatePlayerLoop uses some code that is marked for FIXME._
Therefore, the ultimate solution is to initialize the world completely normally. After that, iterate over the current PlayerLoopSystem structure, and move SimulationSystemGroup from the Update PlayerLoopSystem to the FixedUpdate PlayerLoopSystem. See the solution below:
```csharp
*using Unity.Entities;
using UnityEngine;
using UnityEngine.LowLevel;
using UnityEngine.PlayerLoop;

///


/// Migrates the premade (default) SimulationSystemGroup containing all
/// automatically added systems from the Update loop to the FixedUpdate loop.
/// This happens during world bootstrapping.
///

public struct SimulationSystemGroupFixedUpdateMigration : ICustomBootstrap
{
public bool Initialize(string defaultWorldName)
{
// Initalize the world normally
var systems = DefaultWorldInitialization.GetAllSystems(WorldSystemFilterFlags.Default);

var world = new World(defaultWorldName);
World.DefaultGameObjectInjectionWorld = world;

DefaultWorldInitialization.AddSystemsToRootLevelSystemGroups(world, systems);
ScriptBehaviourUpdateOrder.UpdatePlayerLoop(world);

// Moving SimulationSystemGroup to FixedUpdate is done in two parts.
// The PlayerLoopSystem of type SimulationSystemGroup has to be found,
// stored, and removed before adding it to the FixedUpdate PlayerLoopSystem.

PlayerLoopSystem playerLoop = ScriptBehaviourUpdateOrder.CurrentPlayerLoop;

// simulationSystem has to be constructed or compiler will complain due to
//    using non-assigned variables.
PlayerLoopSystem simulationSystem = new PlayerLoopSystem();
bool simSysFound = false;

// Find the location of the SimulationSystemGroup under the Update Loop
for (var i = 0; i < playerLoop.subSystemList.Length; ++i)
{
  int subsystemListLength = playerLoop.subSystemList[i].subSystemList.Length;

  // Find Update loop...
  if (playerLoop.subSystemList[i].type == typeof(Update))
  {
    // Pop out SimulationSystemGroup and store it temporarily
    var newSubsystemList = new PlayerLoopSystem[subsystemListLength - 1];
    int k = 0;

    for (var j = 0; j < subsystemListLength; ++j)
    {
      if (playerLoop.subSystemList[i].subSystemList[j].type == typeof(SimulationSystemGroup))
      {
        simulationSystem = playerLoop.subSystemList[i].subSystemList[j];
        simSysFound = true;
      }
      else
      {
        newSubsystemList[k] = playerLoop.subSystemList[i].subSystemList[j];
        k++;
      }
    }
    playerLoop.subSystemList[i].subSystemList = newSubsystemList;
  }
}

// This should never happen if SimulationSystemGroup was created like usual
// (or at least I think it might not happen :P )
if (!simSysFound)
  throw new System.Exception("SimulationSystemGroup was not found!");

// Round 2: find FixedUpdate...
for (var i = 0; i < playerLoop.subSystemList.Length; ++i)
{
  int subsystemListLength = playerLoop.subSystemList[i].subSystemList.Length;

  // Found FixedUpdate
  if (playerLoop.subSystemList[i].type == typeof(FixedUpdate))
  {
    // Allocate new space for stored SimulationSystemGroup
    //    PlayerLoopSystem, and place simulation group at index defined by
    //    temporary variable.
    var newSubsystemList = new PlayerLoopSystem[subsystemListLength + 1];
    int k = 0;

    int indexToPlaceSimulationSystemGroupIn = subsystemListLength;

    for (var j = 0; j < subsystemListLength + 1; ++j)
    {
      if (j == indexToPlaceSimulationSystemGroupIn)
        newSubsystemList[j] = simulationSystem;
      else
      {
        newSubsystemList[j] = playerLoop.subSystemList[i].subSystemList[k];
        k++;
      }
    }
    playerLoop.subSystemList[i].subSystemList = newSubsystemList;
  }
}

// Set the beautiful, new player loop
ScriptBehaviourUpdateOrder.SetPlayerLoop(playerLoop);

//PlayerLoopSystem newLoop = ScriptBehaviourUpdateOrder.CurrentPlayerLoop;
//DeepDive(ref newLoop);

return true;

}

void DeepDive(ref PlayerLoopSystem sys)
{
for (var i = 0; i < sys.subSystemList.Length; ++i)
{
try
{
Debug.Log(sys.subSystemList[i].type);
DeepDive(ref sys.subSystemList[i]);
}
catch { }
}
}
}*
```
(NOTE: I’ve included this function [/SIZE]DeepDive for anybody curious to see how the PlayerLoopSystem is really structured, and how to iterate over it. It’s pretty jank. Its just a recursive method of iteration (I think that’s how the Entity Debugger does it as well, but idk))
SimulationSystemGroup, along with all of its respective sub-systems, now show under FixedUpdate in EntityDebugger. YAY!!


Also, any systems manually added to SimulationSystemGroup after world bootstrapping will be also in FixedUpdate. Its nice how this works out!

5 Likes