Search Unity

  1. Curious about what's going to be in 2020.1? Have a look at the 2020.1 beta blog post.
    Dismiss Notice

Strategies to allow "user overrides" of functions that get called within jobs

Discussion in 'Data Oriented Technology Stack' started by PhilSA, Feb 4, 2020.

  1. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,296
    EDIT: The solution I ended up going for is described here:
    https://forum.unity.com/threads/strategies-to-allow-user-overrides-of-functions-that-get-called-within-jobs.822126/#post-5453100

    ______________________

    Alright, prepare yourselves for an immense wall of text


    I'd be curious to hear if DOTS people can think of better alternatives to what I'm about to describe

    The code-reuse scenario that I find most difficult to deal with in DOTS is this:
    • You have a complex job that you don't want to rewrite many times
    • You want to let people make several different "overrides" of that job, where a small part of the job's logic can be customized
    • IMPORTANT: Your "overrides" might need access to data that wasn't originally provided to the job. ComponentDataFromEntities, NativeArrays, etc...
    • Modifying the source code of the job is out of the question, because it would ideally live as a package and be used in many projects

    In OOP you'd just make a function virtual and let people do various overrides of it. In DOTS it's a whole different story.

    Take for example a job that does a RaycastAll, filters out some of the hits (this is the customizable part), and then operates on that list of hits. Let's say we want to be able to create many different kinds of "hit filters" for this job

    Here are the solutions I could think of:


    Separating into several jobs
    Most of the time, you might be able to separate that job into 3 jobs and that'll solve the problem:
    1. RaycastAll job
    2. Custom filtering job that is manually inserted between the other two
    3. Operations on hits job
    ...but separating into 3 jobs is not always an option. Imagine our job has to do this instead (pseudocode):

    Code (CSharp):
    1. public struct MyJob : IJobForEach<....>
    2. {
    3.     public void Execute(....)
    4.     {
    5.         for (int i = 0; i < MAX_ITERATIONS; i++)
    6.         {
    7.             allHits = RaycastAll();
    8.  
    9.             // This is the "overrideable" part
    10.             CustomFilterHits(allHits);
    11.  
    12.             bool finished = ProcessHits(allHits);
    13.  
    14.             if (finished)
    15.             {
    16.                 break;
    17.             }
    18.         }
    19.     }
    20. }
    If your RaycastAll must happen within a for loop in your job, you are in trouble. Let's say your for loop can do up to 20 iterations but might exit early in most cases. Using the previous strategy would imply scheduling 20*3 = 60 jobs and make them exit early if the ProcessHits() has determined that we're finished. Scheduling 60 jobs just for this might be too much, especially since most of them won't do anything most of the time.


    Doing a big ugly mess of generics and interfaces, but it ends up working pretty well
    Here's the solution I've found:
    • You create an abstract MySystem<T> where T : IHitFilterer, IComponentData
    • you write your DoRaycastsJob in MySystem and make it operate on entities that have the T component
    • you create a specific child implementation of MySystem with the real filterer type. Let's say MySpecificSystem : MySystem<RigidbodyHitsFilterer>
    • "RigidbodyHitsFilterer" implements a "ShouldAcceptHit(hit, entity, etc...)" that gets called by the DoRaycastsJob for each hit, and is in charge of returning true if the hit should be accepted
    So now you can create "overrides" of logic that happens within jobs. But that's not all! What if your IHitFilterer.ShouldAcceptHit() needs to access certain component types on the entity that was hit? Like for example, check if the hit entity has a PhysicsMass component, and return false if the component is present, etc.... Currently you don't have access to that. Here's my solution:
    • You turn your IHitFilterer into a IHitFilterer<D> where D : struct, and make IHitFilterer have to implement a "GetAdditionalJobData()" which returns a struct of type D
    • before DoRaycastsJob is scheduled, call T.GetAdditionalJobData(). In this function, you would, for example, return a struct that contains a ComponentDataFromEntity<PhysicsMass>
    • The initialized D struct gets passed to the job when it is scheduled
    • When the DoRaycastsJob calls T.ShouldAcceptHit(), it passes the D struct as parameter
    • Your implementation of ShouldAcceptHit now has access to any additional data that you could possible need. It can check if D.physicsMassFromEntity.Exists(e), for example.
    This definitely sounds scary, but I've done performance tests and there was zero noticeable performance difference between doing this, and hard-coding all the logic in a non-generic job. This was a test where the job was operating on about 20k entities, but I guess if we were in the millions of iterations we would start seeing a difference. It's also worth noting that the cost of doing physics queries probably dwarfs the cost of the genericness completely.

    The performance of the "scheduling 60 jobs" approach mentioned earlier was noticeably worse, though


    Code generation
    For now, the only alternative I see would be codegen. And to be honest, I think codegen might be the way to go for this, because, let's face it, the solution I described sounds extremely spaghetti. On some nights, I dream of an Entities.ForEach-style magical hidden CECIL codegen that would let us declare jobs that can be overrided'd, both in terms of overriding a specific chunk of logic within the job based on component type, and overriding the data that is given to the job based on component type. Basically; an Entities.ForEach that can be configured to do different things based on which component type it operates on, but has some common logic that executes in all cases. The component type that defines custom logic could be in the form of an interface, and the .ForEach would generate one unique job per component type in the project that implement this interface. Ideally, the implementation of the "customizable logic and additional data" should be done outside of the Entities.ForEach in order to allow people to make third party packages that users can extend without modifying sources


    Bonus solution: The other way around
    I have yet to try this out, but what if we turned this problem upside down? What if people were in charge of creating systems/jobs that do the custom logic, without interfaces or inheriting from anything, and all the "shared" logic was nicely packaged into easy-to use static functions that tell you what data they need in their parameters? It would require users to follow certain rules (more potential for errors) and write a lot of boilerplate, but it could end up being the "least worst" solution. It's hard to think of good ways to force people to implement a for loop in their jobs, but maybe there are ways. Hmmmm....
     
    Last edited: Feb 7, 2020
    pal_trefall likes this.
  2. Gen_Scorpius

    Gen_Scorpius

    Joined:
    Nov 2, 2016
    Posts:
    54
    My rudimentary understanding is that the chunk job api is closest to what you are looking for. Going through each chunk and capable of executing code loops depending on existing optional components within the chunk as is implemented in the unity transform system.
     
  3. Cell-i-Zenit

    Cell-i-Zenit

    Joined:
    Mar 11, 2016
    Posts:
    114
    You can pass in Lambdas to a job via FunctionPointer
     
  4. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,296
    Here's the part that I forgot emphasize on:

    the customization of logic must be doable outside of the job. Imagine a situation where someone makes a cool package, and users can "extend" that package with their own logic without modifying the source code. Branches in a IJobChunk would imply hard-coding all possible cases in the job

    Oh my, I had no idea that was possible. Is there an example of that somewhere?

    Right now I'm also not sure if lambdas would be sufficient to let you easily define additional component data arrays for the job, though.... See this part:
     
    Last edited: Feb 4, 2020
  5. Cell-i-Zenit

    Cell-i-Zenit

    Joined:
    Mar 11, 2016
    Posts:
    114
  6. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,296
    That's a really nice tool to have

    But yeah, if your custom function needs access to certain data arrays that the original job didn't necessarily supply in the first place (which is the situation I'm in), then this solution is not enough. Still needs a way to define a struct of additional job data to get passed to the job, so we'd end up with a solution that is very similar to my generics+interfaces approach.

    At the end of the day, due to the requirement of being able to pass any additional data in the job, I think it's unavoidable that we need to have one different job per "override" that we make of it. The problem is just a question of figuring out the easiest way to create these multiple jobs that share the same complex logic (which we don't want to have to rewrite every time)
     
    Last edited: Feb 4, 2020
  7. Sarkahn

    Sarkahn

    Joined:
    Jan 9, 2013
    Posts:
    266
    Unfortunately i don't think you actually can pass lambdas to a Burst FunctionPointer. You can only pass static functions decorated with [BurstCompile] inside a static class also decorated with [BurstCompile]. On top of that you can't have your function pointer touch any mutable external state like Phil mentioned in his previous post - and any structs the function works with must be taken and returned by reference.

    TBH I'm not sure why anyone would want to use the Burst FunctionPointer given all the restrictions but I'm sure I'm just missing something due to inexperience.

    This is the method I've been going with, I made up a simple field-of-view solution for my roguelike and it works brilliantly and imo is much more readable and straightforward than what they expect you to do with Burst FunctionPointers:

    Code (CSharp):
    1.         public static void GetVisiblePoints<T>(int2 origin, int range, T visibilityMap, NativeHashSet<int2> buffer)
    2.             where T : IVisibilityMap
    3.         {
    4.             BresenhamCircle circle = new BresenhamCircle(origin, range);
    5.             var points = circle.GetPoints(Allocator.Temp);
    6.             for (int i = 0; i < points.Length; ++i)
    7.             {
    8.                 var p = points;
    9.  
    10.                 ScanFOVLine(origin, p, visibilityMap, buffer);
    11.             }
    12.         }
    13.  
    Callers can model the "VisibilityMap" however they choose and the interface can define any needed data through the functions. As long as the map is a struct and follows the usual Burst rules this can be called from inside systems and jobs without any issues at all. When I was writing it I was worried it wouldn't work due to some Burst restriction I was missing, but as you said in your post, it actually works great.
     
    Last edited: Feb 5, 2020
    PhilSA likes this.
  8. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    1,014
    I also believe that generic jobs with interfaces are the right solution to the problem. Unlike a delegate, an interface comes with context data, which is often what is needed. The language actually makes this very clean. And if it is getting ugly, that is likely a sign of you doing something wrong.

    The first thing I see a lot of people do is they try to make their systems contain/be the algorithms applied to the data. Systems need to do no more than schedule work, or for a better word in this context, "prescribe" work. Some prescriptions are going to be custom and so the custom logic ends up written directly in the system class. But some prescriptions are going to use common algorithms. Those algorithms should not live inside every system but instead be provided in a static class. This includes and especially applies to generic jobs with interfaces.

    What I typically do is expose a fluent-style job scheduler API in a static class. This lets the user configure many of the parameters of the algorithm including setting an instance of the interface. Then at schedule time, the fluent methods can dynamically determine which underlying jobs are necessary to complete the work and schedule them. Another benefit to this approach is I can schedule multiple jobs chained together for a single interface and schedule call. Even better, this style of API is one step away from code-gen lambdas, which is the holy grail of cleanness currently in this DOTS world.

    Here's an example of how I typically write such an API (and how it creates and schedules jobs internally): https://github.com/Dreaming381/Lati...hysics/Utilities/Queries/Physics.FindPairs.cs

    Without lamdas, using it could look something like this:
    Code (CSharp):
    1.         private struct DestroyOnCollision : IFindPairsProcessor
    2.         {
    3.             public EntityCommandBuffer.Concurrent ecb;
    4.  
    5.             public void Execute(FindPairsResult result)
    6.             {
    7.                 if (Physics.DistanceBetween(result.bodyA.collider, result.bodyA.transform, result.bodyB.collider, result.bodyB.transform, 0f, out ColliderDistanceResult _))
    8.                 {
    9.                     ecb.DestroyEntity(result.bodyAIndex, result.bodyA.entity);
    10.                     ecb.DestroyEntity(result.bodyBIndex, result.bodyB.entity);
    11.                 }
    12.             }
    13.         }
    14.  
    15.         protected override JobHandle OnUpdate(JobHandle inputDeps)
    16.         {
    17.             CollisionLayer collisionLayer;
    18.  
    19.             //Code to fetch or initialize collisionLayer omitted for briefness
    20.             //...
    21.             //Assume collisionLayer is now initialized
    22.  
    23.             var simEcbSystem = World.GetExistingSystem<EndSimulationEntityCommandBufferSystem>();
    24.  
    25.             var destroyer = new DestroyOnCollision
    26.             {
    27.                 ecb = simEcbSystem.CreateCommandBuffer().ToConcurrent()
    28.             };
    29.  
    30.             var jh = Physics.FindPairs(collisionLayer, destroyer).ScheduleParallel(inputDeps);
    31.             simEcbSystem.AddJobHandleForProducer(jh);
    32.             return jh;
    33.         }
    Note how I initialized the context of the interface directly in the system (in this case, an EntityCommandBuffer). In the future, such context could be captured by a lamda, reducing the boilerplate code.

    While Burst by default does not support generic jobs in AOT builds, I and others on the forums have written solutions to that problem. I have found in testing that Burst almost always inlines the interface method, and if not, compiles it the same as if it were a member method of the job itself.

    @PhilSA If there's a particular library you are trying to design an API for, care to post specific details here? I'm guessing you and I have similar goals and I have played with quite a few different API designs before landing on the one I currently am using.
     
  9. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,296
    I'm basically doing a character controller where users can customize which hits can be accepted as collisions, how "grounded" status is evaluated, etc.... Both of these things must be called several times in a simulation frame by the character movement code.

    For example, if users want to implement a character that can dash through non-boss enemies, they could "override" the default "IsHitValid(hit)" of the character so that while it is in a dash state, if we hit an entity that has an "enemy" component and the "boss" bool on that component is false, we discard the collision hit

    Right now, for the user, implementing this involves:
    • Creating a struct MyHitEvaluator : IComponentData, IHitEvaluator which serves as their custom hit evaluator
    • Creating a struct of additional job data that can be returned by that previous struct (additional data in this example would be a ComponentDataFromEntity<Enemy>)
    • Creating a MyCharacterSystem : BaseCharacterSystem<MyHitEvaluator>
    • in the MyHitEvaluator.IsHitValid, do the check for if there's the enemy component, etc...
     
    pal_trefall likes this.
  10. Cell-i-Zenit

    Cell-i-Zenit

    Joined:
    Mar 11, 2016
    Posts:
    114
    I am currently giving my jobs a method which is NOT static and sitting on a managed object. If you follow my link you will see that this is possible
     
  11. Sarkahn

    Sarkahn

    Joined:
    Jan 9, 2013
    Posts:
    266
    I see that I was wrong about the static function restrictions, but it still seems like you need to jump through some interesting hoops to get FunctionPointer to do what you actually want it to do. And the "solutions" don't look like the kind of code I would ever want to inflict on a user of an API.

    Not trying to be snarky or ignorant, I still don't understand why one would want to use that over the generic interface solution.
     
  12. pal_trefall

    pal_trefall

    Joined:
    Feb 5, 2019
    Posts:
    71
    That sounds pretty clean to me. It would be interesting to see how the static library approach would look like, where we would build the systems ourselves, but this example here isn't bad at all imo.
     
  13. Razmot

    Razmot

    Joined:
    Apr 27, 2013
    Posts:
    286
    In my (extensive complex business code) experience, "frameworks over frameworks" end up getting in the way :

    If you try to add a level of abstraction on top of unity's ecs/jobs system, you have a high probability to go in directions that the unity team already tried and proved wrong !

    So my advice is to keep it simple and go towards "Bonus solution: The other way around":
    an extensive well designed utility API + some implementation examples.

    Then, you should definitely check what the alpha ECS visual coding tool provides. Maybe you can design your tool around custom nodes for this.

     
    pal_trefall, PhilSA and Sarkahn like this.
  14. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,296
    I tend to agree with what you're saying. I've become more and more convinced that extreme simplicity and straightforwardness is the key to happy and robust programming (even at the cost of extra manual work or boilerplate). The fewer "frameworks" and "design patterns", the better. Especially when it comes to making something that's intended to be used by other people

    But, as always, sometimes there are exceptions. I think I'll give the Bonus Solution a try and see what it looks like, compared to what I have now. Character controllers require intricate logic that is spread over several different systems, with their update orders being very important, and with the user-customizable parts being deeply nested in several layers of loops & conditions within jobs. It might be difficult to pull off the "put things in static functions" solution
     
    Last edited: Feb 5, 2020
    pal_trefall likes this.
  15. snacktime

    snacktime

    Joined:
    Apr 15, 2013
    Posts:
    2,627

    I think the approach Unity.Physics takes also makes sense for a controller based on it. Unity.Physics is largely self contained and it's api's are mostly it's own. It integrates with ECS in a fairly minimal way. Most major features in our game follow the same pattern. Because bringing a lot of complex logic up to the component level just breaks down rather quickly in more ways then one.
     
    pal_trefall likes this.
  16. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    1,014
    While I don't know enough about your problem to give you a surefire suggestion, I can definitely say there are better options out there than this. First off, having an evaluator as an IComponentData is a terrible idea. An evaluator isn't data in this case. Second, having the evaluator be in a separate struct from the context "additional job data" sounds like a type management disaster. And third, how in the world is an IComponentData supposed to return a ComponentDataFromEntity? That's nothing it can store, and it can't fetch it from within a job.

    There's two API mechanisms I can see working out better given the limited info about your problem that I have.

    The first is you create abstract JobComponentSystem classes specific to each stage in your CharacterController pipeline that has overrideable points. Such a system would automatically run inside a specific ComponentSystemGroup for the particular stage and would require implementation of certain methods whose sole job is to provide the custom job override interface via a generic method call. The benefit of this approach is that you can wrap the abstract method call in the base class with checks to ensure that the abstract method user implementation calls the generic method it is supposed to call.

    The second approach involves creating a "Driver" base class in which derived instances can be registered to a particular EntityQuery. Instead of the abstract methods in the JobComponentSystem, the driver instances would contain virtual methods (with base implementations) for scheduling the custom job data. Another appropriate name for this "Driver" class would be "OverrideFactory" as it generates the override interface structs needed in the jobs.

    The third option in the completely other direction is to provide all the transforms in a library of static classes and then provide some systems using write groups as a reference implementation. This is the most flexible solution, but also drastically increases the chances of users putting lead in their feet.
     
    pal_trefall likes this.
  17. Razmot

    Razmot

    Joined:
    Apr 27, 2013
    Posts:
    286
  18. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,296
    I found a new solution which I think has very few downsides aside from asking users to write a bit of boilerplate and follow simple rules. It's more or less a version of the "Bonus Solution" mentioned in first post. It goes like this:
    • Users create their own MyCharacterSystem : JobComponentSystem. No generics or inheriting from other premade systems here
    • That MyCharacterSystem must launch a job which implements the ICharacterHitEvaluatorJob interface on top of a regular job type like IJobChunk. This interface is what makes you implement the IsHitValid() and IsHitStable(). So these things are now the job's responsibility; not the Component's
    • Within that job, users must call specific static util functions in the right order, such as "CharacterGroundProbing" and "CharacterProcessMovement". We pass the job itself as a reference to these functions, so they can call "IsHitValid" and "IsHitStable" when they need to
    • In the job's "IsHitValid" and "IsHitStable", users can have access to any data they could possibly want, because they wrote the job themselves
    Everything seems to make more sense with this approach, I think:
    • Much less "over-engineered" and incoherent
    • Components don't do any logic anymore
    • No need to declare "additional job data" structs anymore
    • Users have total control over execution of things, and can easily insert steps between "CharacterGroundProbing" and "CharacterProcessMovement" for example. They can even do these things in separate systems if they want to
    I guess one downside of this approach is that it makes it not possible to use Entities.ForEach, since a custom job that implements the right interface is needed.

    Here's what the complete code of the new version looks like. This is what the user would have to write, but a huge part of it is just IJobChunk boilerplate:
    Code (CSharp):
    1. using System.Collections.Generic;
    2. using Unity.Burst;
    3. using Unity.Collections;
    4. using Unity.Entities;
    5. using Unity.Jobs;
    6. using Unity.Mathematics;
    7. using Unity.Physics;
    8. using Unity.Physics.Systems;
    9. using Unity.Transforms;
    10.  
    11. [UpdateBefore(typeof(BuildPhysicsWorld))]
    12. public class ExampleCharacterSystem : JobComponentSystem
    13. {
    14.     public BuildPhysicsWorld BuildPhysicsWorldSystem;
    15.     public EntityQuery CharacterQuery;
    16.  
    17.     [BurstCompile(FloatPrecision.High, FloatMode.Default)]
    18.     struct ExampleCharacterSystemJob : IJobChunk, ICharacterHitEvaluatorJob
    19.     {
    20.         public float DeltaTime;
    21.         [ReadOnly]
    22.         public PhysicsWorld PhysicsWorld;
    23.  
    24.         [ReadOnly]
    25.         public ArchetypeChunkEntityType EntityType;
    26.         [ReadOnly]
    27.         public ArchetypeChunkComponentType<CharacterProperties> CharacterPropertiesType;
    28.         [ReadOnly]
    29.         public ArchetypeChunkComponentType<CharacterGroundProbingParameters> CharacterGroundProbingParametersType;
    30.         [ReadOnly]
    31.         public ArchetypeChunkComponentType<PhysicsCollider> PhysicsColliderType;
    32.         [ReadOnly]
    33.         public ArchetypeChunkComponentType<MyCharacterInputs> MyCharacterInputsType;
    34.         public ArchetypeChunkComponentType<Translation> TranslationType;
    35.         public ArchetypeChunkComponentType<Rotation> RotationType;
    36.         public ArchetypeChunkComponentType<CharacterVelocity> CharacterVelocityType;
    37.         public ArchetypeChunkComponentType<CharacterGroundingResult> CharacterGroundingResultType;
    38.         public ArchetypeChunkBufferType<CharacterCastHitsBufferElement> CharacterCastHitsBufferType;
    39.         public ArchetypeChunkBufferType<CharacterDistanceHitsBufferElement> CharacterDistanceHitsBufferType;
    40.         public ArchetypeChunkBufferType<CharacterHitPlanesBufferElement> CharacterHitPlanesBufferType;
    41.  
    42.         [ReadOnly]
    43.         public ComponentDataFromEntity<MyCharacterProperties> MyCharacterPropertiesFromEntity;
    44.         [ReadOnly]
    45.         public BufferFromEntity<IgnoredEntitiesBufferElement> IgnoredEntitiesBufferFromEntity;
    46.  
    47.         public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
    48.         {
    49.             NativeArray<Entity> chunkEntities = chunk.GetNativeArray(EntityType);
    50.             NativeArray<CharacterProperties> chunkCharacterProperties = chunk.GetNativeArray(CharacterPropertiesType);
    51.             NativeArray<CharacterGroundProbingParameters> chunkCharacterGroundProbingParameters = chunk.GetNativeArray(CharacterGroundProbingParametersType);
    52.             NativeArray<PhysicsCollider> chunkPhysicsColliders = chunk.GetNativeArray(PhysicsColliderType);
    53.             NativeArray<MyCharacterInputs> chunkMyCharacterInputs = chunk.GetNativeArray(MyCharacterInputsType);
    54.             NativeArray<Translation> chunkTranslations = chunk.GetNativeArray(TranslationType);
    55.             NativeArray<Rotation> chunkRotations = chunk.GetNativeArray(RotationType);
    56.             NativeArray<CharacterVelocity> chunkCharacterVelocities = chunk.GetNativeArray(CharacterVelocityType);
    57.             NativeArray<CharacterGroundingResult> chunkCharacterGroundingResults = chunk.GetNativeArray(CharacterGroundingResultType);
    58.             BufferAccessor<CharacterCastHitsBufferElement> chunkCharacterCastHitsBuffers = chunk.GetBufferAccessor(CharacterCastHitsBufferType);
    59.             BufferAccessor<CharacterDistanceHitsBufferElement> chunkCharacterDistanceHitsBuffer = chunk.GetBufferAccessor(CharacterDistanceHitsBufferType);
    60.             BufferAccessor<CharacterHitPlanesBufferElement> chunkCharacterHitPlanesBuffer = chunk.GetBufferAccessor(CharacterHitPlanesBufferType);
    61.  
    62.             for (var i = 0; i < chunk.Count; i++)
    63.             {
    64.                 Entity entity = chunkEntities[i];
    65.                 CharacterProperties characterProperties = chunkCharacterProperties[i];
    66.                 CharacterGroundProbingParameters characterGroundProbingParameters = chunkCharacterGroundProbingParameters[i];
    67.                 PhysicsCollider physicsCollider = chunkPhysicsColliders[i];
    68.                 MyCharacterInputs myCharacterInputs = chunkMyCharacterInputs[i];
    69.                 Translation translation = chunkTranslations[i];
    70.                 Rotation rotation = chunkRotations[i];
    71.                 CharacterVelocity characterVelocity = chunkCharacterVelocities[i];
    72.                 CharacterGroundingResult characterGroundingResult = chunkCharacterGroundingResults[i];
    73.                 DynamicBuffer<CharacterCastHitsBufferElement> characterCastHitsBuffer = chunkCharacterCastHitsBuffers[i];
    74.                 DynamicBuffer<CharacterDistanceHitsBufferElement> characterDistanceHitsBuffer = chunkCharacterDistanceHitsBuffer[i];
    75.                 DynamicBuffer<CharacterHitPlanesBufferElement> characterHitPlanesBuffer = chunkCharacterHitPlanesBuffer[i];
    76.                 MyCharacterProperties myCharacterProperties = MyCharacterPropertiesFromEntity[entity];
    77.  
    78.                 // TODO: should we calc character up only once here, and let users update it when they update rotation?
    79.  
    80.                 // Ground probing
    81.                 DCCUtilities.CharacterGroundProbing(
    82.                     entity,
    83.                     ref this,
    84.                     ref PhysicsWorld,
    85.                     ref characterCastHitsBuffer,
    86.                     ref characterGroundingResult,
    87.                     ref characterGroundProbingParameters,
    88.                     ref physicsCollider,
    89.                     ref rotation,
    90.                     ref translation);
    91.  
    92.                 // Decollision
    93.                 DCCUtilities.CharacterDecollision(
    94.                     entity,
    95.                     ref this,
    96.                     ref PhysicsWorld,
    97.                     ref translation,
    98.                     ref rotation,
    99.                     ref physicsCollider,
    100.                     ref characterProperties,
    101.                     ref characterGroundingResult,
    102.                     ref characterDistanceHitsBuffer);
    103.  
    104.                 // TODO: Here the user would modify velocity/rotation based on input
    105.  
    106.                 // Process Movement
    107.                 DCCUtilities.CharacterProcessMovement(
    108.                     DeltaTime,
    109.                     entity,
    110.                     ref this,
    111.                     ref PhysicsWorld,
    112.                     ref characterCastHitsBuffer,
    113.                     ref characterDistanceHitsBuffer,
    114.                     ref characterHitPlanesBuffer,
    115.                     ref characterProperties,
    116.                     ref characterGroundingResult,
    117.                     ref characterGroundProbingParameters,
    118.                     ref physicsCollider,
    119.                     ref rotation,
    120.                     ref translation,
    121.                     ref characterVelocity);
    122.  
    123.                 // Write back
    124.                 chunkTranslations[i] = translation;
    125.                 chunkRotations[i] = rotation;
    126.                 chunkCharacterVelocities[i] = characterVelocity;
    127.                 chunkCharacterGroundingResults[i] = characterGroundingResult;
    128.             }
    129.         }
    130.  
    131.         public bool IsHitStable(Entity hitEntity, float3 hitPosition, float3 hitNormal, int hitRigidbodyIndex, Entity selfEntity, float3 characterUp)
    132.         {
    133.             if (math.dot(characterUp, hitNormal) < MyCharacterPropertiesFromEntity[selfEntity].MaxSlopeDotRatio)
    134.             {
    135.                 return false;
    136.             }
    137.  
    138.             return true;
    139.         }
    140.  
    141.         public bool IsHitValid(Entity hitEntity, float3 hitPosition, float3 hitNormal, int hitRigidbodyIndex, Entity selfEntity, float3 characterUp)
    142.         {
    143.             DynamicBuffer<IgnoredEntitiesBufferElement> ignoredEntitiesBuffer = IgnoredEntitiesBufferFromEntity[selfEntity];
    144.             for (int i = 0; i < ignoredEntitiesBuffer.Length; i++)
    145.             {
    146.                 if (ignoredEntitiesBuffer[i].Entity == hitEntity)
    147.                 {
    148.                     return false;
    149.                 }
    150.             }
    151.  
    152.             return true;
    153.         }
    154.     }
    155.  
    156.     protected override void OnCreate()
    157.     {
    158.         base.OnCreate();
    159.  
    160.         BuildPhysicsWorldSystem = World.GetOrCreateSystem<BuildPhysicsWorld>();
    161.  
    162.         // TODO: make this more exact
    163.         EntityQueryDesc characterQueryDesc = new EntityQueryDesc
    164.         {
    165.             All = new ComponentType[]
    166.             {
    167.                 typeof(CharacterGroundProbingParameters),
    168.                 typeof(CharacterGroundingResult),
    169.                 typeof(CharacterVelocity),
    170.                 typeof(PhysicsCollider),
    171.                 typeof(Translation),
    172.                 typeof(Rotation),
    173.                 typeof(MyCharacterInputs),
    174.                 typeof(MyCharacterProperties),
    175.             }
    176.         };
    177.         CharacterQuery = GetEntityQuery(characterQueryDesc);
    178.     }
    179.  
    180.     protected override JobHandle OnUpdate(JobHandle inputDependencies)
    181.     {
    182.         inputDependencies = new ExampleCharacterSystemJob
    183.         {
    184.             DeltaTime = World.Time.DeltaTime,
    185.             PhysicsWorld = BuildPhysicsWorldSystem.PhysicsWorld,
    186.             EntityType = GetArchetypeChunkEntityType(),
    187.             CharacterPropertiesType = GetArchetypeChunkComponentType<CharacterProperties>(true),
    188.             CharacterGroundProbingParametersType = GetArchetypeChunkComponentType<CharacterGroundProbingParameters>(true),
    189.             PhysicsColliderType = GetArchetypeChunkComponentType<PhysicsCollider>(true),
    190.             MyCharacterInputsType = GetArchetypeChunkComponentType<MyCharacterInputs>(true),
    191.             TranslationType = GetArchetypeChunkComponentType<Translation>(false),
    192.             RotationType = GetArchetypeChunkComponentType<Rotation>(false),
    193.             CharacterVelocityType = GetArchetypeChunkComponentType<CharacterVelocity>(false),
    194.             CharacterGroundingResultType = GetArchetypeChunkComponentType<CharacterGroundingResult>(false),
    195.             CharacterCastHitsBufferType = GetArchetypeChunkBufferType<CharacterCastHitsBufferElement>(false), // todo: can these buffers be readonly
    196.             CharacterDistanceHitsBufferType = GetArchetypeChunkBufferType<CharacterDistanceHitsBufferElement>(false),
    197.             CharacterHitPlanesBufferType = GetArchetypeChunkBufferType<CharacterHitPlanesBufferElement>(false),
    198.             MyCharacterPropertiesFromEntity = GetComponentDataFromEntity<MyCharacterProperties>(true),
    199.             IgnoredEntitiesBufferFromEntity = GetBufferFromEntity<IgnoredEntitiesBufferElement>(true),
    200.         }.Schedule(CharacterQuery, inputDependencies);
    201.  
    202.         return inputDependencies;
    203.     }
    204. }
     
    Last edited: Feb 7, 2020
    Sarkahn, Flipps and pal_trefall like this.
  19. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    1,014
    Yup! That is my preferred approach. Lot's of flexibility at the cost of making it easy for a less-competent user to shoot themselves in the foot. It is perfect if you plan to open-source it, but it could be a support nightmare if you plan to sell it.

    You can call a static generic method from inside a job. So a user could make this work by defining a struct instance inside the OnUpdate implementing the interface.
     
    pal_trefall and PhilSA like this.
unityunity