The project provides extensions for the Arch.System tailored to the Unity Player Loop. It is inspired by Unity Entities' System Groups
- Make sure Unity 2022.2.9f1 is installed (the project is linked against this version)
- Create a release build
- Copy Arch.SystemGroups.dllintoPluginsdirectory of your Unity project.
- Copy Arch.SystemGroups.SourceGenerator.dllinto the desired directory of your project, exclude all platforms, add a new Label "RoslynAnalyzer" to the assembly (see Unity docs for more information)
Use the UpdateInGroup attribute on the member systems to specify which systems need to be updated in a given group.
When the attribute is specified for class that implements Arch.System.ISystem<float> a partial class for the given system is generated.
It contains methods to add a system to the World Builder with respect to its update order.
Note: Only systems that implement
Arch.System.ISystem<float>are supported as thefloatdenotes Delta Time from the Unity Player Loop.
UpdateInGroup accepts as a constructor argument a type of the system group or a custom group.
By default Arch Groups and Systems contain only BeforeUpdate, AfterUpdate and Update methods.
It's quite limiting and not aligned well with the Unity Player Loop.
System groups extend this capability and provide a predefined set of groups that are bound to a specific moment in the Unity Player Loop. Every custom group and system in order to get updated must be a child of one of the system groups directly or transitively.
Updates at the end of the Initialization phase of the player loop. Time.deltaTime is passed as an argument.
[UpdateInGroup(typeof(InitializationSystemGroup))]Updates at the end of the Update phase of the player loop. Time.deltaTime is passed as an argument.
You would normally use this group as an alternative to the Update method of the MonoBehaviour class.
[UpdateInGroup(typeof(SimulationSystemGroup))]Updates at the end of the PreLateUpdate phase of the player loop. Time.deltaTime is passed as an argument.
You would normally use this group as an alternative to the LateUpdate method of the MonoBehaviour class.
[UpdateInGroup(typeof(PresentationSystemGroup))]Updates at the end of the PostLateUpdate phase of the player loop (after Rendering). Time.deltaTime is passed as an argument.
[UpdateInGroup(typeof(PostRenderingSystemGroup))]Updates at the beginning of the FixedUpdate phase of the player loop before all fixed updates. Time.fixedDeltaTime is passed as an argument.
You would normally use this group as an alternative to the FixedUpdate method of the MonoBehaviour class (e.g. to assign Velocity to the objects that move in the current frame).
[UpdateInGroup(typeof(PhysicsSystemGroup))]Updates at the end of the FixedUpdate phase of the player loop.
[UpdateInGroup(typeof(PostPhysicsSystemGroup))]Groups are created automatically on systems creation. The method TryCreateGroup<T>(ref ArchSystemsWorldBuilder<T> worldBuilder) will be generated but you can just ignore it as it should be called from other generated code only.
To create a custom group declare an empty partial class and annotate it with UpdateInGroup attribute. The logic needed for ordering and assigning systems to the group will be autogenerated.
[UpdateInGroup(typeof(InitializationSystemGroup))]
public partial class CustomGroup1
{
    
}In some cases it can be useful to provide custom behavior for groups. For example, you might want to create a group that runs at a reduced frequency.
To do so instead of creating an empty partial class, create a class that inherits from Arch.SystemGroups.CustomGroupBase and annotate it with UpdateInGroup attribute.
If the only constructor it has is an empty one then this group will be instantiated automatically.
/// <summary>
/// Skips every other update
/// </summary>
public class ThrottleGroupBase : CustomGroupBase<float>
{
    private bool open;
    
