Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

[Sources Included] Polymorphism in DOTS, now with Source Generators!

Discussion in 'Entity Component System' started by PhilSA, Apr 8, 2022.

  1. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    Links
    Package Download: [Package]
    Sample Github: [PolymorphicStructs]




    __________________________________________________________


    What does this do?
    This is a codegen tool, using SourceGenerators, that makes it easy to imitate OOP polymorphism, but with structs instead of classes. Based on all structs in your project implementing a certain interface, it generates a single "parent" struct type that contains a union of all the data these other "child" structs can contain, as well as the functions of the interface they implement. When you call an interface function on the generated "parent" struct, it will automatically reconvert itself to its "child" type and call the specific "child" implementation of that function.

    In other words, if you create a IMyEvent interface and several specific structs implementing it, this will generate a MyEvent "parent" struct that can be created from any of the specific "child" event structs implementing the IMyEvent interface. You can then, for example, store a DynamicBuffer<MyEvent> on an entity, and if you iterate on it and call interface functions on those events, it'll call a different implementation of that function for each element of the buffer, based on the specific "child" type it was created from.

    This comes with a sample demonstrating how we can implement an efficient, highly-versatile, simple-to-use, burst-compatible, parallelizable state machine with no structural changes or sync points.

    __________________________________________________________

    How does it work?

    Given this user-created [PolymorphicStruct] interface and two structs implementing it:
    Code (CSharp):
    1. [PolymorphicStruct]
    2. public interface IMyTest
    3. {
    4.     void DoSomething(float a, ref ComponentDataFromEntity<Translation> translationFromEntity);
    5. }
    Code (CSharp):
    1.  
    2. public partial struct TestA : IMyTest
    3. {
    4.     public float A;
    5.     public float B;
    6.     public float C;
    7.     public float3 D;
    8.     public Entity E;
    9.     public Entity F;
    10.  
    11.     public void DoSomething(float a, ref ComponentDataFromEntity<Translation> translationFromEntity)
    12.     {
    13.         B += a * C;
    14.     }
    15. }
    Code (CSharp):
    1.  
    2. public partial struct TestB : IMyTest
    3. {
    4.     public float A;
    5.     public Entity B;
    6.  
    7.     public void DoSomething(float a, ref ComponentDataFromEntity<Translation> translationFromEntity)
    8.     {
    9.         A = math.length(translationFromEntity[B].Value);
    10.     }
    11. }

    The generated files will be:
    Code (CSharp):
    1.  
    2. [Serializable]
    3. public partial struct MyTest
    4. {
    5.     public enum TypeId
    6.     {
    7.         TestA,
    8.         TestB,
    9.     }
    10.  
    11.     public TypeId CurrentTypeId;
    12.     public float float_0;
    13.     public float float_1;
    14.     public float float_2;
    15.     public float3 float3_3;
    16.     public Entity Entity_4;
    17.     public Entity Entity_5;
    18.  
    19.     public void DoSomething(float a, ref ComponentDataFromEntity<Translation> translationFromEntity)
    20.     {
    21.         switch(CurrentTypeId)
    22.         {
    23.             case TypeId.TestA:
    24.             {
    25.                 TestA instance_TestA = new TestA(this);
    26.                 instance_TestA.DoSomething(a, ref translationFromEntity);
    27.                 instance_TestA.ToMyTest(ref this);
    28.                 break;
    29.             }
    30.             case TypeId.TestB:
    31.             {
    32.                 TestB instance_TestB = new TestB(this);
    33.                 instance_TestB.DoSomething(a, ref translationFromEntity);
    34.                 instance_TestB.ToMyTest(ref this);
    35.                 break;
    36.             }
    37.         }
    38.     }
    39. }
    40.  
    Code (CSharp):
    1.  
    2. public partial struct TestA
    3. {
    4.     public TestA(MyTest s)
    5.     {
    6.         A = s.float_0;
    7.         B = s.float_1;
    8.         C = s.float_2;
    9.         D = s.float3_3;
    10.         E = s.Entity_4;
    11.         F = s.Entity_5;
    12.     }
    13.  
    14.     public MyTest ToMyTest()
    15.     {
    16.         return new MyTest
    17.         {
    18.             CurrentTypeId = MyTest.TypeId.TestA,
    19.             float_0 = A,
    20.             float_1 = B,
    21.             float_2 = C,
    22.             float3_3 = D,
    23.             Entity_4 = E,
    24.             Entity_5 = F,
    25.         };
    26.     }
    27.  
    28.     public void ToMyTest(ref MyTest s)
    29.     {
    30.         s.CurrentTypeId = MyTest.TypeId.TestA;
    31.         s.float_0 = A;
    32.         s.float_1 = B;
    33.         s.float_2 = C;
    34.         s.float3_3 = D;
    35.         s.Entity_4 = E;
    36.         s.Entity_5 = F;
    37.     }
    38. }
    39.  
    Code (CSharp):
    1.  
    2. public partial struct TestB
    3. {
    4.     public TestB(MyTest s)
    5.     {
    6.         A = s.float_0;
    7.         B = s.Entity_4;
    8.     }
    9.  
    10.     public MyTest ToMyTest()
    11.     {
    12.         return new MyTest
    13.         {
    14.             CurrentTypeId = MyTest.TypeId.TestB,
    15.             float_0 = A,
    16.             Entity_4 = B,
    17.         };
    18.     }
    19.  
    20.     public void ToMyTest(ref MyTest s)
    21.     {
    22.         s.CurrentTypeId = MyTest.TypeId.TestB;
    23.         s.float_0 = A;
    24.         s.Entity_4 = B;
    25.     }
    26. }
    27.  

    As you can see, the source generators create a "MyTest" struct from the "IMyTest" interface, and this struct can hold a union of all the data that "TestA" and "TestB" can hold. "MyTest" has the interface functions of "IMyTest", and automatically handles converting itself to its assigned type (TestA or TestB) when those functions are called

    Since the final output of this is a simple struct, all of it is fully burstable and there are no weird restrictions to be aware of. It's all very simple code really

    __________________________________________________________

    When to use?

    PolymorphicStructs are a way to associate logic & data to an entity, but independently of its archetype. Therefore, they are useful in situations where organizing logic with entity queries would be too costly or too limiting. In other words; they become useful in cases where you'd have to pay such a high performance price for setting things up for ECS-style linear memory access that the costs of doing this setup would actually outweigh the benefits.

    You must be careful not to over-use them, as they will often not be the DoD way of solving certain problems; but there are certain cases where they do represent the most efficient solution and will largely outperform a more typical "standard ECS" approach.

    Here are signs that using PolymorphicStructs might be an interesting solution to a certain problem:
    • When your current solution heavily relies on frequent/high-volume structural changes
    • When your current solution involves a heavy constant update cost due to change-polling jobs
    • When you have sync points that you wish could be eliminated
    • When your current solution involves scheduling a very large amount of jobs (and/or involves spreading out your entities across a very large amount of different archetypes, only because of one small difference between them). In other words; when you have poor chunk utilization and/or high scheduling costs. Both problems often come together
    • When your current solution involves lots of hard-coding or manually-written switch statements that you wish could be modularized or auto-generated in some way (for better code maintainability or extensibility)
    PolymorphicStructs are essentially a way to avoid structural changes, change-polling, sync points, high archetype counts, poor chunk utilization, and multiple-job scheduling costs, all at the same time

    Let's take for example the use case of implementing an ordered events system, where events of various different types must be processed in order. This is probably the most obvious of all use cases for PolymorphicStructs, because the alternatives would all be too terrible to even consider implementing them

    Structural changes approach:
    • Each event is an entity with a specific component type for the logic to execute
    • Each event type has a MyEventSystem that handles executing the event if it has a "ExecuteEvent" component. These systems are all part of a "EventUpdateGroup"
    • A system has a NativeQueue<Entity> that remembers the order in which the events must be executed
    • The events update iterates on each event Entity of the NativeQueue<Entity>, and does this at each iteration:
      • Add a "ExecuteEvent" component on that event entity
      • Sync point for structural changes
      • Manually update EventUpdateGroup
    • This means if you have 100 events in your queue, and 10 event types, on every frame you will be:
      • Applying 200 structural changes
      • Create 100 separate sync points
      • Scheduling 100 jobs just for event updates (but also testing the existence of 100*10 = 1000 queries)
      • Iterating over 100 event entities to execute them
    Overall, this is an awful solution. Too many sync points, and too many jobs to schedule

    Enabled components approach:
    • This approach is similar to the structural changes approach, but instead of adding a "ExecuteEvent" component to event entities when we want to execute them, we simply write to a "Execute" byte in a component that's pre-added to all event entities
    • So the events update iterates on each event Entity of the NativeQueue<Entity>, and does this at each iteration:
      • Set the "Execute" byte to 1 on the iterated event entity
      • Manually update EventUpdateGroup (only executes events if their "Execute" byte is set to 1, and uses WithChangeFilter() for the Execute component)
    • This means if you have 100 events in your queue, and 10 event types, on every frame you will be:
      • Applying 0 structural changes
      • Creating 1 sync point before the events update just to know how many events we have in our queue (we need to know, because our job scheduling depends on that event count)
      • Scheduling 100*10 = 1000 jobs just for event updates
      • Depending on event archetype sizes, we'd be iterating on 100*numberOfEventsPerChunk event entities to check if we should execute them. But since events will typically be rather small in size, the # of events per chunk is likely to be above 100. So here we'd likely be iterating on about 100*100 = 10,000 event entities per frame even though we only have 100 events
    This could be an improvement over the structural changes approach due to eliminating sync points, but it is still very far from ideal. Too many jobs to schedule, and too many entities to iterate on. Our events update cost still grows very rapidly based on the number of events in the ordered queue and the amount of different event types

    Polymorphic structs approach:
    • All events are implemented as PolymorphicStructs, which means we have a common MyEvent struct that can represent any event type
    • A system has a NativeQueue<MyEvent> representing the events to process in order
    • For an events update, we simply iterate on the NativeQueue<MyEvent> and call .Execute() on each element of the queue
    • This means if you have 100 events in your queue, and 10 event types, on every frame you will be:
      • Applying 0 structural changes
      • Creating 0 sync points
      • Scheduling 1 job for event updates
      • Iterating over 100 events to check if we should execute them
    The PolymorphicStructs approach is a clear winner here. No structural changes, no sync points, only 1 job, and the amount of events to iterate on is exactly the amount of events in the queue.

    Not only does it perform significantly better, but it's also by far the simplest & easiest implementation. It's just as simple as how you'd implement this in OOP, but it has the advantage of being burst-compiled, being doable off of the main thread, and allowing events to be created in parallel from jobs without resulting in any sync points

    __________________________________________________________

    How to install & use
    See [Readme]

    The actual source generators code is here. But you don't need to worry about it because it's all compiled to a .dll. It's just if you're curious to see how it works

    __________________________________________________________

    Performance
    See this post for a comparison between this PolymorphicStructs approach and a StructuralChanges approach:
    https://forum.unity.com/threads/sou...-with-source-generators.1264616/#post-8036138
     
    Last edited: Aug 31, 2022
  2. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,753
    Nice

    I saw your library when you first posted it but never got around to looking into it and forgot about it. I already do something similar myself for a variety of systems (AI, stats) though pretty much hand code it.

    Definitely going to make a note and have a look at this at some point. If I can remove the need for other devs to use unsafe code, even as safe and checked as i've implemented it, all that much better.
     
    Krajca and iamarugin like this.
  3. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    5,533
    That is cool, thanks for sharing it! Though you may want to add a license.

    Although the cubes brought me here, I think it could really be useful for the missiles I have in mind. I want them to be able to change state mid-flight. For example it may be launched like a grenade, then fires engine 5 meters ahead of owner, flies way above target, and once there, turns downwards, homing in on target at super-high velocity. Pretty much a deadly statemachine. :cool:
     
    PhilSA likes this.
  4. davenirline

    davenirline

    Joined:
    Jul 7, 2010
    Posts:
    969
    Please give links/resources to learning how to use SourceGenerators. Is this a separate package?
     
  5. DrBoum

    DrBoum

    Joined:
    Apr 19, 2020
    Posts:
    26
    hello phil,
    i've freshly cloned your repo and it seems like the source generator is not generating the MyState struct, any pointers ?
     
  6. Shinyclef

    Shinyclef

    Joined:
    Nov 20, 2013
    Posts:
    502
    Thanks for sharing. This looks very interesting indeed!
     
  7. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    Here are the steps to make them work in 2020.3.30:
    • try following the example on this page, step by step (just the part under the "Source Generators" section) https://docs.unity3d.com/2021.2/Documentation/Manual/roslyn-analyzers.html
      • Don't forget the part about adding the Microsoft.CodeAnalysis.CSharp 3.8 NuGet package in VS
    • Make sure your generator dll has these options
    • Make sure your generator dll has these two asset labels:
      • RoslynAnalyzer
      • SourceGenerator
    • Make sure you have the com.unity.roslyn package in your project (but Entities 0.50 depends on it)
    • Make sure all the code that can be "found" by the source generator is in an .asmdef that references the .asmdef that your generator dll is in

    As for learning resources, I mostly looked at Unity's code under the Entities package ("Unity.Entities/SourceGenerators"), and did some case-by-case googling. This is a good intro:
    https://itnext.io/getting-into-source-generators-in-net-6bf6d4e9e346
     
    Last edited: Apr 14, 2022
  8. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    Just figured out we can easily control the type of the generated structs through partial structs:

    Code (CSharp):
    1. [PolymorphicStruct]
    2. public interface IMyState
    3. {
    4.     public void OnStateEnter(ref StateUpdateData_ReadWrite refData, in StateUpdateData_ReadOnly inData);
    5.     public void OnStateExit(ref StateUpdateData_ReadWrite refData, in StateUpdateData_ReadOnly inData);
    6.     public void OnStateUpdate(ref StateUpdateData_ReadWrite refData, in StateUpdateData_ReadOnly inData);
    7. }
    8.  
    9. // Using partial structs, we can declare that the generated "MyState" struct will also be a "IBufferElementData"
    10. public partial struct MyState : IBufferElementData
    11. {
    12.  
    13. }
    In this example, the source generators generate a "MyState" based on the "IMyState" interface. But we can declare a "partial struct MyState : IBufferElementData" to make the generated struct be a IBufferElementData

    That way we can directly generate polymorphic DOTS components of any type (IComponentData, IBufferElementData, ISystemStateComponentData, etc...), and we don't have to nest the polymorphic struct inside a component

    I've updated the sample code to make use of this
     
    Last edited: Apr 8, 2022
  9. Enzi

    Enzi

    Joined:
    Jan 28, 2013
    Posts:
    954
    Really amazing work! Thanks a lot for sharing!

    With the stylish nature of source generators I'm still baffled that every line has to be written out in strings but I suppose there's no other way around it? I'd have expected something akin to enclosed statements like "using".
     
    dannyalgorithmic and hippocoder like this.
  10. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    Honestly I don't really know if the way I did it is good practice or not (I started learning about source generators like 3 days ago), but it seems to be the way a lot of the tutorials I've found do it

    It might be possible to build some kind of SyntaxTree and just call .ToString() on it at the end, and it converts the syntax nodes to code directly, but I'm not sure

    I can also imagine that we could use some sort of template system to make things less "write each line one by one". Using StringBuilder would also be an improvement over what I did. It's a potential future improvement, but I wouldn't say it's crucial since this has no effect on runtime performance
     
    Last edited: Apr 8, 2022
  11. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    Added a "Performance" section to the original post

    Conclusions: a "polymorphic" function call for moving translation is roughly 1.4x the cost of moving translation directly.

    It's something that can better inform our decision to use it or not. Keep in mind that while each polymorphic function call is heavier than a direct call, PolymorphicStructs will often make up for that added cost by removing the cost of any structural changes, sync points and additional job scheduling. They will often make the code much more simple as well
     
    Last edited: Apr 8, 2022
  12. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    Is it an error in the Unity console, or is it that in Visual Studio you can't find the generated struct? I've tried doing a fresh clone on another PC of mine and couldn't find issues

    What I'm using, in case it matters:
    • Windows 11
    • Visual Studio 2022
    • Unity 2020.3.30f1
     
  13. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    Pushed an update to the sample where we can choose to use a "structural changes" state machine implementation instead of the polymorphic one, in order to compare the two approaches



    Here are my conclusions:

    _________________________________________________________________

    Code size/complexity

    There is a big difference between the size (lines of code) of each approach's implementation. On the left is the PolymorphicStructs implementation, and on the right is the StructuralChanges implementation



    You can look at the actual code of each implementation here:
    PolymorphicStructs
    StructuralChanges

    Note that the StructuralChanges approach is at a disadvantage because it needs generic jobs in order to work, and these cannot be done with Entities.ForEach or IJobEntities at the moment. So perhaps the implementation would shrink a little in future DOTS updates, but it would still be at least 3x larger than the PolymorphicStructs implementation.

    Maintenance-wise, the StructuralChanges approach necessitates a whole new job to be written every time we want to add a new function to our states, and needs new systems & generic jobs to be declared for each state. The PolymorphicStructs approach, on the other hand, never needs new jobs or systems, no matter how many states or functions you add.

    _________________________________________________________________

    Performance

    I've measured the total frame cost of each approach. Some details about the test:
    • We run 300,000 state machines updating at random speeds, so the state change cost is spread out relatively evenly across frames
    • All renderers/camera disabled
    • For each approach, we have the cost of scheduling jobs on a single thread VS scheduling in parallel
    • We use a fixed deltaTime value of 0.02 for these tests (but no catch-up), in order to have a more accurate performance comparison
    • All profiling is done in builds
    StructuralChanges approach:
    PolymorphicStructs approach:
    There are very little differences in frame timings for the StructuralChanges approach, between "Single" and "Parallel". This is because nearly all of the frame cost is spent applying structural changes on the main thread in both cases

    Notice the differences in the "Job" row of the Profiler (the green bars in there represent jobs running on the available threads). The StructuralChanges approach has big holes in there where no jobs are allowed to run, because structural changes are being applied. This is another hidden advantage of the PolymorphicStructs approach: not only does it perform much better, requires less code, and has more consistent frame times; but it also allows other jobs in your game to run during the entire frame on top of it all.

    Also, if we synchronize all state machines to change states at the same time, the StructuralChanges approach gets these enormous spikes of 700ms on state changes and an average time of 4.5ms when states are not changing. The same test case with the PolymorphicStructs approach gives us spikes of 8ms on state changes and an average time of 4ms when states are not changing.

    EnabledComponents approach
    I've also attempted a quick "Enabled Components" implementation, where each state is an Entity, and each state function has its own job that runs only if a certain byte is set to 1 on a component on the state entity. However, so far the frame time for this approach is around 20ms. It's better than the structural changes, but a lot worse than the polymorphic structs. The main problem with this approach is that since state entities need to read/write to the state machine's entity (a separate entity) via ComponentDataFromEntity, the read/writing is much more expensive. This approach is also only parallelizable if we disable parallel for restrictions. And finally, I'd say that this approach is the most difficult to make user-friendly and boilerplate-free of all 3 so far. But I will try to see if I can come up with a better implementation of it.

    If you'd like to try out the EnabledComponents approach, disable the "Spawner" gameObject in the subscene, and enable "EnabledComponentsSpawner" instead
     
    Last edited: Apr 12, 2022
    mikaelK, NotaNaN and hippocoder like this.
  14. Soaryn

    Soaryn

    Joined:
    Apr 17, 2015
    Posts:
    328
    So far this seems like it will fit my needs for something I was trying to solve; however, I am curious if an implicit conversion from Specific type to the polymorphic type would be easy to add. I would try myself, but I have no idea how to edit the DLL off hand
    Mostly so it would appear cleaner when adding to say a list

    First thought of the backing code would be along the lines of
    Code (CSharp):
    1. public static implicit operator PseudoParentType(TypeA d) => new PseudoParentType {
    2.     CurrentTypeId = PseudoParentType.TypeId.TypeA,
    3. };
    4.  
    5. public static explicit operator TypeA(PseudoParentType d) => new TypeA(d);
    This would hopefully allow for some cleaner uses code style wise.
    Code (CSharp):
    1. var list = new List<PseudoParentType>();
    2.  
    3. //From
    4. list.Add(new TypeA().ToPseudoParentType());
    5. //To
    6. list.Add(new TypeA());
     
    Last edited: Apr 28, 2022
  15. Soaryn

    Soaryn

    Joined:
    Apr 17, 2015
    Posts:
    328
    Also sadly, BlobAssetReferences break to the code gen quite a bit :p
     
  16. Soaryn

    Soaryn

    Joined:
    Apr 17, 2015
    Posts:
    328
    Final bit of feedback for the moment, is there a way to make the source gen work across namespaces?
    Right now, if I make events, it works perfectly fine so long as the namespace matches the interface namespace; however, if I scatter these events to different namespaces, the using statements don't pull the namespace that the events are in.

    Adding the ability to have an arbitrary namespace per event struct would be extremely helpful for designing an API layer like package.
     
  17. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    @Soaryn all good suggestions. I may not find the time to make these additions in the next few days, but I'll put them on my todo list
     
    Soaryn likes this.
  18. Soaryn

    Soaryn

    Joined:
    Apr 17, 2015
    Posts:
    328
    Awesome! The namespace and overrides are work aroundable atm... but the blobasset breaking is something that is a little hard to work around atm. But hopefully it is a minor fix when you do find the time
     
  19. Soaryn

    Soaryn

    Joined:
    Apr 17, 2015
    Posts:
    328
    I think I see part of the problem on the source gen result :p Seems the variable name is what breaks down due to the generic.
    upload_2022-4-29_4-18-26.png
     
  20. xseagullx

    xseagullx

    Joined:
    Nov 7, 2019
    Posts:
    24
    There is a weird bug I see, that affects my IDE.

    I use Rider, and it is only able to see generated code, if
    PolymorphicStructsSourceGenerators
    is in the project. If I move the dll to the package and import that package -- generation happens, code compiles, but in IDE it will be highlighted red and refuse to resolve.

    I wonder if anyone in this thread has a solution.
     
  21. xseagullx

    xseagullx

    Joined:
    Nov 7, 2019
    Posts:
    24
    Hey, @PhilSA
    Please consider this PR. PR description should be quite detailed. I would love to get in touch about tests though, and the project to re-build DLL. Will it be OK, if I ping you in discord? Right now, I created a project locally and can build the dlls, but I did not include it in the PR.
    So there is no easy way for you to run the tests I wrote, as well as no easy way to re-build DLL.
    But I'm assuming you actually have a project somewhere too, so I wonder if it can be included in the repo.
     
  22. brunocoimbra

    brunocoimbra

    Joined:
    Sep 2, 2015
    Posts:
    679
    Update the com.unity.ide.rider package (it doesn't show as an update by default as it is not verified yet, just click to show other version and the versions 3.x will be there).
     
    xseagullx likes this.
  23. TieSKey

    TieSKey

    Joined:
    Apr 14, 2011
    Posts:
    223
    Interesting code. Plz remember that structs will initialize their variables to the "default" value of the type which might be bigger than a "null", so, take care when using a LOT of derived types with their own variables and a LOT of entities of the "parent" type cuz memory usage will increase.
     
  24. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    I've pushed the .sln to the repo, let me know if you're able to build the dll with this

    Thanks for the PR. I'm a bit too busy at the moment to review it, but I hope to come back to it later
     
  25. xseagullx

    xseagullx

    Joined:
    Nov 7, 2019
    Posts:
    24
    Oh, don't worry, I can build it locally.
    I will update the PR, to utilise the solution, so we can have tests running there as well. I can also build and push the dlls, if you prefer.
     
  26. SergeyRomanko

    SergeyRomanko

    Joined:
    Oct 18, 2014
    Posts:
    47
    I can't open PolymorphicStructsSourceGenerators.sln because PolymorphicStructsSourceGenerators\PolymorphicStructsSourceGenerators.csproj is missing
     
  27. xseagullx

    xseagullx

    Joined:
    Nov 7, 2019
    Posts:
    24
    Please, try the PR branch. I've just fixed solution there. (should have done it ages ago)
    If you see that tests do not compile, force re-build solution. It'll recompile source gen dll. Tests use new features, not available in original dll.
     
  28. Dechichi01

    Dechichi01

    Joined:
    Jun 5, 2016
    Posts:
    39
    That's a very interesting approach. I have been using it on a prototype I'm making for an AnimationState machine.

    The major piece of problem I encountered is that the current implementation does not support generating code for implementations outside of the .asmdef in which the interface is defined. i.e if I define the PolymorphicStruct interface in assembly A, and try to implement it in assembly B, the source generator won't pick the implementation up.
     
    mikaelK likes this.
  29. xseagullx

    xseagullx

    Joined:
    Nov 7, 2019
    Posts:
    24
    Hey, @Dechichi01!

    I wonder what the use case for that would be? If you wanna have common code in plugin asmdef, and allow users to define their behavior, that's exactly why I raised PR above. I hope Phil will have time to look at it, and it'll be merged at some point :)
    With it you should be able to define an interface (without annotation, can have Generics) that your logic will use in assembly A. Let's call it base interface.

    Then in the assembly B you create another interface, this time with Polymorphic attribute, and inherit it from the base interface. You then define implementing structs in the same assembly.

    Will it solve your issue? Do I make sense?
     
    Dechichi01 and MostHated like this.
  30. Dechichi01

    Dechichi01

    Joined:
    Jun 5, 2016
    Posts:
    39
    Thanks for the workaround. That's precisely the use case I had (defining an interface that could be implemented by users). Your workaround works, though I think it would be nice for the tool should probably support this, so hopefully your PR get's approved!
     
  31. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    I've approved & merged the PR, and updated the project for DOTS 0.51 and Unity 2021.3. Everything looks great, sorry it took such a long time! I just had to add the "NUnit3TestAdapter" package to make tests run in VS 2022 (they were all getting skipped otherwise). Thank you so much for the contribution

    I was told 2021.3 should improve source generators support with IDEs, and from what I've seen it certainly looks like the generated code gets detected properly so far in VS

    New release downloadable here https://github.com/PhilSA/PolymorphicStructs/releases/tag/0.3
     
    Last edited: Jul 4, 2022
  32. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    3,296
    Looks interesting, but unfortunately, importing package in large projects cause either infinite, or VERY (>10 min) long script recompilation. (with Unity 2021.3.5f1)

    And I'm not sure what's going, whether its the package, or the generators that mess up Unity that badly;

    So previously, I was using DynamicBuffers per damage type, and it didn't scale well with amount of code required to be written and maintained. For these types of cases "Polymorphic" structures seems to be the best solution as well.

    So no:
    - No extra systems to maintain (no extra jobs required);
    - No extra memory footprint from multiple buffers;
    - No extra archetypes created (less chunk fragmentation);
    And most importantly - magnitudes less code required to be maintained (speaking about hundreds of lines of code);

    Such simple solution as to put type inside the data struct and switch alike what virtual table does for the C#. Wish I figured out this initially. Even writing it down manually without a codegen is magnitudes more faster than what I did initially. Reason was - "switch is bad" C# agenda. But in reality, each and everyone uses interfaces and virtual types, and its somehow okay. And tests show that Burst can chew through switches just fine, with completely valid speeds.

    As for the strutural changes aspect, I think I'll just replace that logic with aggregated bitmask and call it a day.


    In any case, this gave some very important idea for me, and I just wanted to say thanks.
     
    yokobe0012 and Harry-Wells like this.
  33. JohnTomorrow

    JohnTomorrow

    Joined:
    Apr 19, 2013
    Posts:
    135
    This is really cool stuff! I am just now familiarizing myself with ECS, its been a bit of learning curve and its hard to know what tool is best suited for the job with so many options. I was wondering, for a state machine, with the new ability of being able to toggle the IDataComponent bit for enabled/disabled how do you all feel about using this polymorphism approach over using systems for each state? I guess the bit solves the major structural change problem but it will still require a bunch of more code compared to the polymorphism approach. Any other major pros/cons?
     
  34. toomasio

    toomasio

    Joined:
    Nov 19, 2013
    Posts:
    198
    This design approach is suited if it is crucial for modular "events" to send and receive in a specific order, where something that happens during runtime dictates that order...and you need it to happen in the same frame instead of offloading the event to a queue. I have yet to see a better solution to solve this problem. Currently using this solution for a visual graph workflow in ecs...where nodes/flows wont always be in the same order depending on the designer.
     
    JohnTomorrow likes this.
  35. Kleptine

    Kleptine

    Joined:
    Dec 23, 2013
    Posts:
    274
    Hi, is there any update on this library? How does it work with DOTS 1.0? Any other more proper options?
     
  36. Laicasaane

    Laicasaane

    Joined:
    Apr 15, 2015
    Posts:
    358
    You might take a look at Trove which is his updated version. I currently use Trove in ECS 1.0 with no issue.
    https://github.com/PhilSA/Trove/
     
    toomasio likes this.