Search Unity

How to work without interfaces and other questions

Discussion in 'Data Oriented Technology Stack' started by D4rt, Sep 10, 2019.

  1. D4rt

    D4rt

    Joined:
    Apr 20, 2013
    Posts:
    6
    I've been trying to implement a generic Spline system that could easily be used by other systems to do whatever (walk along a path, create procedural meshes like tentacles, etc.). This sort of system is very easy with good old OOP and interfaces, where you just have to implement an ISpline interface for all the different spline types and then you have no need to know the type of the spline when you use it, like below.

    Code (CSharp):
    1.  
    2. public interface ISpline
    3. {
    4.     float Length { get; }
    5.     float Interpolate(float t);
    6. }
    7.  
    8. public void DoSomething(ISpline spline)
    9. {
    10.     var value = spline.Interpolate(0.5);
    11.     ...
    12. }
    13.  
    With ECS I've ran into some design issues and my questions can be found at the end of the post, but first I'll show my schema for some context.

    Code (CSharp):
    1.  
    2. // Component that tells we should use catmull rom interpolation.
    3. public struct CatmullRomSpline { public float Alpha; }
    4.  
    5. // Component that tells we should use bezier interpolation.
    6. public struct BezierSpline { }
    7.  
    8. // Other spline types...
    9.  
    10. ...
    11.  
    12. // Common data for all splines.
    13. public struct Spline : IComponentData { public float Length; }
    14.  
    15. // Control points through which the splines goes.
    16. public struct SplineControlPoint : IBufferElementData { public float3 Value; }
    17.  
    18. // Used to scale the interpolation value so that it uniformly covers the length of the spline.
    19. public struct SplineSegmentLength : IBufferElementData { public float Value; }
    20.  
    21. // Used to trigger recalculation of SplinesSegmentLength when SplineControlPoint changes.
    22. public struct SplineChangedFlag : IBufferElementData { public float3 Value; }
    23.  
    Additionally there is a sytem called SplineMeasureSystem that calculates the SplineSegmentLengths when the SplineChangedFlag is present.

    Question 1:

    It's not clear to me how the Spline should be used in an unknown number of other systems in an easy way. I don't really want the other systems to know that such a thing as "BezierSpline" or "CatmullRomSpline" exists, I simply want them to see that there is a Spline component and then somehow use behaviour equivelant to the OOP methods ISpline.Interpolate(float value) to get the result without knowing any details. The code in each system would have to:
    1. Find out what sort of interpolation logic to use based on the splines type.
    2. Find all components needed to do the interpolation.
    3. Interpolate based on the splines type.
    4. Do all this in a way that would work with the Job System.
    I don't want the systems to need to bother about any of this.

    Question 2:

    Is there a more elegant way to track changes to a component other than using another separate flag component like SplineChangedFlag?

    Question 3:

    How do I make sure you can't accidentally add two specialized spline components to one entity (a spline can't be both a bezier spline and a catmull rom spline after all).
     
  2. LastResortMatthew

    LastResortMatthew

    Joined:
    Apr 10, 2019
    Posts:
    41
    Question 1: I think you're thinking about this in a very OOP way. The DOD way would be:

    CatmullRomSplineSystem operates on entities with a CatmullRomSpline component, and this System contains the logic from the "OOP style"
    CatmullRomSpline.Interpolate(float value)


    BezierSplineSystem operates on entities with a BezierSpline component, etc.

    There's really no need for inheritance or interfaces in this pattern. If there's common data between the two (say, in a Spline component) then you can have each system require both the generic and specific component to be present before operating on it. If there's common logic between the two, that's a bit trickier but others have given examples of how to use static utility methods to share logic between systems.

    Question 2: Not really, though the alternative to a tag component is to have a bool field on the generic Spline component and check that in your systems, earlying out on it if necessary. Which approach is better depends on a number of factors such as how often the flag will be changing (if it changes every frame, the bool field is going to be much much more performant).

    Question 3: I'm not aware of a way to actually prevent the components being added to an entity in an invalid way, but you could put debug asserts in your systems when processing the entities (if you want to be alerted to an invalid configuration), or make your EntityQueries exclude invalid configurations if you want to ignore them:

    Code (CSharp):
    1. _query = GetEntityQuery(
    2.     ComponentType.ReadOnly<Spline>(),
    3.     ComponentType.ReadOnly<BezierSpline>(),
    4.     ComponentType.Exclude<CatmullRomSpline>()
    5. );
     
    Last edited: Sep 10, 2019
  3. D4rt

    D4rt

    Joined:
    Apr 20, 2013
    Posts:
    6
    Thanks for the answer.

    I still have some concerns about Question 1. My idea was that there would be some other systems, for example a "FollowSplineSystem" or a "CreateEntitiesAlongSplineSystem" that don't really have anything to do with the evaluation logic of the splines themselves and don't really care about the type of the spline. They just want to "query" the spline for results if that makes sense.

    For example if the CreateEntitiesAlongSplineSystem wanted to create 1000 entities along the spline, it would just query for 1000 positions with the interpolation value ranging from 0 to 1 in 1000 small steps.
     
  4. LastResortMatthew

    LastResortMatthew

    Joined:
    Apr 10, 2019
    Posts:
    41
    Oh, I see. Sorry, I was assuming that you wanted something precomputed by a system, to then have the results available for use by other systems. Clearly in this use case it's not practical for a SplineSystem to calculate all the points that might be required by some other system, and so any system would need to request points when they require them.

    In that case, I'd suggest the "common logic" approach. I misspoke earlier when I said "static utility methods", the example I saw was actually @tertle using a struct to contain the logic, and passing that struct into a job: https://forum.unity.com/threads/bet...-jobs-lots-of-parameters.735599/#post-4905692
     
  5. D4rt

    D4rt

    Joined:
    Apr 20, 2013
    Posts:
    6
    Interesting. That looks like a reasonable enough solution. Thanks for the help!
     
  6. tarahugger

    tarahugger

    Joined:
    Jul 18, 2014
    Posts:
    103
    You can use interfaces on jobs with generic arguments and an interface constraint. For example

    Code (CSharp):
    1.     [BurstCompile]
    2.     public struct UpdateGoalsJob<TBufferData, TComponent> : IJobForEach_BC<TBufferData, TComponent>
    3.         where TBufferData : struct, IBufferElementData, ISomeInterface
    4.         where TComponent : struct, IComponentData, ISomeOtherInterface
    5.     {
    6.         public DynamicBuffer<GoalData> Goals;
    7.  
    8.         public void Execute([ReadOnly] DynamicBuffer<TBufferData> buffer, [ReadOnly] ref TComponent component)
    9.         {
    10.             // do stuff with ISomeInterface
    11.         }
    12.     }
    Code (csharp):
    1.  
    2.         inputDeps = new UpdateGoalsJob<ExplosionHitBufferData, ExplosionEvent>
    3.         {
    4.             Goals = goals
    5.  
    6.         }.ScheduleSingle(_explosionsQuery, inputDeps);
    7.  
    8.         inputDeps = new UpdateGoalsJob<SelectionBufferData, MatchedEvent>
    9.         {
    10.             Goals = goals
    11.  
    12.         }.ScheduleSingle(_matchedQuery, inputDeps);
    13.  
    or you could keep non-generic jobs and move the logic into a generic static method in the system that is called by multiple jobs.
     
    Last edited: Sep 10, 2019
  7. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    436
    While it's not a common, sometimes you really need polymorphic components. Colliders are a pretty typical example. Due to the language restrictions, you kinda have to reimplement polymorphism's constructs. There's two ways I know to do it. The first uses pointer casting, and is what Unity.Physics does. The second has a base struct representation which contains a union of all the other collider types and an enum specifying which type it is. And then it uses cast operations and T4 interface implementations for the base type calling a switch case casted "derived" type's implementation. The downside is you have to declare all of your types in an enum with such polymorphic approaches.
     
  8. davenirline

    davenirline

    Joined:
    Jul 7, 2010
    Posts:
    489
    I wish there's a better way to do this. You have to go through hoops to emulate polymorphism. I know it's not the ECS way but it's just so intuitive.

    Does anyone have a simple example of pointer casting?
     
  9. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    1,663
    Best example is Unity.Physics
     
  10. D4rt

    D4rt

    Joined:
    Apr 20, 2013
    Posts:
    6
    For those interested, I went with a separate struct containing the logic and being passed to jobs. The (simplified code is below).

    Code (CSharp):
    1. public struct SplineInterpolatorFromEntity
    2. {
    3.     private BufferFromEntity<SplineControlPoint> _controlPointsFromEntity;
    4.  
    5.     public SplineInterpolatorFromEntity(JobComponentSystem system)
    6.     {
    7.         _controlPointsFromEntity = system.GetBufferFromEntity<SplineControlPoint>(true);
    8.     }
    9.  
    10.     public SplineInterpolator GetInterpolator(Entity entity, Spline spline)
    11.     {
    12.         var controlPoints = _controlPointsFromEntity[entity];
    13.         return new SplineInterpolator(spline, controlPoints);
    14.     }
    15. }
    Code (CSharp):
    1. public struct SplineInterpolator
    2. {
    3.     private Spline _spline;
    4.     private DynamicBuffer<SplineControlPoint> _controlPoints;
    5.  
    6.     public SplineInterpolator(Spline spline, DynamicBuffer<SplineControlPoint> controlPoints)
    7.     {
    8.         _spline = spline;
    9.         _controlPoints = controlPoints;
    10.     }
    11.  
    12.     public float3 Interpolate(float t)
    13.     {
    14.         switch (_spline.Type)
    15.         {
    16.             case SplineType.Bezier:
    17.                 return InterpolateBezier(t);
    18.             case SplineType.CatmullRom:
    19.                  return InterpolateCatmullRom(t);
    20.         }
    21.     }
    22.  
    23.     public float3 InterpolateBezier(t)
    24.     {
    25.         ...
    26.     }
    27.  
    28.     public float3 InterpolateCatmullRom(t)
    29.     {
    30.         ...
    31.     }
    32. }
    33.  
    And it is used like so:

    Code (CSharp):
    1. public sealed class SplineInterpolateSystem : JobComponentSystem
    2. {
    3.  
    4.     [BurstCompile]
    5.     public struct SplineInterpolateJob : IJobForEachWithEntity<Spline>
    6.     {
    7.         [ReadOnly]
    8.         public SplineInterpolatorFromEntity SplineInterpolatorFromEntity;
    9.  
    10.         public void Execute(Entity entity, int index, [ReadOnly] ref Spline spline)
    11.         {
    12.             var interpolator = SplineInterpolatorFromEntity.GetInterpolator(entity, spline);
    13.             // Do whatever.
    14.         }
    15.     }
    16.  
    17.     protected override JobHandle OnUpdate(JobHandle inputDependencies)
    18.     {
    19.         var job = new SplineInterpolateJob
    20.         {
    21.             SplineInterpolatorFromEntity = new SplineInterpolatorFromEntity(this)
    22.         };
    23.  
    24.         return job.Schedule(_query, inputDependencies);
    25.     }
    26. }
    27.  
     
    Last edited: Sep 13, 2019
    psuong, tarahugger and PublicEnumE like this.
  11. PublicEnumE

    PublicEnumE

    Joined:
    Feb 3, 2019
    Posts:
    106
    I’m very curious, since I’ve seen other people arrive at this solution lately:

    Is there a functional advantage to this approach over calling static utility methods from inside of the job? Is there something this approach can do (or do in a more performant way) that static methods can’t?

    This is in no way a criticism of your code. Glad you found a great approach!
     
    Last edited: Sep 13, 2019
  12. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    1,663
    Why ever have a non static method, just make your entire app static methods and pass everything around!

    So for starters means you don't need a method that looks like this

    Code (CSharp):
    1.  private static void FillVision<T>(
    2.      float3 position,
    3.      float radius,
    4.      Entity entity,
    5.      VisibilityPolygonComputer computer,
    6.      NativeList<OccluderSegment> segments,
    7.      NativeList<float2> polygon,
    8.      T visit,
    9.      NativeHashMap<int, Empty> observerHistory,
    10.      NativeHashMap<Entity, Empty> observedObstructions
    11.      CollisionWorld collisionWorld,
    12.      NativeArray<Entity> teamToEntity,
    13.      BufferFromEntity<VisionElement> visions,
    14.      BufferFromEntity<VisionHistoryElement> history,
    15.      BufferFromEntity<VisionCountElement> count,
    16.      BufferFromEntity<ObserverHistoryElement> observerHistory,
    17.      BufferFromEntity<OccluderSegment> occluderSegments,
    18.      ComponentDataFromEntity<PhysicsCollider> physicColliders,
    19.      ComponentDataFromEntity<Translation> translations,
    20.      ComponentDataFromEntity<Rotation> rotations,
    21.      int2 size,
    22.      int2 offset,
    23.      float scale,
    24.      Color32 visibleColor,
    25.      Color32 visitedColor,
    26.      CollisionFilter collisionFilter,
    27.      bool storeHistory,
    28.      bool clear,
    29.      NativeArray<ArchetypeChunk> contourChunks,
    30.      ArchetypeChunkComponentType<Contour> contourType,
    31.      ArchetypeChunkBufferType<OccluderSegment> occluderSegmentType,
    32.      float heightOffset,
    33.      bool highlightObstructions)
    34.      where T : struct, IMapper
    35. {
    36.      // Do something
    37. }
     
    PublicEnumE likes this.
  13. PublicEnumE

    PublicEnumE

    Joined:
    Feb 3, 2019
    Posts:
    106
    Thanks for the answer - makes sense. Is there also a performance benefit, or is the main benefit code simplification? Honest questions here. :)

    PS. At the last studio I worked at, we used a homebuild ECS engine, and we did just that (used primarily static utility methods). The engineering director had a thing against the “logic as data” paradigm. There were actually compiler checks to make sure utility methods were being used in some cases.

    It took some getting used to, but it was performant. :p Thankfully I never ran into a crazy method signature like that one.
     
    Last edited: Sep 13, 2019
  14. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    1,663
    I'm not actually sure how it performs I've never bothered benchmarking it. It hasn't noticeably hindered my performance but I wouldn't be surprised if it's a bit slower than a static method.

    The reason I started doing it was that for one specific algorithm I was starting to get some really long method signatures (not as long as above but maybe nearly half that length) and I wanted a cleaner way.

    My post has been linked a few times to now and people seem to like the idea so can't be that bad (I hope)

    -edit-

    I'd still just use a static method for most stuff. But for example, the first algorithm I converted to this is 492 lines long so it's dozens of methods and was a lot to pass around.
     
    PublicEnumE likes this.
  15. D4rt

    D4rt

    Joined:
    Apr 20, 2013
    Posts:
    6
    In my case doing this in a struct should be more performant, as we want to cache the controlPoints (and some other components that I omitted for clarity). We might want to call interpolate several times, but fetch the controlPoints only once at the beginning. If this was a static method and the fetching of controlPoints was done every time we call interpolate, it would most likely perform worse. Although what is and isn't performant in the job systems is a bit of a black box to me. I guess it's something you have to learn by practice.

    Other solution would be to leave fetching the controlPoints to the system calling the static function, but as I already said, there are other components that need to be fetched as well and the whole goal was to make this as easy to use in other systems as possible, without them having to worry what extra data is associated with the spline.