Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Feedback Boilerplate in ISystem

Discussion in 'Entity Component System' started by illinar, May 3, 2023.

  1. illinar

    illinar

    Joined:
    Apr 6, 2011
    Posts:
    857
    A lot of code could be moved to extension methods by developers according to their needs, however I encountered some blocks doing that. Here DOTS doesn't allow me to use SystemAPI outside a system, and thus I can't get the singleton?

    All over my systems I see code that could be reduced 2-5x in size.

    Code (CSharp):
    1.                 // Original code:
    2.                 var reqEntity = ecb.CreateEntity();
    3.                 ecb.AddComponent<TestRPC>(reqEntity);
    4.                 ecb.AddComponent(reqEntity, new SendRpcCommandRequest { TargetConnection = SystemAPI.GetSingletonEntity<ServerConnection>() });
    5.  
    6.                 // Using extension method (doesn't work):
    7.                 ecb.CreateServerRPC(state, new TestRPC());
    The extension method in question that I wish I could write. (I also couldn't figure out how to satisfy a non-nullable constraint for ecb.AddComponent)

    Code (CSharp):
    1.     public static void CreateServerRPC<T0>(this EntityCommandBuffer ecb, SystemState state, T0 rpc0) where T0 : struct, IRpcCommand
    2.     {
    3.         var rpcEntity = ecb.CreateEntity();
    4.         ecb.AddComponent(rpcEntity, rpc0);
    5.         var serverConnectionEnity = SystemAPI.GetSingletonEntity<ServerConnection>();
    6.         ecb.AddComponent(rpcEntity, new SendRpcCommandRequest { TargetConnection = serverConnectionEnity });
    7.     }
    I started writing extension methods to drastically reduce the amount of code, and to have the API more consistent where I could just do everything through "state.", but SystemAPI stopped working outside systems, and for some reason some important functionality is there and not the EntityManager and not SystemState.

    Another case of a substantial footprint reduction, but that I actually can do:

    Code (CSharp):
    1. var e = state.EntityManager.CreateEntity(typeof(TestRPC));
    2. state.EntityManager.SetComponentData(e, new TestRPC { Value = 1});
    3.  
    4. var e2 = state.CreateEntity(new TestRPC { Value = 1 });
     
    Last edited: May 3, 2023
    apkdev likes this.
  2. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    3,983
    This has been a huge blocker for allowing my tech to play more nicely with other people's things. Even allowing custom OnCreateForCompiler() methods using an attribute or something so that we can cache things in ISystems with custom source generators would go a long ways.
     
    apkdev likes this.
  3. illinar

    illinar

    Joined:
    Apr 6, 2011
    Posts:
    857
    Yes, ability to extend source gen could be nice. I can see it coming later down the line. For now I just want to be able to do everything via state and ecb with my extension methods, and write my extension methods tailored to my project.

    And someone gave me idea about the solution. Getting singletons from Queries, not SystemAPI :)
     
    Last edited: May 3, 2023
  4. illinar

    illinar

    Joined:
    Apr 6, 2011
    Posts:
    857
    So it's more of an issue thread then, and It is pretty much resolved so far. Hopefully I will be able to write all the necessary extension methods.

    This one works now (using Query instead of SystemAPI):

    Code (CSharp):
    1.                 // Original:
    2.                 var reqEntity = ecb.CreateEntity();
    3.                 ecb.AddComponent<TestRPC>(reqEntity);
    4.                 ecb.AddComponent(reqEntity, new SendRpcCommandRequest { TargetConnection = SystemAPI.GetSingletonEntity<ServerConnection>() });
    5.  
    6.                 // Custom:
    7.                 ecb.CreateServerRPC(state, new TestRPC());
    Code (CSharp):
    1.     public static void CreateServerRPC<T0>(this EntityCommandBuffer ecb, SystemState state, T0 rpc0) where T0 : unmanaged, IRpcCommand
    2.     {
    3.         var rpcEntity = ecb.CreateEntity();
    4.         ecb.AddComponent(rpcEntity, rpc0);
    5.         var serverConnectionEnity = state.GetEntityQuery(typeof(ServerConnection)).GetSingletonEntity();
    6.         ecb.AddComponent(rpcEntity, new SendRpcCommandRequest { TargetConnection = serverConnectionEnity });
    7.     }
    EDIT: It compiled but I forgot that Burst won't take typeof and params[] so here is the new version:
    Code (CSharp):
    1.     public static Entity CreateServerRPC2<T>(this EntityCommandBuffer ecb, SystemState state, T c1) where T : unmanaged, IComponentData
    2.     {
    3.         var serverConnectionEnity = state.GetEntityQuery(ComponentType.ReadOnly<ServerConnection>()).GetSingletonEntity();
    4.         var c2 = new SendRpcCommandRequest { TargetConnection = serverConnectionEnity };
    5.         var e = ecb.CreateEntity();
    6.         ecb.AddComponent(e, c1);
    7.         ecb.AddComponent(e, c2);
    8.         return e;
    9.     }
    Or this insane looking one, and I'm wondering if it is a bit faster:
    Code (CSharp):
    1.     public static Entity CreateServerRPC<T>(this EntityCommandBuffer ecb, SystemState state, T c1) where T : unmanaged, IComponentData
    2.     {
    3.         var type = new NativeArray<ComponentType>(2, Allocator.Temp);
    4.         type[0] = ComponentType.ReadOnly<T>();
    5.         type[1] = ComponentType.ReadOnly<SendRpcCommandRequest>();
    6.         var archetype = state.EntityManager.CreateArchetype(type);
    7.         var serverConnectionEnity = state.GetEntityQuery(ComponentType.ReadOnly<ServerConnection>()).GetSingletonEntity();
    8.         var c2 = new SendRpcCommandRequest { TargetConnection = serverConnectionEnity };
    9.         var e = ecb.CreateEntity(archetype);
    10.         ecb.SetComponent(e, c1);
    11.         ecb.SetComponent(e, c2);
    12.         return e;
    13.     }
    14.  
    I have a feeling that Burst would make them do the same thing, or the difference is negligeble to begin with. Thinking about the case where adding 4 components consecutively is a structural change every time unlike setting them.

    And for creating an entity with EntityManager do I HAVE TO do it that way? Because unlike ecb it can't infer the component type from the type of the given IComponentData
    Code (CSharp):
    1.    
    2.     public static Entity CreateEntity<T0, T1, T2>(this SystemState state, T0 c1, T1 c2, T2 c3)
    3.     where T0 : unmanaged, IComponentData
    4.     where T1 : unmanaged, IComponentData
    5.     where T2 : unmanaged, IComponentData
    6.     {
    7.         var types = new NativeArray<ComponentType>(3, Allocator.Temp);
    8.         types[0] = ComponentType.ReadOnly<T0>();
    9.         types[1] = ComponentType.ReadOnly<T1>();
    10.         types[2] = ComponentType.ReadOnly<T2>();
    11.         var archetype = state.EntityManager.CreateArchetype(types);
    12.         var entity = state.EntityManager.CreateEntity(archetype);
    13.         state.EntityManager.SetComponentData(entity, c1);
    14.         state.EntityManager.SetComponentData(entity, c2);
    15.         state.EntityManager.SetComponentData(entity, c3);
    16.         return entity;
    17.     }
    18.  
     
    Last edited: May 3, 2023
  5. OUTTAHERE

    OUTTAHERE

    Joined:
    Sep 23, 2013
    Posts:
    656
    So much this. :(

    I think your approach is interesting, I've been looking to make something that works a little bit how Bevy spawns entities - but that requires using a tuple/value tuple, and Burst really doesn't like those despite them being fairly simple abstractions.
     
    Last edited: May 6, 2023
  6. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    3,292
    As a suggestion, create archetype only once before anything else runs to ensure it exists.
    When you need it - fetch it and use it inside of extension. Or, system.

    Alternatively, create & cache archetype in the system itself (in OnCreate);
    That way you can pass it into the method to be used with ECB.

    This will cut amount of EntityManager accesses as well as param[] allocs.
    Also, while using ECB with consecutive .AddComponent works, its slower than to have a "complete" entity created via EntityArchetype overload.

    What happens is this per each component type:
    Archetype Alloc -> Move Data -> Archetype Alloc -> Move Data -> ...

    What you want:
    Archetype Alloc -> Set Data
     
    Last edited: May 7, 2023
  7. DaxodeUnity

    DaxodeUnity

    Unity Technologies

    Joined:
    Aug 5, 2021
    Posts:
    27
    We did investigate supporting SystemAPI into static functions, but didn't get there in time for 1.0, so let's see what the future brings, for now the solution to users is to use the backing types for all systemapi functionality. E.g. SystemAPI.GetComponent is caching a ComponentLookup, and indexing it. All singleton api is just caching an EntityQuery and calling the methods on it. For more detail on this: https://docs.unity3d.com/Packages/com.unity.entities@1.0/manual/systems-systemapi.html#access-data

    As for the CreateEntity params burst problem, a proposal like spans as params would be burst compliant: https://github.com/dotnet/csharplang/blob/main/proposals/params-span.md if it makes it way into C# that is ;)

    A user-made OnCreateForCompiler can also be done in a similar vein by having sourcegen create a function, and OnCreate calling it. Wouldn't save you from calling the function, but it would save you from doing all the custom caching work :3
     
    TheOtherMonarch and Anthiese like this.
  8. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    3,983
    From a third-party library developer's point-of-view, this is way too easy to forget. I feel like my best approach would probably be to use ILPP to inject the extra OnCreateXXX method calls into the existing methods, as code weaving is better documented in Cecil than the concrete generic instantiation I couldn't figure out. But if the SystemAPI static methods thing is going to come by the end of the year, I may just want to wait.
     
  9. DaxodeUnity

    DaxodeUnity

    Unity Technologies

    Joined:
    Aug 5, 2021
    Posts:
    27
    Agreed, and if this is the route you wanna take, ILPP can inject that call for you. Everything else, still being the same.

    Wouldn't count on that. Having investigated is a sign we know it's a problem. It doesn't mean we have the time to focus on a proper solution yet. If you have a use case, and you would like to discuss how to do it today? That i can help with :D
     
  10. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    3,983
    That's good insight! Thank you!

    I can provide two specific use cases where these sorts of things would be useful.

    In one case, I have a static method as public API that when given an EntityQuery builds a special data structure through the use of 5 different jobs. The first job is an IJobChunk and requires specific ComponentTypeHandles. Right now, I have a wrapper struct that encapsulates these handles that a user creates, caches, and updates, and then passes to the static method. I'd like to do away with that and just let them "generate" the handles directly in OnUpdate(). The way I would do this with source generators is make the static method generic (that could either be a new method just for the type handles or the scheduler itself) on the system type and then source generators add extra code to the system to hold the cached type handles such that the method can retrieve them. The issue is actually creating the type handles, and that's where I would probably need ILPP to extend the OnCreate() methods.

    The other use case is that I am working on multiple alternate transform systems, that all work a bit differently, and only one is ever enabled at a time based on scripting define symbols. I want to create some generalized types that can abstract the specific details for common use cases. And that means each of these types would have to be self-managing of their own type handles and lookups and all. Again, I know the path to achieve this today, but it is a fair bit of work and tricky to maintain relative to just having static methods and no custom source generators or ILPP.

    The use case I'm actually stumped on is this thread: https://forum.unity.com/threads/fixing-generic-jobs-for-real.1428796/
    Right now I am using the "third best option" along with calling EarlyJobInit via reflection for unused combinations that Burst doesn't detect, and it seems to be working so far. But if the user's custom type is a concrete generic itself, then it falls apart. Plus the runtime reflection and direct calling of EarlyJobInit are probably frowned upon.
     
  11. JesOb

    JesOb

    Joined:
    Sep 3, 2012
    Posts:
    1,081
    Currently C# have feature in the works for .Net8 that do exactly this (redirect method call to source generated method).
    Called Interceptors https://github.com/dotnet/roslyn/bl...7b885201c7c8dc0/docs/features/interceptors.md

    so in Unity 2024+ (or may be earlier if Unity decides to use latest roslyn in Unity 2023 LTS) we will be free from OnCreateForCompiler and friends. Also can create api that dont generate code nut just create fields with caches for user.

    I just want to say that any today designs most likely want to take into account this Future Feature :)
     
    PolarTron likes this.
  12. DaxodeUnity

    DaxodeUnity

    Unity Technologies

    Joined:
    Aug 5, 2021
    Posts:
    27
    We're curious to see if it makes it in, but if it does, my view is that it hopefully stops some of the crazier stuff from happening like what we sometimes do with ILPP today :3
     
    Anthiese likes this.