Search Unity

Update systems at FixedUpdate frequency (with new APIs)?

Discussion in 'Entity Component System' started by Mr-Mechanical, Apr 7, 2019.

  1. Mr-Mechanical

    Mr-Mechanical

    Joined:
    May 31, 2015
    Posts:
    507
    Before I created a systemgroup and manually updated it...
    How would I do this now? (Edit: this still works just some small API changes)

    Thanks, input is appreciated.
     
    Last edited: Apr 10, 2019
    NotaNaN likes this.
  2. Spy-Shifty

    Spy-Shifty

    Joined:
    May 5, 2011
    Posts:
    546
    In an older versions you could use the [UpdateBefore(typeof(FixedUpdate))]

    Code (CSharp):
    1.  [UpdateBefore(typeof(FixedUpdate))]
    2.     public class Test : ComponentSystem {
    3.         protected override void OnUpdate() {
    4.         }
    5.     }
    unfortunately this doesn't work at the moment :(
     
  3. 5argon

    5argon

    Joined:
    Jun 10, 2013
    Posts:
    1,555
    Right now `ScriptBehaviourUpdateOrder.UpdatePlayerLoop(World world)` is locked to put the specific 3 top system groups to a specific step, the way maybe you have to modify the ScriptBehaviourUpdateOrder.CurrentPlayerLoop that comes out of that, then use ScriptBehaviourUpdateOrder.SetPlayerLoop back again. By "modify" you would have to put your system group's update delegate in the correct subsystem like what ScriptBehaviourUpdateOrder.cs is doing (a pain, but at least now you just need to put 1 update of the whole group instead of every updates in the group's system. Great when you have to add more systems to the group too so sorting update order in the group again does not affect what the player loop is running)
     
  4. JooleanLogic

    JooleanLogic

    Joined:
    Mar 1, 2018
    Posts:
    447
    There's probably a better way but this is working for me atm.

    Code (CSharp):
    1. void Awake() {
    2.     World.Active.GetExistingSystem<FixedUpdateGroup>().Enabled = false;
    3. }
    4.  
    5. void FixedUpdate() {
    6.     var fixedUpdateGroup = World.Active.GetExistingSystem<FixedUpdateGroup>();
    7.     fixedUpdateGroup.Enabled = true;
    8.     fixedUpdateGroup.Update();
    9.     fixedUpdateGroup.Enabled = false;
    10. }
    All my fixed update systems are in FixedUpdateGroup which is in Simulation group.
     
    Krajca, phobos2077, Opeth001 and 3 others like this.
  5. StefanaUnity

    StefanaUnity

    Joined:
    Apr 9, 2019
    Posts:
    18
    So no one actually know the proper way to do it? I'm not kidding when i say this, its crossed my mind that the ECS dev doesnt even know how to write fixedupdate in ecs :D
     
    phobos2077 likes this.
  6. Roycon

    Roycon

    Joined:
    Jul 10, 2012
    Posts:
    50
    I don't think there is a proper way yet, just use one of these and then replace it in a few ECS versions when there is a proper way

    Unity will add fixed update support at some point :) we just need to wait or do one of these work arounds
     
    phobos2077 and StefanaUnity like this.
  7. JooleanLogic

    JooleanLogic

    Joined:
    Mar 1, 2018
    Posts:
    447
    To my knowledge, there currently is no proper way.
    Hybrid FixedUpdate is literally not part of ecs at the moment. They're working on it but you need your own temp solution in the meantime.

    - Edit - Roycon beat me. :)
     
    StefanaUnity likes this.
  8. StefanaUnity

    StefanaUnity

    Joined:
    Apr 9, 2019
    Posts:
    18
    Hey thanks guys I liked your post, can i get a example? I have this but still get this warning:
    Code (CSharp):
    1. Ignoring invalid [UpdateBefore] dependency for SystemFixedUpdate_: UnityEngine.Experimental.PlayerLoop.FixedUpdate must be a member of the same ComponentSystemGroup.
    2.  
    This is my attempt

    Code (CSharp):
    1. [UpdateBefore(typeof(FixedUpdate))]
    2. public class SystemFixedUpdate_ : ComponentSystem
    3. {
    4.  
    5. }
    6.  
    7.  
    8. private void FixedUpdate ()
    9. {
    10.         var updategroup = World.Active.GetExistingSystem<SystemFixedUpdate_>();
    11.         updategroup.Enabled = true;
    12.         updategroup.Update();
    13.         updategroup.Enabled = false;
    14. }
    15.  
     
  9. JooleanLogic

    JooleanLogic

    Joined:
    Mar 1, 2018
    Posts:
    447
    So atm, I don't think FixedUpdate is utilised and there's just these three parent groups in which your systems have to be. At least for the default scenario:
    InitializationSystemGroup
    SimulationSystemGroup
    PresentationSystemGroup

    I just made my own sub group 'FixedUpdateGroup' and put it in the SimulationSystemGroup.
    Code (CSharp):
    1. // FixedUpdateGroup.cs
    2. [UpdateInGroup(typeof(SimulationSystemGroup))]
    3. public class FixedUpdateGroup : ComponentSystemGroup {}
    Then I add my systems to that FixedUpdateGroup
    Code (CSharp):
    1. // MyFixedUpdateSystem.cs
    2. [UpdateInGroup(typeof(FixedUpdateGroup))]
    3. public class MyFixedUpdateSystem : BaseComponentSystem {}
    Then I add that code from post #4 above to a MonoBehaviour in the scene.
     
    phobos2077 and StefanaUnity like this.
  10. psuong

    psuong

    Joined:
    Jun 11, 2014
    Posts:
    126
    Hey so they actually reverted the change for SimulationGroup to run in Update (not FixedUpdate) until they've got more infrastructure in place.

    https://github.com/Unity-Technologi...Samples/blob/master/ReleaseNotes.md#changes-3

     
  11. james_unity988

    james_unity988

    Joined:
    Aug 10, 2018
    Posts:
    71
    This is a better solution:

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using Unity.Entities;
    4. using Unity.Physics.Systems;
    5. using UnityEngine;
    6. public class PhysicsRunner : MonoBehaviour
    7. {
    8.     private IEnumerable<ComponentSystemBase> simSystems;
    9.     void Start()
    10.     {
    11.         World.Active.GetOrCreateManager<SimulationSystemGroup>().Enabled = false;
    12.         simSystems = World.Active.GetOrCreateManager<SimulationSystemGroup>().Systems;
    13.     }
    14.     void FixedUpdate()
    15.     {
    16.         foreach(var sys in simSystems)
    17.         {
    18.             sys.Update();
    19.         }
    20.     }
    21. }
    Found here: Unity Physics Discussion
     
  12. StefanaUnity

    StefanaUnity

    Joined:
    Apr 9, 2019
    Posts:
    18
    Is it just me? I wouldn't notice it if it wasn't for my camera following the character's head in LateUpdate. Basically the camera doesn't follow the head properly, it's as if its not in LateUpdate because only LateUpdate gets animation bones transform.
     
    Last edited: Apr 11, 2019
  13. james_unity988

    james_unity988

    Joined:
    Aug 10, 2018
    Posts:
    71
    Oh, I thought you were asking about FixedUpdate not LateUpdate. If you need something to run in LateUpdate you could use a very similar approach to what I mentioned above, but I would recommend creating your own system group instead of using SimulationSystemGroup.
     
  14. cort_of_unity

    cort_of_unity

    Unity Technologies

    Joined:
    Aug 15, 2018
    Posts:
    98
    Earlier this week I added a sample demonstrating the current FixedUpdate workaround; it is basically what's described in this thread though (#11 pretty much nails it). The sample should be included in the next Entities package release.

    Re: @StefanaUnity's question about the camera lagging behind the object it's tracking -- unfortunately this is a known issue. As a workaround, the PresentationSystemGroup runs (for now) after the legacy LateUpdate player loop phase, for systems running in that group can access the most up-to-date, post-animation object locations. Some caveats:
    1. Make sure your systems are ordered before the actual built-in presentation systems, or the work will be done too late to be rendered.
    2. Note that the transformation system currently only runs in the SimulationSystemGroup, so if you modify any the transformation components of any Entities beyond that point, you'll have to manually generate an up-to-date LocalToWorld matrix for the presentation systems to consume.
     
    GilCat and james_unity988 like this.
  15. StefanaUnity

    StefanaUnity

    Joined:
    Apr 9, 2019
    Posts:
    18
    You are correct I needed FixedUpdate and I also was doing LateUpdate with the examples you guys provided
     
  16. OndrejP

    OndrejP

    Joined:
    Jul 19, 2017
    Posts:
    304
    Another variant of running SimulationUpdateGroup in fixed update.
    It finds SimulationUpdateGroup in PlayerLoop and moves it from Update to FixedUpdate.
    No need for MonoBehaviour, just place this script anywhere into project.

    Code (CSharp):
    1. using System;
    2. using Unity.Entities;
    3. using UnityEngine;
    4. using UnityEngine.Experimental.LowLevel;
    5. using UnityEngine.Experimental.PlayerLoop;
    6.  
    7. public static class FixedUpdateRunner
    8. {
    9.     [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
    10.     static void MoveSimulationGroup()
    11.     {
    12.         // This must be called AFTER DefaultWorldInitialization, otherwise DefaultWorldInitialization overwrites PlayerLoop
    13.         var playerLoop = ScriptBehaviourUpdateOrder.CurrentPlayerLoop;
    14.         var func = RemoveCallback<SimulationSystemGroup>(playerLoop);
    15.         if (func != null)
    16.         {
    17.             InstallCallback<SimulationSystemGroup>(playerLoop, typeof(FixedUpdate), func);
    18.             ScriptBehaviourUpdateOrder.SetPlayerLoop(playerLoop);
    19.         }
    20.     }
    21.  
    22.     static void InstallCallback<T>(PlayerLoopSystem playerLoop, Type subsystem, PlayerLoopSystem.UpdateFunction callback)
    23.     {
    24.         for (var i = 0; i < playerLoop.subSystemList.Length; ++i)
    25.         {
    26.             int subsystemListLength = playerLoop.subSystemList[i].subSystemList.Length;
    27.             if (playerLoop.subSystemList[i].type == subsystem)
    28.             {
    29.                 // Create new subsystem list and add callback
    30.                 var newSubsystemList = new PlayerLoopSystem[subsystemListLength + 1];
    31.                 for (var j = 0; j < subsystemListLength; j++)
    32.                 {
    33.                     newSubsystemList[j] = playerLoop.subSystemList[i].subSystemList[j];
    34.                 }
    35.                 newSubsystemList[subsystemListLength].type = typeof(FixedUpdateRunner);
    36.                 newSubsystemList[subsystemListLength].updateDelegate = callback;
    37.                 playerLoop.subSystemList[i].subSystemList = newSubsystemList;
    38.             }
    39.         }
    40.     }
    41.  
    42.     static PlayerLoopSystem.UpdateFunction RemoveCallback<T>(PlayerLoopSystem playerLoop)
    43.     {
    44.         for (var i = 0; i < playerLoop.subSystemList.Length; ++i)
    45.         {
    46.             int subsystemListLength = playerLoop.subSystemList[i].subSystemList.Length;
    47.             for (var j = 0; j < subsystemListLength; j++)
    48.             {
    49.                 var item = playerLoop.subSystemList[i].subSystemList[j];
    50.                 if (item.type == typeof(T))
    51.                 {
    52.                     playerLoop.subSystemList[i].subSystemList = ExceptIndex(playerLoop.subSystemList[i].subSystemList, j);
    53.                     return item.updateDelegate;
    54.                 }
    55.             }
    56.         }
    57.         return null;
    58.     }
    59.  
    60.     static T[] ExceptIndex<T>(T[] array, int exceptIndex)
    61.     {
    62.         T[] result = new T[array.Length - 1];
    63.         if (exceptIndex > 0)
    64.         {
    65.             Array.Copy(array, result, exceptIndex);
    66.         }
    67.         if (exceptIndex < array.Length - 1)
    68.         {
    69.             Array.Copy(array, exceptIndex + 1, result, exceptIndex, array.Length - exceptIndex - 1);
    70.         }
    71.         return result;
    72.     }
    73. }
     
  17. Mxill

    Mxill

    Joined:
    Jul 25, 2017
    Posts:
    61

    Hello its been a long time! This code you provided, how do I apply to my OnUpdate? Do I write :

    [ UpdateInGroup ( typeof ( FixedUpdate ) ) ]


    above my class? I have no idea what to do if you could help please
     
  18. eizenhorn

    eizenhorn

    Joined:
    Oct 17, 2016
    Posts:
    2,683
  19. phobos2077

    phobos2077

    Joined:
    Feb 10, 2018
    Posts:
    350
    There is another solution. If you create worlds manually (don't use default world initialization at all), you can extract code from ScriptBehaviourUpdateOrder.UpdatePlayerLoop and rewrite it in such a way that FixedUpdate is used for SimulationSystemGroup instead of normal Update. There might be some issues with this approach and it might be too complicated for most people, but for complex cases (like multiplayer games), might be a better solution than relying on MonoBehavior. Also I think if you update ComponentSystemGroup manually from MonoBehavior you won't see the system in EntityDebugger.

    Basically idea is similar to what OndrejP suggested. Except we don't have DefaultWorldInitialization in the first place.
     
    Last edited: Oct 22, 2019
  20. pekaram

    pekaram

    Joined:
    Jul 26, 2017
    Posts:
    14
  21. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,264
    Your solution only works if your framerate is higher than your FixedUpdate rate. Using a ComponentSystemGroup for FixedUpdate and updating it in a MonoBehaviour or injecting it into the FixedUpdate Subsystem in the player loop are better solutions.
     
  22. pekaram

    pekaram

    Joined:
    Jul 26, 2017
    Posts:
    14
    If you need faster fixed updates, you can calculate the missed intervals between each OnUpdate and do them in a single execution in the normal OnUpdate rate before you render. The end result will probably be the same.
     
  23. pekaram

    pekaram

    Joined:
    Jul 26, 2017
    Posts:
    14
    A simple solution, and you still use JobComponentSystem's dependency handling
     
  24. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,264
    This is even worse if you are relying on any internal Unity system that uses FixedUpdate. Not to mention you are also wastefully calculating your FixedUpdate cycles in each system when simply injecting a ComponentSystemGroup into the fixed update of a playerloop is perfectly sufficient.
     
  25. pekaram

    pekaram

    Joined:
    Jul 26, 2017
    Posts:
    14
    Lol, Do you really think a simple division to calculate how many intervals passed is such a hard operation?
     
  26. pekaram

    pekaram

    Joined:
    Jul 26, 2017
    Posts:
    14
    On another note, can you show a code example about your suggested solution to DI the systems into the player loops?
     
  27. pekaram

    pekaram

    Joined:
    Jul 26, 2017
    Posts:
    14
    Might help us to how injection works
     
  28. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,264
    So I am in the middle of rewriting my framework to work with the latest DOTS and to become the first public release, so unfortunately I don't really have much time to write up a proper example.

    So instead here is a completely untested snippet from a BootstrapTools class in my framework. UCL license derivative work.
    Code (CSharp):
    1. /// <summary>
    2.         /// Update the player loop with a world's root-level systems including FixedUpdate
    3.         /// </summary>
    4.         /// <param name="world">World with root-level systems that need insertion into the player loop</param>
    5.         /// <param name="existingPlayerLoop">Optional parameter to preserve existing player loops (e.g. ScriptBehaviourUpdateOrder.CurrentPlayerLoop)</param>
    6.         public static void UpdatePlayerLoopWithFixedUpdate(World world, PlayerLoopSystem? existingPlayerLoop = null)
    7.         {
    8.             //Use reflection to snag the private delegate needed for this
    9.             var insertMethodInfo = typeof(ScriptBehaviourUpdateOrder).GetMethod("InsertManagerIntoSubsystemList",
    10.                                                                                 System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
    11.             var simMethod   = insertMethodInfo.MakeGenericMethod(typeof(SimulationSystemGroup));
    12.             var presMethod  = insertMethodInfo.MakeGenericMethod(typeof(PresentationSystemGroup));
    13.             var initMethod  = insertMethodInfo.MakeGenericMethod(typeof(InitializationSystemGroup));
    14.             var fixedMethod = insertMethodInfo.MakeGenericMethod(typeof(FixedSimulationSystemGroup));
    15.  
    16.             var playerLoop = existingPlayerLoop ?? PlayerLoop.GetDefaultPlayerLoop();
    17.  
    18.             if (world != null)
    19.             {
    20.                 // Insert the root-level systems into the appropriate PlayerLoopSystem subsystems:
    21.                 for (var i = 0; i < playerLoop.subSystemList.Length; ++i)
    22.                 {
    23.                     int subsystemListLength = playerLoop.subSystemList[i].subSystemList.Length;
    24.                     if (playerLoop.subSystemList[i].type == typeof(Update))
    25.                     {
    26.                         var newSubsystemList = new PlayerLoopSystem[subsystemListLength + 1];
    27.                         for (var j = 0; j < subsystemListLength; ++j)
    28.                             newSubsystemList[j] = playerLoop.subSystemList[i].subSystemList[j];
    29.                         simMethod.Invoke(null, new object[] {newSubsystemList,
    30.                                                              subsystemListLength + 0, world.GetOrCreateSystem<SimulationSystemGroup>() });
    31.                         playerLoop.subSystemList[i].subSystemList = newSubsystemList;
    32.                     }
    33.                     else if (playerLoop.subSystemList[i].type == typeof(PreLateUpdate))
    34.                     {
    35.                         var newSubsystemList = new PlayerLoopSystem[subsystemListLength + 1];
    36.                         for (var j = 0; j < subsystemListLength; ++j)
    37.                             newSubsystemList[j] = playerLoop.subSystemList[i].subSystemList[j];
    38.                         presMethod.Invoke(null, new object[] {newSubsystemList,
    39.                                                               subsystemListLength + 0, world.GetOrCreateSystem<PresentationSystemGroup>() });
    40.                         playerLoop.subSystemList[i].subSystemList = newSubsystemList;
    41.                     }
    42.                     else if (playerLoop.subSystemList[i].type == typeof(Initialization))
    43.                     {
    44.                         var newSubsystemList = new PlayerLoopSystem[subsystemListLength + 1];
    45.                         for (var j = 0; j < subsystemListLength; ++j)
    46.                             newSubsystemList[j] = playerLoop.subSystemList[i].subSystemList[j];
    47.                         initMethod.Invoke(null, new object[] {newSubsystemList,
    48.                                                               subsystemListLength + 0, world.GetOrCreateSystem<InitializationSystemGroup>() });
    49.                         playerLoop.subSystemList[i].subSystemList = newSubsystemList;
    50.                     }
    51.                     else if (playerLoop.subSystemList[i].type == typeof(FixedUpdate))
    52.                     {
    53.                         var newSubsystemList = new PlayerLoopSystem[subsystemListLength + 1];
    54.                         for (var j = 0; j < subsystemListLength; ++j)
    55.                             newSubsystemList[j] = playerLoop.subSystemList[i].subSystemList[j];
    56.                         fixedMethod.Invoke(null, new object[] {newSubsystemList,
    57.                                                                subsystemListLength + 0, world.GetOrCreateSystem<FixedSimulationSystemGroup>() });
    58.                         playerLoop.subSystemList[i].subSystemList = newSubsystemList;
    59.                     }
    60.                 }
    61.             }
    62.  
    63.             ScriptBehaviourUpdateOrder.SetPlayerLoop(playerLoop);
    64.         }
     
    florianhanke likes this.
  29. pekaram

    pekaram

    Joined:
    Jul 26, 2017
    Posts:
    14
    As a temp solution for getting FixedUpdate feels like an overkill. I personally would advice for a simpler solution.
     
  30. pekaram

    pekaram

    Joined:
    Jul 26, 2017
    Posts:
    14
    Not to mention that you ditched the simple dependency handling provided in JobComponentSystem
     
  31. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,264
    Deep breath
    Beggars can't be choosers.

    But personally, I think a method, a ComponentSystemGroup, and a couple of tweaks to the Unity sample bootstrap is a pretty small price to pay to mirror the functionality of SimulationSystemGroup that runs in FixedUpdate instead. There's one gotcha with the bootstrap in which you need to make sure FixedSimulationSystemGroup is created but not auto-injected into SimulationSystemGroup.

    This is simply not true. If you want a system to run in FixedUpdate, you just add the attribute
    Code (CSharp):
    1. [UpdateInGroup(typeof(FixedSimulationSystemGroup))]
    UpdateBefore/After rules apply as well.
     
    florianhanke likes this.
  32. pekaram

    pekaram

    Joined:
    Jul 26, 2017
    Posts:
    14
    It's just about avoid wasting time writing extra complex code for nothing =)
     
  33. pekaram

    pekaram

    Joined:
    Jul 26, 2017
    Posts:
    14
    If you enjoy doing that no problem, go fetch it. But I personally would go a simple temp solution for that Fixed update
     
  34. pekaram

    pekaram

    Joined:
    Jul 26, 2017
    Posts:
    14
    Also I think this conversation isn't useful for anyone any more. So go resume work on your framework instead
     
  35. pekaram

    pekaram

    Joined:
    Jul 26, 2017
    Posts:
    14
    I mean for a person very busy for giving a proper example to help others. You waste some good amount of time on those comments =D
     
  36. pekaram

    pekaram

    Joined:
    Jul 26, 2017
    Posts:
    14
    You are busy, but addicted to just making posts but not giving proper code examples?
     
  37. pekaram

    pekaram

    Joined:
    Jul 26, 2017
    Posts:
    14
    Good luck with the your framework man ;) although I kind of think you are wasting time for nothing.
     
  38. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,264
    I write posts to take short breaks from the very technical work I am doing. However, I don't have a great workflow to test and validate examples I might write up during those break periods, so that's why I don't post tested examples.

    Anyways, if anyone has questions regarding how the code I posted is supposed to work or how to use it with a Bootstrap, I would be more than happy to answer questions.
     
  39. Mr-Mechanical

    Mr-Mechanical

    Joined:
    May 31, 2015
    Posts:
    507
    Hello,

    My solution creates a null reference exception and no longer works:

    Code (CSharp):
    1.     [DisableAutoCreation]
    2.     public class ExampleSystemGroup : ComponentSystemGroup {}
    3.  
    4. void FixedUpdate()
    5.     {
    6.         //group = World.Active.GetOrCreateManager<ExampleSystemGroup>();
    7.         group = World.DefaultGameObjectInjectionWorld.GetOrCreateSystem<ExampleSystemGroup>();
    8.         group.Update();
    9.     }
    10.  
    11.  
    null reference exception happens when group is assigned.

    any thoughts?
     
  40. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,264
    Are you using an ICustomBootstrap? You need to assign the DefaultGameObjectInjectionWorld in the bootstrap if you are.
     
    Mr-Mechanical likes this.
  41. Mr-Mechanical

    Mr-Mechanical

    Joined:
    May 31, 2015
    Posts:
    507
    Fascinating. How do I use ICustomBootstrap? I am unfamiliar with this API.
     
  42. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,264
    If you aren't using it then I don't know what your problem is. ICustomBootstrap is an interface you implement that Unity will call which lets you manually set up your own World instances and populate them with systems. The best documentation for it is actually in the changelog in version 0.2.0. There's an example there. https://docs.unity3d.com/Packages/com.unity.entities@0.4/changelog/CHANGELOG.html

    There's also examples of custom bootstraps with the networking stuff, although I don't find them to be very straightforward.

    In my framework I released a few weeks ago, I have implemented an API that lets you mix and match rule-based system ordering and explicit system ordering among other things. While I haven't tested all the code paths enough, I do know that the template at least works. It injects Unity systems and anything subclassing RootSuperSystem and relies on explicit system initialization for everything else. On the topic of FixedUpdate, the code I posted above can also be found in there. I still haven't properly tested it. I mostly wrote it to get the idea down on virtual paper. Though if its broken I'd be happy to fix it given enough information about the problem.
    Template: https://github.com/Dreaming381/Lati...ore/Core.Editor/ScriptTemplates/Bootstrap.txt
    API: https://github.com/Dreaming381/Latios-Framework/blob/master/Core/Core/Framework/BootstrapTools.cs
     
    Mr-Mechanical likes this.