Search Unity

Jobs and Delegates

Discussion in 'Entity Component System' started by Endlord, Aug 9, 2019.

  1. Endlord

    Endlord

    Joined:
    Jan 7, 2014
    Posts:
    11
    Hello,

    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:

    Code (CSharp):
    1. public class ASystem : MyBaseSystem<T1> {
    2. ...
    3.     protected override JobHandle OnUpdate(JobHandle inputDeps) {
    4.         var job1 = new GenericJob() {
    5.              MethodDelegate =  _Some Method_
    6.         };
    7.     }
    8. }
    9.  
    10. public struct GenericJob: IJobForEachWithEntity<...>{
    11.     Action<int> MethodDelegate;
    12.     public void Execute(Entity entity, int index, ...) {
    13.         MethodDelegate(index);
    14.     }
    15. }
    16.  
    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<int> 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?
     
  2. Antypodish

    Antypodish

    Joined:
    Apr 29, 2014
    Posts:
    10,778
    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.
     
  3. M_R

    M_R

    Joined:
    Apr 15, 2015
    Posts:
    559
    delegates are reference types. they are not allowed in the job.

    what you could do is (ab)use generics and generic constraints:
    Code (CSharp):
    1. interface IAction { void Execute(int index);}
    2.  
    3. class GenericSystem<TAction, ...>
    4.     where TAction : struct, IAction
    5. {
    6.     struct Job : IJobWhatever<...> {
    7.         TAction action = default;
    8.         public void Execute(...) {
    9.             action.Execute(index);
    10.         }
    11.     }
    12.     OnUpdate() => schedule Job;
    13. }
    14.  
    15. class MySystem : GenericSystem<MySystem.Action> {
    16.     struct Action : IAction {...}
    17. }
     
    tonytopper likes this.
  4. GilCat

    GilCat

    Joined:
    Sep 21, 2013
    Posts:
    676
    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:
    Code (CSharp):
    1. [AlwaysUpdateSystem]
    2. public class SystemWithDelegate : JobComponentSystem {
    3.  
    4.   public delegate int MyDelegate(int arg1, int arg2);
    5.  
    6.   int Increment(int arg1, int arg2) {
    7.     return arg1 + arg2;
    8.   }
    9.  
    10.   NativeArray<int> Values;
    11.   NativeArray<int> Result;
    12.  
    13.   protected override void OnCreate() {
    14.     base.OnCreate();
    15.     Values = new NativeArray<int>(Enumerable.Range(0, 1000).ToArray(), Allocator.Persistent);
    16.     Result = new NativeArray<int>(Values.Length, Allocator.Persistent);
    17.   }
    18.  
    19.   protected override void OnDestroy() {
    20.     base.OnDestroy();
    21.     Values.Dispose();
    22.     Result.Dispose();
    23.   }
    24.  
    25.   [BurstCompile]
    26.   struct JobWithDelegate : IJobParallelFor {
    27.     public FunctionPointer<MyDelegate> Function;
    28.     public int IncrementValue;
    29.     [ReadOnly]
    30.     public NativeArray<int> Source;
    31.     [WriteOnly]
    32.     public NativeArray<int> Result;
    33.  
    34.     public void Execute(int index) {
    35.       Result[index] = Function.Invoke(Source[index], IncrementValue);
    36.     }
    37.   }
    38.  
    39.   [BurstCompile]
    40.   public struct CopyToNativeArrayJob<T> : IJobParallelFor where T : struct {
    41.     [ReadOnly]
    42.     public NativeArray<T> Source;
    43.     [WriteOnly]
    44.     public NativeArray<T> Target;
    45.     public void Execute(int index) {
    46.       Target[index] = Source[index];
    47.     }
    48.   }
    49.  
    50.   protected override JobHandle OnUpdate(JobHandle inputDeps) {
    51.     inputDeps = new JobWithDelegate {
    52.       Function = new FunctionPointer<MyDelegate>(Marshal.GetFunctionPointerForDelegate((MyDelegate)Increment)),
    53.       IncrementValue = 2,
    54.       Source = Values,
    55.       Result = Result
    56.     }.Schedule(Values.Length, 64, inputDeps);
    57.     inputDeps = new CopyToNativeArrayJob<int> {
    58.       Source = Result,
    59.       Target = Values
    60.     }.Schedule(Values.Length, 64, inputDeps);
    61.     return inputDeps;
    62.   }
    63. }
     
    cdiggins, aDaxxas, vheren and 8 others like this.
  5. Endlord

    Endlord

    Joined:
    Jan 7, 2014
    Posts:
    11
    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.

    Code (CSharp):
    1. public struct FunctionContainerStruct : IComponentData, ISomeInterface {
    2.    public void Hello(int a, int b) {
    3.     // Do something
    4. }
    5. }
    6.  
    7. public interface ISomeInterface {
    8.     void Hello(int a, int b)
    9. }
    And then passing the struct to the job via the system.

    In the end, my system becomes:
    Code (CSharp):
    1. public class HealthModificationSystem : GameplayEffectAttributeModificationSystem<HealthAttributeModifier> { }
    2.  
    3. public abstract class GameplayEffectAttributeModificationSystem<AttributeMod> : JobComponentSystem
    4. where AttributeMod : struct, IComponentData, AttributeModifier
    5. {
    6. ...
    7. }
    8.  
    9. public struct HealthAttributeModifier : IComponentData, AttributeModifier {
    10.     // Implementation
    11. }
    12.  
    13. public interface AttributeModifier  {
    14.     // Methods to pass into job
    15. }
    16.  
    .

    and I can create systems for other character attributes by creating a new struct similar to HealthAttributeModifier.
     
  6. runner78

    runner78

    Joined:
    Mar 14, 2015
    Posts:
    792
    This example make no sense :p Why don't make it simple?
    Code (CSharp):
    1.  
    2. public void Execute(int index) {
    3.     Result[index] = Source[index] + IncrementValue;
    4. }
    5.  
    Methods who use only the parameter values, can make static
    Code (CSharp):
    1.  
    2. static int Increment(int arg1, int arg2) {
    3.     return arg1 + arg2;
    4. }
    5.  
    6. public void Execute(int index) {
    7.     Result[index] = SystemWithStatic.Increment(Source[index], IncrementValue);
    8. }
    9.  
     
  7. GilCat

    GilCat

    Joined:
    Sep 21, 2013
    Posts:
    676
    Yeah. This is only to show how delegates can be used in a job with burst. Would you prefer that I've written a full game? ;)
     
    amarcolina likes this.
  8. jasonboukheir

    jasonboukheir

    Joined:
    May 3, 2017
    Posts:
    84
    Just thought I'd pipe in.

    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.