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.dll
intoPlugins
directory of your Unity project. - Copy
Arch.SystemGroups.SourceGenerator.dll
into 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 thefloat
denotes 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 systems
Note: If a system is injected before the custom group it is included into directly or transitively an exception of type
GroupNotFoundException
will be thrown.
Note: If a group does not belong to a
System Group
then 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
UpdateInGroup
are taken into consideration UpdateAfter
andUpdateBefore
can't contain an open generic type (e.g.typeof(CustomSystem<>)
). If you have such need, create a custom group and annotate the system withUpdateInGroup
attribute.- 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
ArchSystemsWorldBuilder
with a desired type ofWorld
. WithArch
you are most probably usingArch.Core.World
var 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 Method
InjectToWorld
of the system and passworldBuilder
asref
CustomSystem1.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 Method
orAddSystem
extension
-
-
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
Aggregate
which 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>.IFactory
along 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 usesT
andIComparer<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
World
should 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
Metadata
field. 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 theAttributesInfo
class 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
/if
expression.