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

Question Using persistent dictionary with ECS

Discussion in 'Entity Component System' started by alec100_94, Sep 20, 2020.

  1. alec100_94

    alec100_94

    Joined:
    Jan 9, 2017
    Posts:
    26
    Hi, I am trying to get my head round the new ECS system to use it as the basis of the statistics system for our game, I am not trying to build a whole game with ECS or even a hybrid-ECS approach, I basically just want ECS to handle stats with some communication between my system and existing MonoBehaviours. My main reason for doing this is because I wanted the stats to be "Data Oriented", with a clear separation of data and logic and I thought that was essentially what ECS done.

    I have a system that sort of works and makes sense in my head, but am struggling with how one would go about implementing a basic dictionary (hashmap/lookup table) that is persistent before running (never changes at runtime) and can be used in my system (using this to convert string based attack names into equivalent damage values). From what I've seen this may be a Blob based system but that seems to involve an awful lot of boilerplate for something so simple and I can't seem to find an example of how that would work for my use case either. I have tried the NativeHashMap and keep getting errors however I use it. Here is my code for an idea of what I am trying to do:

    Code (CSharp):
    1.        
    2. public class StatsSystem : SystemBase
    3. {
    4.     protected override void OnUpdate()
    5.     {
    6.         Entities.ForEach((ref PlayerStatsDispatcher dispatcher, ref CharacterStats characterStats) => {
    7.             PlayerWasAttacked(ref dispatcher, ref characterStats);
    8.         }).ScheduleParallel();
    9.  
    10.         Entities.ForEach((ref MonsterStatsDispatcher dispatcher, ref MonsterStats monsterStats) => {
    11.             MonsterDidAttack(ref dispatcher, ref monsterStats);
    12.             MonsterWasAttacked(ref dispatcher, ref monsterStats);
    13.         }).ScheduleParallel();
    14.     }
    15.  
    16.     static void PlayerWasAttacked(ref PlayerStatsDispatcher dispatcher, ref CharacterStats characterStats)
    17.     {
    18.         if(!(dispatcher.wasAttacked is PlayerStatsDispatcher.PlayerWasAttacked wasAttacked)) return;
    19.         MonsterStats monsterStats = wasAttacked.monster;
    20.         uint attackDamage = 2;
    21.         // TODO: Set attackDamage from {monsterStats.name}_{monsterStats.lastAttack} in monsterAttackDamageValues Lookup Table
    22.  
    23.         if(attackDamage == 0) return;
    24.         characterStats.hp.x -= characterStats.hp.x > 0 ? attackDamage : 0;
    25.         dispatcher.wasAttacked = null;
    26.     }
    27. }
    28.  
     
  2. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    3,984
    You need to elaborate on this. Most likely you are forgetting to mark the NativeHashMap as ReadOnly using WithReadOnly() in the Entities.ForEach. You might also need to switch from string to FixedString.

    Depending on the length of your strings, it might be faster to just create a NativeArray of string/value pairs and sort it by string name, and then do binary search lookups.
     
  3. alec100_94

    alec100_94

    Joined:
    Jan 9, 2017
    Posts:
    26
    I have tried NativeHashmap<NativeString32, uint>, having it as a member variable of the system, have also tried static member variable. Have tried capturing it in my update loop, also tried marking the member with the C# readonly keyword. Tried it as a static member of the component also. If there is a standard convention for this then that is what I would like to adopt, in my mind it makes sense to have this as a part of the system as that is what reads it but as already explained I have been unable to get it to work like this (might just be confused by the syntax, or might be way off not entirely sure, but everything I try just gives a new error). Using WithReadOnly (shown below), it still gives an error (about lambda's not being able to use generics this time)
    Code (CSharp):
    1.    
    2. Entities
    3.         .WithReadOnly(monsterAttackDamageValues)
    4.         .ForEach((ref PlayerStatsDispatcher dispatcher, ref CharacterStats characterStats, in NativeHashMap<NativeString32, uint> monsterAttackDamageValues) => {
    5.             PlayerWasAttacked(ref dispatcher, ref characterStats,
    6. , in monsterAttackDamageValues
    7. );
    8.         }).ScheduleParallel();
    9.  
     
  4. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    3,984
    1) Make it a member variable of the system.
    2) In OnUpdate, assign it to a local var. Use the local var to refer to it everywhere else. (Assigning it to a local var makes it capturable without capturing the system context which is a class type.)
    3) Use WithReadOnly just like in your code snippet.
    4) Do not specify it as a lambda argument. Those are for components on entities.
    5) Reference the local var inside the lambda body.

    For future reference, it is better to show complete code and complete error rather than try to summarize it, especially if you can keep your system size down to under 100 lines.
     
    florianhanke likes this.
  5. alec100_94

    alec100_94

    Joined:
    Jan 9, 2017
    Posts:
    26
    That was the complete code at time of posting. I tend to discard code that doesn't work, so none of that was in my project at the time. Thanks for helping me with that anyway. So I got that bit working, but now I'm having another (somewhat related) issue which is that I don't actually seem to have the ability to do a basic string concatenation (add two strings together) in ECS, so I cannot actually retrieve my value from my hashmap. I'll just post the entire system here this time (though some of it is somewhat irrelevant) in case there is a glaring mistake with how I am trying to structure this.

    Code (CSharp):
    1.  
    2. public class StatsSystem : SystemBase
    3. {
    4.     NativeHashMap<NativeString32, uint> monsterAttackDamageValues;
    5.  
    6.     protected override void OnCreate()
    7.     {
    8.         monsterAttackDamageValues = new NativeHashMap<NativeString32, uint>(30, Allocator.Persistent);
    9.         monsterAttackDamageValues.Add("Skeleton_Attack1", 2);
    10.     }
    11.  
    12.     protected override void OnUpdate()
    13.     {
    14.         NativeHashMap<NativeString32, uint> monsterAttackDamageValues = this.monsterAttackDamageValues;
    15.         Entities.WithReadOnly(monsterAttackDamageValues)
    16.             .ForEach((ref PlayerStatsDispatcher dispatcher, ref CharacterStats characterStats) => {
    17.                 PlayerWasAttacked(ref dispatcher, ref characterStats, in monsterAttackDamageValues);
    18.             }).ScheduleParallel();
    19.  
    20.         Entities
    21.             .ForEach((ref MonsterStatsDispatcher dispatcher, ref MonsterStats monsterStats) => {
    22.                 MonsterDidAttack(ref dispatcher, ref monsterStats);
    23.                 MonsterWasAttacked(ref dispatcher, ref monsterStats);
    24.             }).ScheduleParallel();
    25.     }
    26.  
    27.     protected override void OnDestroy()
    28.     {
    29.         monsterAttackDamageValues.Dispose();
    30.     }
    31.  
    32.     static void PlayerWasAttacked(ref PlayerStatsDispatcher dispatcher, ref CharacterStats characterStats, in NativeHashMap<NativeString32, uint> monsterAttackDamageValues)
    33.     {
    34.         if(!(dispatcher.wasAttacked is PlayerStatsDispatcher.PlayerWasAttacked wasAttacked)) return;
    35.         MonsterStats monsterStats = wasAttacked.monster;
    36.         // Gives error, cannot add strings
    37.         if(monsterAttackDamageValues.TryGetValue(monsterStats.name + "_" + monsterStats.lastAttack, out uint attackDamage)) monsterStats.lastAttack = null;
    38.         else return;
    39.  
    40.         if(attackDamage == 0) return;
    41.         characterStats.hp.x -= characterStats.hp.x > 0 ? attackDamage : 0;
    42.         dispatcher.wasAttacked = null;
    43.     }
    44.  
    45.     static void MonsterDidAttack(ref MonsterStatsDispatcher dispatcher, ref MonsterStats monsterStats)
    46.     {
    47.         if(!(dispatcher.didAttack is MonsterStatsDispatcher.MonsterDidAttack didAttack)) return;
    48.         monsterStats.lastAttack = didAttack.attack;
    49.         dispatcher.didAttack = null;
    50.     }
    51.  
    52.     static void MonsterWasAttacked(ref MonsterStatsDispatcher dispatcher, ref MonsterStats monsterStats)
    53.     {
    54.         if(!(dispatcher.wasAttacked is MonsterStatsDispatcher.MonsterWasAttacked wasAttacked)) return;
    55.         WeaponStats weaponStats = wasAttacked.weapon;
    56.         uint attackDamage = (uint) (weaponStats.attack.x * 0.5f);
    57.         // TODO: Handle Strengths And Weaknesses Here
    58.  
    59.         if(attackDamage == 0) return;
    60.         monsterStats.hp.x -= monsterStats.hp.x > 0 ? attackDamage : 0;
    61.         dispatcher.wasAttacked = null;
    62.     }
    63.  
    64.     public static void Dispatch<T, TDispatcher>(T dispatch, Entity entity) where T : struct where TDispatcher : struct, IComponentData
    65.     {
    66.         var toDispatch = typeof(TDispatcher).GetFields().FirstOrDefault(x => x.FieldType.IsAssignableFrom(dispatch.GetType()));
    67.         if(entity == null || toDispatch == null) return;
    68.  
    69.         var entityManager = World.DefaultGameObjectInjectionWorld.EntityManager;
    70.         var dispatcher = (object) entityManager.GetComponentData<TDispatcher>(entity);
    71.         toDispatch.SetValue(dispatcher, dispatch);
    72.         entityManager.SetComponentData(entity, (TDispatcher)dispatcher);
    73.     }
    74. }
    75.  
     
    Last edited: Sep 20, 2020
  6. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    3,984
    1) Use FixedString, not NativeString. FixedString is the new and improved version of NativeString.
    2) Burst support with strings is still a little rough yet. If switching to FixedString does not outright resolve your issue (I don't use strings much for performance reasons), you might have better luck using string interpolation instead of operator concatenation. Worst comes to worst, you can use the Append method on the FixedString. It is more verbose, but it will work.
     
  7. alec100_94

    alec100_94

    Joined:
    Jan 9, 2017
    Posts:
    26
    Still cannot seem to get it to work, I've tried multiple variations of interpolation, concat, + operator, string.format, etc. I thought the append had a good chance of working but I keep getting multiple variations of the error below. I don't think performance is really a huge concern in this scenario as it's happening as a part of a messaging system rather than every frame (the string and lookup are used to keep the message friendly).

    Burst error BC1349: Invalid argument. Expecting a string literal or a string.Format or a fixed string.

    Code (CSharp):
    1.            
    2. if(!(dispatcher.wasAttacked is PlayerStatsDispatcher.PlayerWasAttacked wasAttacked)) return;
    3. MonsterStats monsterStats = wasAttacked.monster;
    4. FixedString32 attackLookup = monsterStats.name;
    5. attackLookup.Append(new FixedString32("_"));
    6. attackLookup.Append(monsterStats.lastAttack);
    7. if(monsterAttackDamageValues.TryGetValue(attackLookup, out uint attackDamage)) monsterStats.lastAttack = null;
    8. else return;
    9.  
     
  8. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    3,984
    Which line is that happening? You might have better luck just appending '_' (note the single quotes).
     
  9. alec100_94

    alec100_94

    Joined:
    Jan 9, 2017
    Posts:
    26
    The error is actually being thrown on the TryGetValue line itself, I tried changing it to [] based indexing and got the same error. I have tried countless combinations (of append, FixedString.Format, and more) at this point now and it doesn't seem to work. It still seems to give the error even if I just pass monsterStats.name to it (which obviously wouldn't find my value). All my strings are FixedString32, also tried updating them all to FixedString128 with the same result. My current version looks like this, and gives the same result as above.

    Code (CSharp):
    1.            
    2. if(!(dispatcher.wasAttacked is PlayerStatsDispatcher.PlayerWasAttacked wasAttacked)) return;
    3. MonsterStats monsterStats = wasAttacked.monster;
    4. FixedString32 attackLookup = $"{monsterStats.name}_{monsterStats.lastAttack}";
    5. if(monsterAttackDamageValues.TryGetValue(attackLookup, out uint attackDamage)) monsterStats.lastAttack = null;
    6.  else return;
    7.  
    8. if(attackDamage == 0) return;
    9. characterStats.hp.x -= characterStats.hp.x > 0 ? attackDamage : 0;
    10. dispatcher.wasAttacked = null;
    11.  
     
  10. alec100_94

    alec100_94

    Joined:
    Jan 9, 2017
    Posts:
    26
    Ah, I'm an idiot lol. Just kind of assumed the default value would be null, but it's a struct so it's not. Still not fully working as intended, but at least it's not giving errors now. Thanks for the help.
     
  11. alec100_94

    alec100_94

    Joined:
    Jan 9, 2017
    Posts:
    26
    This is the version of the code that actually worked. I think the FixedString.Format was essential too

    Code (CSharp):
    1.  
    2. if(!(dispatcher.wasAttacked is PlayerStatsDispatcher.PlayerWasAttacked wasAttacked)) return;
    3. MonsterStats monsterStats = wasAttacked.monster;
    4. FixedString32 attackLookup = FixedString.Format("{0}_{1}", monsterStats.name, monsterStats.lastAttack);
    5. if(monsterAttackDamageValues.TryGetValue(attackLookup, out uint attackDamage)) monsterStats.lastAttack = default(FixedString32);
    6. else return;
    7.  
    8. if(attackDamage == 0) return;
    9. characterStats.hp.x -= characterStats.hp.x > 0 ? attackDamage : 0;
    10. dispatcher.wasAttacked = null;
    11.