    public override void Dispose()
    {
        DisposeInternal();
    }
    public override void Initialize()
    {
        InitializeInternal();
    }
    public override void BeforeUpdate(in float t)
    {
        // Before Update is always called first in the same frame
        open = !open;
        
        if (open)
            BeforeUpdateInternal(in t);
    }
    public override void Update(in float t)
    {
        if (open)
            UpdateInternal(in t);
    }
    public override void AfterUpdate(in float t)
    {
        if (open)
            AfterUpdateInternal(in t);
    }
}You may want to customize groups behaviour even further by providing a custom constructor.
In this case the instantiated group should be passed manually by calling InjectCustomGroup before injecting any other systems or groups dependent on it.
[UpdateInGroup(typeof(SimulationSystemGroup))]
public partial class ParametrisedThrottleGroup : ThrottleGroupBase
{
    public ParametrisedThrottleGroup(int framesToSkip) : base(framesToSkip)
    {
    }
}
_worldBuilder.InjectCustomGroup(new ParametrisedThrottleGroup(framesToSkip));
// then inject all systemsNote: If a system is injected before the custom group it is included into directly or transitively an exception of type
GroupNotFoundExceptionwill be thrown.
Note: If a group does not belong to a
System Groupthen it is detached from the Player Loop and its system won't be updated
Systems update order is controlled by UpdateAfter and UpdateBefore attributes.
- Both systems and groups can be annotated with these attributes.
- Only systems and groups annotated by UpdateInGroupare taken into consideration
- UpdateAfterand- UpdateBeforecan't contain an open generic type (e.g.- typeof(CustomSystem<>)). If you have such need, create a custom group and annotate the system with- UpdateInGroupattribute.
- It is possible to have several of them
- it is possible to place redundant attributes, they will be properly resolved/ignored
- As an argument attributes accept the system or group type
- Depth first search is used to sort systems; System Groups act as root nodes.
- Sorting happens only once on Systems Instantiation
In order to bind systems to the player loop, distribute them in groups and sort accordingly, one must use auto-generated API.
The API is generated for non-abstract generic and non-generic systems that implement Arch.System.ISystem<float>.
- 
Instantiate ArchSystemsWorldBuilderwith a desired type ofWorld. WithArchyou are most probably usingArch.Core.Worldvar worldBuilder = new ArchSystemsWorldBuilder<World>(World.Create()); The system must have a constructor with a first argument of the World type. [UpdateInGroup(typeof(InitializationSystemGroup))] [UpdateBefore(typeof(CustomGroup1))] public partial class CustomSystem1 : BaseSystem<World, float> { public CustomSystem1(World world) : base(world) { } } 
- 
Add systems to the builder. There are multiple ways of doing so: - 
Use a static Factory MethodInjectToWorldof the system and passworldBuilderasrefCustomSystem1.InjectToWorld(ref worldBuilder); If the system has arguments pass the corresponding arguments as well [UpdateInGroup(typeof(InitializationSystemGroup))] public partial class CustomSystemWithParameters1 : BaseSystem<TestWorld, float> { private readonly string _param1; private readonly int _param2; public CustomSystemWithParameters1(TestWorld world, string param1, int param2) : base(world) { _param1 = param1; _param2 = param2; } } CustomSystemWithParameters1.InjectToWorld(ref worldBuilder, "test", 1); 
- 
Invoke Extensions. For every system an extension method is generated Add{SystemName}({Arguments}). If you rename the system you will have to modify the code accordingly manually.worldBuilder.AddCustomSystemWithParameters1("test", 1) 
- 
Bulk creation. If you have many systems sharing the same constructor's signature using a bulk instantiation may be particularly beneficial Instead of writing something like worldBuilder .AddSystemCGroupAA() .AddSystemCGroupAB() .AddSystemAGroupAA() .AddSystemAGroupAB() .AddSystemBGroupAA() .AddSystemBGroupAB() .AddSystemDGroupBA() .AddSystemCGroupBA() .AddSystemCGroupBAA() .AddSystemBGroupBA() .AddSystemBGroupBB() .AddSystemBGroupBAA() .AddSystemAGroupBA() .AddSystemAGroupBB() .AddSystemAGroupBAA() .AddSystemAGroupBAB(); you can simply write an equivalent that will inject all systems (here all the systems are without any arguments) at once: worldBuilder.AddAllSystems(); For every arguments set a separate extension is generated so you can chain them like this: worldBuilder .AddAllSystems(new CustomClass1()) .AddAllSystems("test", 1) .AddAllSystems(1.0, (f, i) => { }) For generic systems such extensions are not generated. You will have to use the Factory MethodorAddSystemextension
 
