Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Build And Test
on: [push]

env:
BuildVersion: '12.0.${{github.run_number}}'
BuildVersion: '13.0.${{github.run_number}}'
SolutionFile: 'src/EcsR3.sln'

jobs:
Expand Down
8 changes: 7 additions & 1 deletion docs/ecs-r3/framework/computeds.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,18 @@ Now we can all laugh at the name here, but this is basically the same as the pre

This provides you with an `IComputedEntityGroup` as the `DataSource` then you translate them into whatever you want `T` to be.

> This inherits from the lazy computed line, so it will only refresh its value when you read its `Value`(and it has awaiting changes) or you explicitly call `ForceRefresh`.

## `IComputedComponentGroup`

While this is listed as a high level `Computed` and not a convention, in reality it is a `ComputedFromEntityGroup<ReadOnlyMemory<ComponentBatch<...>>>` which is a bit of a mouthful, but it provides a really performant way to access components for `Entities`.

> For example if you have a group that requires `ComponentA`, `ComponentB` then this computed will provide `ComponentBatch<ComponentA, ComponentB>` for each entity, allowing quick lookup and processing, this is what `BatchedSystems` use under the hood to resolve components.

> This inherits from the lazy computed line, so it will only refresh its value when you read its `Value`(and it has awaiting changes) or you explicitly call `ForceRefresh`.

### `ComputedFromComponentGroup<T>`

This provides you a `IComputedComponentGroup` as the `DataSource` and lets you process it however you want into `T`.
This provides you a `IComputedComponentGroup` as the `DataSource` and lets you process it however you want into `T`.

> This inherits from the lazy computed line, so it will only refresh its value when you read its `Value`(and it has awaiting changes) or you explicitly call `ForceRefresh`.
57 changes: 57 additions & 0 deletions docs/ecs-r3/framework/systems.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,63 @@ public class BatchedExampleSystem : BatchedMixedSystem<SomeStructComponentA, Som

> Due to the MANY permutations of this that you can have its recommended that if you have very specific scenarios you just copy the code for the current `BatchedMixedSystem` and alter the signatures however you need.

### `MultiplexingSystem` + `IMultiplexedJob`

This is very much like a batched system, but it acts as a sort of multiplexer to allow you to run multiple `jobs` within one update, a `job` is basically the same as a batched systems `Process` method but standalone.

> This is really useful if you have multiple systems which all require same components and execute at same time, this ensures that it only needs to lookup the batches once and then runs all jobs back to back with the batch data, this can allow for better utilisation of CPU/Memory.

```csharp
// Make as many jobs as you want, you can also use ISystemPreProcessor/PostProcessor with it
public class Job1 : IMultiplexedJob<ClassComponent, ClassComponent2, ClassComponent3>
{
// Notice we dont give it a schedule, it is handled by the systems schedule
public void Process(Entity entity, ClassComponent component1, ClassComponent2 component2, ClassComponent3 component3)
{
component1.Position += Vector3.One;
component1.Something += 10;
component2.IsTrue = true;
component2.Value += 10;
}
}

// Notice that even though we don't use Component2 here, and the previous didnt use Component1 it doesnt matter too much as we
// still get a performance bonus due to it scheduling both things in same block
public class Job2 : IMultiplexedJob<ClassComponent, ClassComponent2, ClassComponent3>
{
public void Process(Entity entity, ClassComponent component1, ClassComponent2 component2, ClassComponent3 component3)
{
component1.Position += Vector3.One;
component1.Something += 10;
component3.IsTrue = true;
component3.Value += 10;
}
}


// This acts like a normal batched system, but it just expects you to provide it the jobs you want to run
public class ExampleMultiplexedSystem : MultiplexingBatchedSystem<ClassComponent, ClassComponent2, ClassComponent3>
{
public ExampleMultiplexedSystem(IComponentDatabase componentDatabase, IEntityComponentAccessor entityComponentAccessor, IComputedComponentGroupRegistry computedComponentGroupRegistry, IThreadHandler threadHandler) : base(componentDatabase, entityComponentAccessor, computedComponentGroupRegistry, threadHandler)
{}

// This is scheduling when all jobs should be run
protected override Observable<Unit> ReactWhen() => Observable.EveryUpdate();

// This is a simple example, but you can always DI in the jobs and pass them into here for more complex use cases
protected override IEnumerable<IMultiplexedJob<ClassComponent, ClassComponent2, ClassComponent3>> ResolveJobs()
{ return [new Job1(), new Job2()]; }
}
```
On one hand this may seem slightly more complex but in a way it also makes things a bit simpler as your Jobs are lightweight objects that can just be scheduled together and you have a smaller logic footprint.

> Remember you dont need to have 100% overlap on required components etc, there will be a tipping point but if you have several systems which all use 75% of the same components you can possibly get a decent performance bonus making them into jobs and giving them all the same components, even if a few of the jobs ignore a component or two it may still end up being more efficient than running them as fully fledged systems.

### `MultiplexingBatchedRefSystem` + `IMultiplexedRefJob`
Same as above but it lets you pass the components to jobs with `ref` keyword, mainly meant for struct scenarios.

