Search Unity

Bug Code generation does not work with generic IComponent types

Discussion in 'NetCode for ECS' started by Jawsarn, Dec 12, 2021.

  1. Jawsarn

    Jawsarn

    Joined:
    Jan 12, 2017
    Posts:
    245
    I'm doing some code generation where I'm having generic component variants like

    Code (CSharp):
    1.  
    2.    [assembly: RegisterGenericComponentType(typeof(GameplayAttributeData<Health>))]
    3.  
    4.    public struct Health {}
    5.  
    6.     public struct GameplayAttributeData<T> : IComponentData where T : struct
    7.     {
    8.         [GhostField]
    9.         public float Value;
    10.         public float MaxValue;
    11.         public float MinValue;
    12.     }
    This does not work with the current code generator as it produces faulty code of

    "...GameplayAttributeData`1GhostComponentSerializer"

    "ref var component = ref GhostComponentSerializer.TypeCast<...GameplayAttributeData`1>(componentData, 0);"

    Would be nice to know if this is something solved in the future e.g. 0.50 or 1.0
     
  2. CMarastoni

    CMarastoni

    Unity Technologies

    Joined:
    Mar 18, 2020
    Posts:
    900
    We don't support it in right now and I don't think we can make it for 0.5.
    It is something we can plan to add for 1.0.

    In general, I dunno if there is a different pattern to use/exploit for this kind of situation.
    Given that MaxValue and MinValue are set at design time and don't change (I presume, since it is not replicated) I wondering if using something like:

    Code (csharp):
    1.  
    2. struct Health : IComponentData { [GhostField] float Value; }
    3.  
    4. struct GameplayAttributeData<T> : IChunkComponent where T: IComponentData
    5. {
    6.     public float MinValue;
    7.     public float MaxValue;
    8. }
    9.  
    would have the advantages of saving memory (only one gameplay component per type per chunk) at the cost of a slightly (unfortunately) more complex use, but achieving the same result.

    An authoring component can just have the values all together and add the chunk and component to the entity at conversion time.
     
  3. Jawsarn

    Jawsarn

    Joined:
    Jan 12, 2017
    Posts:
    245
    Thank you for the reply!

    To start of; the IChunkComponent seems very interesting! I guess it separates chunks similar to ISharedComponentData?
    However the Min and MaxValues are really irelelvant here for what I'm doing.

    What I'm trying to accomplish is very inspired of UE Gameplay Ability System system. To be able to specify gameplay logic dependencies and effects in the editor in an easy way. I have GameplayAffected entities which can have various GameplayAttribute<T>s and can have GameplayAbilities which have GameplayEffects which can have various GameplayAttributeModifier<T>. By this I have a GameplayAttributeModifySystem<T> which sums up modifications by different effects and applies it to attributes. My script generation simply scans project of GameplayAttribute SOs and outputs some code as below. If this would be considered added support for in 1.0 it would be nice to also add "RegisterGenericSystemType".

    Thank you! :)

    Code (CSharp):
    1. using SpaceCraft.ClientAndServer.GameplayEffects.Modifier;
    2. using SpaceCraft.ClientAndServer.GameplayAttributes;
    3. using Unity.Entities;
    4. using Unity.NetCode;
    5.  
    6. [assembly: RegisterGenericComponentType(typeof(GameplayAttributeData<Health>))]
    7. [assembly: RegisterGenericComponentType(typeof(GameplayAttributeModifier<Health>))]
    8. [assembly: RegisterGenericComponentType(typeof(GameplayAttributeData<Shield>))]
    9. [assembly: RegisterGenericComponentType(typeof(GameplayAttributeModifier<Shield>))]
    10. [assembly: RegisterGenericComponentType(typeof(GameplayAttributeData<Speed>))]
    11. [assembly: RegisterGenericComponentType(typeof(GameplayAttributeModifier<Speed>))]
    12.  
    13. namespace SpaceCraft.ClientAndServer.GameplayAttributes
    14. {
    15.     public struct Health {}
    16.     public struct Shield {}
    17.     public struct Speed {}
    18.  
    19.     [UpdateInGroup(typeof(ClientAndServerSimulationSystemGroup))]
    20.     public class GameplayHealthAttributeModifySystem : GameplayAttributeModifySystem<Health> { }
    21.     [UpdateInGroup(typeof(ClientAndServerSimulationSystemGroup))]
    22.     public class GameplayShieldAttributeModifySystem : GameplayAttributeModifySystem<Shield> { }
    23.     [UpdateInGroup(typeof(ClientAndServerSimulationSystemGroup))]
    24.     public class GameplaySpeedAttributeModifySystem : GameplayAttributeModifySystem<Speed> { }
    25.  
    26.     public static class GameplayAttributesUtility
    27.     {
    28.         public static void AddAttributeToEntity(GameplayAttribute gameplayAttribute, Entity entity, EntityManager dstManager)
    29.         {
    30.             var attributeName = gameplayAttribute.name;
    31.             switch (attributeName)
    32.             {
    33.                 case "Health":
    34.                 {
    35.                     dstManager.AddComponentData(entity, new GameplayAttributeData<Health>());
    36.                     break;
    37.                 }
    38.                 case "Shield":
    39.                 {
    40.                     dstManager.AddComponentData(entity, new GameplayAttributeData<Shield>());
    41.                     break;
    42.                 }
    43.                 case "Speed":
    44.                 {
    45.                     dstManager.AddComponentData(entity, new GameplayAttributeData<Speed>());
    46.                     break;
    47.                 }
    48.  
    49.             }
    50.         }
    51.     }
    52. }
    53.  
     
  4. CMarastoni

    CMarastoni

    Unity Technologies

    Joined:
    Mar 18, 2020
    Posts:
    900
    It does not separate the chunk like the SharedComponent. It allow you to have only one instance of that component per chunk (instead of one per entity). So if all you health min/max are the same for a certain entity archetype, it may be beneficial in terms of memory (no need to duplicate the data multiple times). It is handful in some specific use cases.

    In the architecture you shown the assumptions is that all the components and modified has the exact same parameters. And that works pretty fine, until you need some more differentiation (ex: some attribute need different type of Value);
    I think it is a little more flexible having the attribute Value inside the component itself and the GameplayAttributeData<T> and GameplayModifier<T> as separate components in general for that use case, since it give you a little more flexibility.

    That is why I suggested to split the struct in two parts, so one is modifiable and serialisable and the others are pretty much "constant" data. That can also not be the case of course, because of that reason and depending on how your system works, they can be normal IComponentData (especially if attribute components can be modified)

    It would work exactly the same as now, with the only difference that to modify the health you will need something like:
    Code (csharp):
    1.  
    2. var dt = Time.DeltaTime;
    3. Entities.ForEach((Entity ent, ref Health health, in GameplayAttributeData<Health> data, in GameplayAttributeModifier<Health> mod)=>
    4. {
    5.      health.Value = math.clamp(health.Value + mod.Value*dt, data.MinValue, data.MaxValue);
    6. });
    7.  
    This as just a work around I suggest for 0.5 (or better, until we support the generic interface) and that will let you maintain your current game architecture.[/quote]
     
  5. Jawsarn

    Jawsarn

    Joined:
    Jan 12, 2017
    Posts:
    245
    Interesting indeed!

    It's by design it is only one value per component here, as it should only be touched by specific systems & only changed by modifiers. For other types I would do GameplayFloatAttribute<T> GameplayIntegerAttribute<T> etc with their seperate but similar generic system, but it is nothing that I see use for now.

    In the end I realized I could just do some text template the systems and generate the code for them by hand then doing it generic. But another thing was that I could use a interface on the attribute, and thus being able to keep it generic - as a temporary solution until 1.0.

    What I end up with then;
    Code (CSharp):
    1.     public interface IGameplayAttribute
    2.     {
    3.         public float GetValue();
    4.         public void SetValue(float value);
    5.     }
    6.  
    7.     public struct Health : IGameplayAttribute, IComponentData
    8.     {
    9.         [GhostField]
    10.         public float Value;
    11.      
    12.         public float GetValue()
    13.         {
    14.             return Value;
    15.         }
    16.  
    17.         public void SetValue(float value)
    18.         {
    19.             this.Value = value;
    20.         }
    21.     }
    Which I can then use in a generic system
    Code (CSharp):
    1.             Entities.ForEach((Entity entity, ref T attribute, in GameplayAttributeData<T> gameplayAttributeData, in DynamicBuffer<GameplayAttributeModifier<T>> modifierBuffer) =>
    2.             {
    3.                ...
    4.             });
    (But in a IJobForEachWithEntity for now as Entities.ForEach does not support generic^^)

    Though the get and setter probably has some overhead, but something I can live with until it is supported :).

    Thank you!