- 
- 
Add as many systems as needed 
- 
Call var groupWorld = worldBuilder.Finish()to create all groups and systems, and sort them
- 
When the world creation is finalized, system groups are added to the Aggregatewhich in turn is attached to the Player Loop.By default it is assumed that the order in which system groups within the same player loop stage are executed is irrelevant. However, it might be beneficial to customize it: e.g. in case there is a reliance on the execution order or the worlds have different priority. It's achieved by passing an implementation of ISystemGroupAggregate<T>.IFactoryalong the data unique for the given world to theFinish<TAggregationData>(ISystemGroupAggregate<TAggregationData>.IFactory aggregateFactory, TAggregationData aggregationData)method.You can take a look at OrderedSystemGroupAggregate<T>as a reference. it usesTandIComparer<T>to sort system groups being added to and removed from the aggregate according to data passed on worlds creation.
- 
Call groupWorld.Initialize()to recursively initialize systems, it will be called in accordance toUpdate Order
- 
From this point all your systems are attached to the Unity Player Loop 
- 
Once the Worldshould be disposed, callgroupWorld.Dispose()to detach systems from the Player Loop
In order to minimize CPU impact it might be beneficial to introduce throttling on the System Groups level unlike the possibility to do it in CustomGroups the logic of which is executed individually per group.
There are two contracts for that: IUpdateBasedSystemGroupThrottler is responsible for systems that are executed with Unity Player Loop Update frequency, and IFixedUpdateBasedSystemGroupThrottler for systems that are executed with Unity Player Loop FixedUpdate frequency.
Just provide them as arguments to the ArchSystemsWorldBuilder constructor and they will be executed only once for each Root System Groups.
if ShouldThrottle returns true then the whole graph of the system group is executed in a throttling mode.
Within the same dependency tree systems and groups may have a finely controlled possibility to throttle. It's achieved by annotating a class by the ThrottlingEnabled attribute. Thus, it is possible to tell systems in the same group to follow throttling (that is returned by ShouldThrottle) or ignore it.
If the group is annotated with this attribute its children will throttle no matter whether ThrottlingEnabled is specified for them or not.
Similar to every other native callback (Update, LateUpdate, Awake, etc) Unity invokes Player Loop delegates as isolated calls.
Thus, if an exception occurs it does not break the whole loop but the current step only. In terms of system groups it means that the whole root group will skip an execution frame starting from the exception onwards.
It might be not exactly what a user expects when they introduce a systems pipeline.
In order to customize this behaviour it is possible to provide an implementation of ISystemGroupExceptionHandler to the ArchSystemsWorldBuilder constructor so it can tell the whole world what to do if an exception occurs: Keep running, Suspend or Dispose.
As the source generator in the project already operates with attributes it exposes such information to the user so it is possible get attributes data without reflection.
For every system and group a custom class inherited from AttributesInfoBase is generated.
It is possible to access it in two ways:
- Use a strongly-typed static Metadatafield. The instance is created lazily upon the first retrieval. So if it is not used no memory overhead will present.
- If a system inherits from PlayerLoopSystem<TWorld>the overriden methodprotected abstract AttributesInfoBase GetMetadataInternal();will be generated providing the access to theAttributesInfoclass instance. From this pointT GetAttribute<T>()andIReadOnlyList<T> GetAttributes<T>()methods are available. They don't rely on reflection either so the performance is similar to a simpleswitch/ifexpression.