> There is currently no mixed one but it may be added in the future, there is so many varieties of approaches to mix `ref` it is currently left to you to implement your own variants if you need them.

### `IBasicEntitySystem`

This system is like a `IBasicSystem` allowing you to process each entity within the group on every update cycle.
Expand Down
8 changes: 7 additions & 1 deletion docs/systems-r3/framework/computeds.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ Computed values are basically read only values which are proxy a data source and

## Computed Types

There are 3 default computed types available within the system:
There are 4 default computed types available within the system:

### `IComputed` (For computed single values)
Simplest computed and provides a current value and allows subscription to when the value changes, this can be very useful for precomputing things based off other data, i.e calculating MaxHp once all buffs have been taken into account.

### `ILazyComputed` (Same as above)
Same as normal Computed but only updates when value is read or `ForceRefresh` is called, triggering `OnChange` exposes `OnHasChange` as well to indicate the dependent data has changed but not been refreshed.

### `IComputedCollection` (For computed collections of data)
A reactive collection which provides an up to date collection of values and allows you to subscribe to when it changes, this could be useful for tracking all beneficial buffs on a player where the source data is just ALL buffs/debuffs on the entity.

Expand Down Expand Up @@ -64,6 +67,9 @@ var computedFirstPlaceRacer = new ComputedFirstPlace(collectionOfRacers); // inh
RacerHud.CurrentWinner.Text = computedFirstPlaceRacer.Value.Name;
```

### `LazyComputedFromData<TOutput, TInput>`
Same as previous `ComputedFromData` but a lazily evaluated variant.

### `ComputedFromObservable<TOutput, TInput>`

Much like the above `ComputedFromData` but the `DataSource` needs to be an `Observable<TInput>`, and will listen for changes on the observable and update its internal state accordingly, these are often known as **Pure Computeds** as they just proxy the underlying Observable.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public override void Setup()

public override void Cleanup()
{
EntityCollection.RemoveAll();
EntityCollection.Clear();
BatchingSystem.StopSystem();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
using System.Collections.Generic;
using System.Numerics;
using BenchmarkDotNet.Attributes;
using EcsR3.Blueprints;
using EcsR3.Components.Database;
using EcsR3.Computeds.Components.Registries;
using EcsR3.Entities;
using EcsR3.Entities.Accessors;
using EcsR3.Examples.Custom.BatchTests.Components;
using EcsR3.Extensions;
using EcsR3.Systems.Batching.Convention;
using EcsR3.Systems.Batching.Convention.Multiplexing;
using EcsR3.Systems.Batching.Convention.Multiplexing.Handlers;
using R3;
using SystemsR3.Threading;

namespace EcsR3.Benchmarks.Benchmarks;

[BenchmarkCategory("Systems")]
public class BatchVsMultiplexedClassComponentBenchmark : EcsR3Benchmark
{
public class BatchVsMultiplexedBlueprint : IBlueprint, IBatchedBlueprint
{
public void Apply(IEntityComponentAccessor entityComponentAccessor, Entity entity)
{ entityComponentAccessor.AddComponents(entity, new ClassComponent(), new ClassComponent2(), new ClassComponent3()); }

public void Apply(IEntityComponentAccessor entityComponentAccessor, Entity[] entities)
{ entityComponentAccessor.CreateComponents<ClassComponent, ClassComponent2, ClassComponent3>(entities); }
}

#region Batch Systems
public class ClassBatchSystem1 : BatchedSystem<ClassComponent, ClassComponent2>
{
public ClassBatchSystem1(IComponentDatabase componentDatabase, IEntityComponentAccessor entityComponentAccessor, IComputedComponentGroupRegistry computedComponentGroupRegistry, IThreadHandler threadHandler) : base(componentDatabase, entityComponentAccessor, computedComponentGroupRegistry, threadHandler)
{}

protected override Observable<Unit> ReactWhen() => Observable.Never<Unit>();
public void ForceRun() => ProcessBatch();
public bool UseMultithreading(bool should) => ShouldMultithread = should;

protected override void Process(Entity entity, ClassComponent component2, ClassComponent2 component3)
{
component2.Position += Vector3.One;
component2.Something += 10;
component3.IsTrue = true;
component3.Value += 10;
}
}

public class ClassBatchSystem2 : BatchedSystem<ClassComponent, ClassComponent3>
{
public ClassBatchSystem2(IComponentDatabase componentDatabase, IEntityComponentAccessor entityComponentAccessor, IComputedComponentGroupRegistry computedComponentGroupRegistry, IThreadHandler threadHandler) : base(componentDatabase, entityComponentAccessor, computedComponentGroupRegistry, threadHandler)
{}

protected override Observable<Unit> ReactWhen() => Observable.Never<Unit>();
public void ForceRun() => ProcessBatch();
public bool UseMultithreading(bool should) => ShouldMultithread = should;

protected override void Process(Entity entity, ClassComponent component2, ClassComponent3 component3)
{
component2.Position += Vector3.One;
component2.Something += 10;
component3.IsTrue = true;
component3.Value += 10;
}
}

public class ClassBatchSystem3 : BatchedSystem<ClassComponent2, ClassComponent3>
{
public ClassBatchSystem3(IComponentDatabase componentDatabase, IEntityComponentAccessor entityComponentAccessor, IComputedComponentGroupRegistry computedComponentGroupRegistry, IThreadHandler threadHandler) : base(componentDatabase, entityComponentAccessor, computedComponentGroupRegistry, threadHandler)
{}

protected override Observable<Unit> ReactWhen() => Observable.Never<Unit>();
public void ForceRun() => ProcessBatch();
public bool UseMultithreading(bool should) => ShouldMultithread = should;

protected override void Process(Entity entity, ClassComponent2 component2, ClassComponent3 component3)
{
component2.IsTrue = true;
component2.Value += 10;
component3.IsTrue = true;
component3.Value += 10;
}
}
#endregion

#region Multiplex Systems
public class Job1 : IMultiplexedJob<ClassComponent, ClassComponent2, ClassComponent3>
{
public void Process(Entity entity, ClassComponent component1, ClassComponent2 component2, ClassComponent3 component3)
{
component1.Position += Vector3.One;
component1.Something += 10;
component2.IsTrue = true;
component2.Value += 10;
}
}

public class Job2 : IMultiplexedJob<ClassComponent, ClassComponent2, ClassComponent3>
{
public void Process(Entity entity, ClassComponent component1, ClassComponent2 component2, ClassComponent3 component3)
{
component1.Position += Vector3.One;
component1.Something += 10;
component3.IsTrue = true;
component3.Value += 10;
}
}

public class Job3 : IMultiplexedJob<ClassComponent, ClassComponent2, ClassComponent3>
{
public void Process(Entity entity, ClassComponent component1, ClassComponent2 component2, ClassComponent3 component3)
{
component2.IsTrue = true;
component2.Value += 10;
component3.IsTrue = true;
component3.Value += 10;
}
}

public class ExampleMultiplexedSystem : MultiplexingBatchedSystem<ClassComponent, ClassComponent2, ClassComponent3>
{
public ExampleMultiplexedSystem(IComponentDatabase componentDatabase, IEntityComponentAccessor entityComponentAccessor, IComputedComponentGroupRegistry computedComponentGroupRegistry, IThreadHandler threadHandler) : base(componentDatabase, entityComponentAccessor, computedComponentGroupRegistry, threadHandler)
{}

protected override Observable<Unit> ReactWhen() => Observable.Never<Unit>();
public void ForceRun() => ProcessBatch();
public bool UseMultithreading(bool should) => ShouldMultithread = should;

protected override IEnumerable<IMultiplexedJob<ClassComponent, ClassComponent2, ClassComponent3>> ResolveJobs()
{ return [new Job1(), new Job2(), new Job3()]; }
}

#endregion

[Params(1000)]
public int EntityCount;

[Params(false, true)]
public bool UseMultithreading;

[Params(1000)]
public int Invocations;

public ClassBatchSystem1 BatchingSystem1 { get; private set; }
public ClassBatchSystem2 BatchingSystem2 { get; private set; }
public ClassBatchSystem3 BatchingSystem3 { get; private set; }
public ExampleMultiplexedSystem MultiplexedSystem { get; private set; }

public override void Setup()
{
BatchingSystem1 = new ClassBatchSystem1(ComponentDatabase, EntityComponentAccessor, ComputedComponentGroupRegistry, new DefaultThreadHandler());
BatchingSystem1.StartSystem();
BatchingSystem2 = new ClassBatchSystem2(ComponentDatabase, EntityComponentAccessor, ComputedComponentGroupRegistry, new DefaultThreadHandler());
BatchingSystem2.StartSystem();
BatchingSystem3 = new ClassBatchSystem3(ComponentDatabase, EntityComponentAccessor, ComputedComponentGroupRegistry, new DefaultThreadHandler());
BatchingSystem3.StartSystem();
MultiplexedSystem = new ExampleMultiplexedSystem(ComponentDatabase, EntityComponentAccessor, ComputedComponentGroupRegistry, new DefaultThreadHandler());
MultiplexedSystem.StartSystem();

EntityCollection.CreateMany<BatchVsMultiplexedBlueprint>(EntityComponentAccessor, EntityCount);
}

public override void Cleanup()
{
EntityCollection.Clear();
BatchingSystem1.StopSystem();
BatchingSystem2.StopSystem();
BatchingSystem3.StopSystem();
MultiplexedSystem.StopSystem();
}

[Benchmark]
public void ForceBatchRuns_Class()
{
BatchingSystem1.UseMultithreading(UseMultithreading);
BatchingSystem2.UseMultithreading(UseMultithreading);
BatchingSystem3.UseMultithreading(UseMultithreading);
for (var i = 0; i < Invocations; i++)
{
BatchingSystem1.ForceRun();
BatchingSystem2.ForceRun();
BatchingSystem3.ForceRun();
}
}

[Benchmark]
public void ForceMultiplexRuns_Class()
{
MultiplexedSystem.UseMultithreading(UseMultithreading);
for (var i = 0; i < Invocations; i++)
{ MultiplexedSystem.ForceRun(); }
}
}
Loading
Loading