I’m trying to create a generic system which calls a particular job. The system itself is responsible for updating some data (e.g. character attributes), but the logic that executes in the job can vary. E.g. I would like to be able to define some systems like:
public class ASystem : MyBaseSystem<T1> {
...
protected override JobHandle OnUpdate(JobHandle inputDeps) {
var job1 = new GenericJob() {
MethodDelegate = _Some Method_
};
}
}
public struct GenericJob: IJobForEachWithEntity<...>{
Action<int> MethodDelegate;
public void Execute(Entity entity, int index, ...) {
MethodDelegate(index);
}
}
where T1 refers to a job, and the system defines (some of) the code that gets executed. The idea behind this is to have multiple generic systems that do similar things, but where the job execution code differs slightly.
The code above won’t work, because Action is a reference type. Is there any way I can do something like the above, and pass in a method to execute from a system to the job?
I am not sure about using delegates with jobs. Neither if this is actually good idea.
But if I were you, I would simply use switch case inside job, to call relevant method, rather than passing via delegates.
Actually you can do it by using the delegate function pointer and it’s even burstable and all.
I’ve been using it for quiet a while in some specific scenarios.
The latest version of Burst has now a struct wrapper that handles it for you.
Here is a working system that i’ve put together as an example:
[AlwaysUpdateSystem]
public class SystemWithDelegate : JobComponentSystem {
public delegate int MyDelegate(int arg1, int arg2);
int Increment(int arg1, int arg2) {
return arg1 + arg2;
}
NativeArray<int> Values;
NativeArray<int> Result;
protected override void OnCreate() {
base.OnCreate();
Values = new NativeArray<int>(Enumerable.Range(0, 1000).ToArray(), Allocator.Persistent);
Result = new NativeArray<int>(Values.Length, Allocator.Persistent);
}
protected override void OnDestroy() {
base.OnDestroy();
Values.Dispose();
Result.Dispose();
}
[BurstCompile]
struct JobWithDelegate : IJobParallelFor {
public FunctionPointer<MyDelegate> Function;
public int IncrementValue;
[ReadOnly]
public NativeArray<int> Source;
[WriteOnly]
public NativeArray<int> Result;
public void Execute(int index) {
Result[index] = Function.Invoke(Source[index], IncrementValue);
}
}
[BurstCompile]
public struct CopyToNativeArrayJob<T> : IJobParallelFor where T : struct {
[ReadOnly]
public NativeArray<T> Source;
[WriteOnly]
public NativeArray<T> Target;
public void Execute(int index) {
Target[index] = Source[index];
}
}
protected override JobHandle OnUpdate(JobHandle inputDeps) {
inputDeps = new JobWithDelegate {
Function = new FunctionPointer<MyDelegate>(Marshal.GetFunctionPointerForDelegate((MyDelegate)Increment)),
IncrementValue = 2,
Source = Values,
Result = Result
}.Schedule(Values.Length, 64, inputDeps);
inputDeps = new CopyToNativeArrayJob<int> {
Source = Result,
Target = Values
}.Schedule(Values.Length, 64, inputDeps);
return inputDeps;
}
}
Thanks for the replies everyone. A while after typing this (and forgetting about the post), I came up with another solution which I don’t think has been mentioned yet ( @M_R seems to be similar, but the implementation appears to be slightly different) : using a struct to store methods.
public struct FunctionContainerStruct : IComponentData, ISomeInterface {
public void Hello(int a, int b) {
// Do something
}
}
public interface ISomeInterface {
void Hello(int a, int b)
}
And then passing the struct to the job via the system.
In the end, my system becomes:
public class HealthModificationSystem : GameplayEffectAttributeModificationSystem<HealthAttributeModifier> { }
public abstract class GameplayEffectAttributeModificationSystem<AttributeMod> : JobComponentSystem
where AttributeMod : struct, IComponentData, AttributeModifier
{
...
}
public struct HealthAttributeModifier : IComponentData, AttributeModifier {
// Implementation
}
public interface AttributeModifier {
// Methods to pass into job
}
.
and I can create systems for other character attributes by creating a new struct similar to HealthAttributeModifier.
Unity’s Entities ForEach method uses Mono.Cecil codegen to modify IL lambda delegates to work with Burst. It’s pretty interesting. Here’s a comment I found in the source code that kind of describes what’s going on: https://hatebin.com/bqrmegqhzj
You can see the reasoning behind the limitations as well.
Long story short, the lambda function is compiled into a class by default. Unity replaces that class with a struct.