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

DOTS skill system repo available

Discussion in 'Entity Component System' started by WAYNGames, May 19, 2020.

  1. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    988
    Edit - repo available here https://github.com/WAYN-Games/MGM-Ability

    edit - Changed title to better reflect most of the content of the thread.

    Hi,

    I have an initializaition system that need to get data from an IComponentData class to add/set IComponentData struct based on its content before removing the IComponentData class from the entity.

    I can make the code run fine on the main thread my question is, I know I'm not working on conflicting entities (no race condition) and appart from the conversion system, it's the only system that write to these component, so how can I go about offloading the work to a job, even better if possible have it multithreaded ?

    When testing with 20K entities I get from 400 to 900 ms executions wich I'd like to reduce as much as possible, even if it's an intialization system that should only run once.

    I also managed to make this work with c# Tasks but I don't know about the compatibility of this method over the diferent platforms.

    If none of this works, I may also be able to spread the initialization over several frames by limiting the number of entities processed per frame so that it don't freeze the game.

    Any suggestions ?
     
    Last edited: Jun 11, 2021
    bb8_1 and nanobot_games like this.
  2. brunocoimbra

    brunocoimbra

    Joined:
    Sep 2, 2015
    Posts:
    679
  3. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    988
    does that imply I will be making one job per entity ?
    If so, I'm worried about the shob scheduling overhead miking it even less efficient...
     
  4. brunocoimbra

    brunocoimbra

    Joined:
    Sep 2, 2015
    Posts:
    679
    I don't see why it would imply that as it would work the same as it would with C# Tasks, but using the Job System instead.
     
  5. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    988
    Sorry I thought the method you proposed would allow me to multithread the process but it just allow to offlaod it to a background thread.

    I'm investigation manual chunk iteration with the help of GetComponentObjects to get the IComponentData class.

    For now it does not trigger any safetycheck but I don't do anything either in my job...

    If this work it would allow to process each chunk in parallel.

    If this work out I'll post a sample code in this thread.

    Also I need to figure aout a way to get a viable index for using in the concurrent ECB i'll need in hte IJob. :/
     
  6. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
    I would strongly advise against any of this. If you want performance. Then using struct IComponentData and use the builtin functionality to make it fast.

    Ultimately multithreading is only a small part of getting good performance. Memory layout & codegen is equally important.

    Class components exist for handling a couple of entities in the game where you are absolutely sure that performance is not important.

    I'd love to know what the specific scenario is that makes doing that impossible for to use struct IComponentData and friends?
     
    brunocoimbra likes this.
  7. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    988
    Hi @Joachim_Ante, thanks for your interest in my post,solving the root problem would be great but for now I just worked arround several issues to make sure that my goals was at least doable. (which they seem to be :D)

    I'll try to put something together to explain it as well a possible (hat's gona be a long post :)) and ping you again once it's done.
     
  8. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    988
    @Joachim_Ante ,here is the scenario in which I need the IComponentData Class. Basically it boils down to a compatibility issue with subscene and the use of hash code (that could be worked around).

    This is explained to the best of my languages ability and hope it makes sense (it's almost a draft documentation of the thing...)

    I’m not yet ready to call that a release so for know let’s consider it as an alpha preview .


    What is the goal ?

    Make a designer friendly skill system


    What is a skill ?

    The skill is an ability that produce effects on one or several entities. The effect can for example be from reducing a pool (HP/Mana/Bullet magazine), moving an entity (apply a push back, teleport,…), triggering a message.

    Skill have some properties in common (ex : a range, a cost, …) but each have a unique set of effects.

    Some skills can however share the same effects.

    The effect can be applied either to the target or « caster » of the skill.

    As I said an effect can be a wide range of actions. An the action it has on entities can depend on other factor to the entities itself (damage can be reduced by armor based on the damage type,…)


    How to convey such effect in DOTS?


    To covey the effect on the different entities we can use a flavor of an event system.

    We could for instance add a IComponentData to the affected entities where the IComponentData have a the necessary data to apply the effect in a system that iterate aver all entities with that component (and the ones needed for the effect to apply like armor).

    The drawback is that this implementation trigger a change in the archetype every time we want to apply an effect.


    To work around that we can implement a custom event system with native containers (native stream) and have a system that consume the stream to apply the effect to the necessary entities.


    This work great when we know the effect we want to apply when codding the actual game logic, but that’s not designer friendly.


    When thinking about it a skill effect should not change at runtime it’s defined by the disigner when building the game and don’t change once the game run (it’s just applied).

    So my idea is to make a registry based event system.


    What is a registry based event system ?


    Like the name should imply it’s an event system that uses a registry of possible events.

    The idea is to let the designer define the set of effect a skill should have through the editor with some custom editors. That way we can author the behavior with the handy abstractions of OOP while having a (hopefully) performant runtime.


    How does it work ?



    This process work in several phases :

    The authoring phase

    The authoring phase consist in defining the list of effects that a skill should have through the unity editor.

    The conversion phase

    The conversion phase add this list of effect in an IComponentData class to be consumed by the next phase.

    The initialization phase

    The initialization phase iterate over all the entities with the IComponentData class added at the conversion phase. This IComponentData class contains the list of effect (IEffect) to be added to the skill, it adds it to a registry (a singleton class containing a dictionary of dictionary). The registry returns a reference to the effect (the reference is composed of a hash of the effect type and a hash of the effect itself). The registration is unique so if I try to register 2 identical effect, there is only one instance in the registry and when trying to add the second one the registry returns the same reference.

    The effect reference is then added to a dynamic buffer of effect reference.

    Doing that allow the next phase to trigger effect without knowing what it actually is.

    The work done by the initialization phase cannot be done at convert time because it would not work for entities having skill in subscene (the registry is a simple singleton so it’s not persisted in the serialized subscene and the effect reference relies on the hash which would be different at runtime because not computed by the same process) That’s why I need the IComponentData Class.

    The production phase

    During this phase, any system that need to trigger effect grab a native queue from the dispatcher system and enqueue an effect command containing the emitter entity, the target entity and the reference of the effect.

    The dispatcher phase

    The dispatcher loop through the list of native queue requested by the producer systems and dispatch the effect in a native multi hashmap (command map) based on the effect type id (contained in the effect reference)


    The consumer Phase

    In the consumer phase a IJob per effect type gets the list of effect to apply from the dispatcher (command map). It take all the necessary component data for the application of the effect and apply it.

    That way, the effect can be handle in parallel (careful about the order so that 2 consumer don’t alter the same component).


    End notes

    For now I don’t have the skill system per say, it’s just the effect that I apply based on the collisions between entities. (each skill will probably be an entity that has a buffer of effect and that is triggered by some system) (skill themselves will likely be attached to a « skill bar » buffer of the player entity)


    The effect system POC work in my playground tank game. (implementation in this sample is « full » of bad code)

    Now I’m trying to extract the registry based event to a package with proper unit and performance tests, to improve both the « cleanness » of the code an performance.



    Tank demo playground :

    - The full source : : https://github.com/WAYN-Games/MGM

    - A showcase video :
    (perf is poor in editor while recording video… it’s much better in build)(note whe nI say all is trigerre by the event system, I mean the effect triggered on collision, not the mouvement of course)

    - A windows 64 build : https://we.tl/t-rS8HagOsjN
     
    Last edited: Jun 11, 2021
    bb8_1 likes this.
  9. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
    Fair enough. So to summarize. You want some sort of a state machine / event system. Right now you think that changing state via AddComponent / RemoveComponent is too expensive, because it moves archetypes.

    We have found a similar problem in Dots shooter. But it really depends on the frequency of the addcomponent / removecomponent calls, is a perf issue or not.

    That is why we are currently working on adding Enabled state to all components. This way you can enable / disable components from any job safely without changing the structure. Entities.ForEach will automatically do the filtering for you. It will process only the entities that match essentially a disabled component appears as a component that doesn't exist.

    Internally this is done via packed bitfields across multiple entities and SIMD bit mask tricks so it will be quite efficient.

    In Dots shooter our plan is then that during conversion you setup all possible components (Enabled or disabled) and at runtime you simply enable / disable components. Our game code team also prefers this approach since it will be clearer to have all possible components already present. So its easier to identify a specific type of object in the world...

    That sounds like it should solve your problem...

    So the question what do you do for now, until this feature lands.

    In DOTS Shooter we are simply pretending the functionality exists and are putting a bool Enabled; in each of the Ability System components. And then we do the filtering manually in Entities.ForEach in the first line of code.
    It's not optimal but it gets us to a place where we can structure our game code how we want it to be and once native support for enabled / disabled components lands we can simply remove all of the .Enabled bools and let ecs take care of the filtering for us.

    An alternative is to simply live with the cost of add/remove component even for high frequency changes for the time being, until we land native support for Enabled flags.
     
    Last edited: May 20, 2020
    bb8_1, MehO, lclemens and 31 others like this.
  10. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    988
    Thanks for the reply @Joachim_Ante .


    That's a cool concept.


    Will it also work with dynamic buffers (or are considered not there the empty ones maybe) ? if so the skill system could be something like this :


    The skill is an entity with an IComponentData for each of its effect.

    Target gets a DynamicBuffer of IBufferElement per effect type to act as a queue of effect to apply to the entity (this is to allow on target to receive several of the same effect in one tick/frame, most likely possible in a MMORPG ), when adding an element to the buffer we add/activate it.

    Then we have one system per DynamicBuffer type (run only on enabled ones) that loop through the buffer and apply the effect to the necessary component data on the entity, at the end, the buffer is cleared and disabled.


    The authoring is easy to manage with skill entities (prefab) (target get the buffer on the first occurrence of the effect and then keep it disable). this will most likely work well with live link.


    This would still use the ECB to alter the effect buffer content on the target entity in a concurrent way.


    Until the feature land (any ETA ?:p), I'll try it in a similar fashion that you mention in the DOTS sample to see if it works.
     
  11. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
    Agree. This approach works well with authoring & live link.

    I expect this to land somewhere between 3weeks to 3 months.
    It's unclear mostly because it just really changes a massive amount of assumptions in the internals of entities that it is hard to say exactly how long it takes to ensure all of them are handled robustly and as expected.
     
    WAYNGames likes this.
  12. Flipps

    Flipps

    Joined:
    Jul 30, 2019
    Posts:
    51
    Is this concept of "enable/disable" components also used to replicate the components attached to a ghost in netcode? So for example you can enable a "Buff" component on a ghost on the server and it gets automatically added to the ghost on the client?
     
  13. snacktime

    snacktime

    Joined:
    Apr 15, 2013
    Posts:
    3,356
    I've done a couple of effect systems in production and a few more just refining the approach.

    What you want is a concept of effects attach to other logical entities. And then you tick the effects. Some will just tick once, some get a lot more involved, the pattern holds up well over almost any genre and handles almost any type of once and over time logic you can dream up.

    Then you have what your affects do. This should be reduced down to a specific singular action, like add health/remove health, increase magic recovery, decrease magic recovery, etc.. An effect does one thing and one thing only. So effects are NOT player level abstractions. Skills, spells, whatever your higher level abstraction is, will have a list of effects that get created when they trigger.

    How you resolve effects is multiple steps. I start with an effect context abstraction. That gets created when you trigger an effect. It has the effect lifetime state info and the entity it's attached to at a minimum.

    On your effect tick interval you first iterate all of your effect context's and group them by effect type. Then those groups are handled by effect specific systems (not necessarily one system per effect type). You don't have to do this but it solves a bunch of issues in one go. It makes it easy to have effect specific systems, you created a nice linear list of data for each of them to operate on. Your effect ticking should be abstracted out from what your effects actually do. And you often find you want effects processed in order of the effect type, which in itself requires this type of transformation.

    We solve this outside of the context of Unity so I'm thinking off the top of my head what tools would work best in an ECS context. Effect types definitely not a component but an enum. You need to key off of that in a lot of context's that are more granular.

    For effect context's you could likely use entities just fine. Effects are generally not something you create so many of per frame that it's going to be an issue. Note that iterating and grouping every tick is important. The groups can potentially change every tick for reasons other then effect context's coming and going. So your effect context's need to persist. Ie you can't for example just use a NativeQueue for your effect context's.

    How exactly you group the entities to act on by effect type could go several ways. I would most likely use DynamicBuffer I think. An entity per effect type each with a DynamicBuffer holding the entities that effect should act on. Practically speaking effects most often modify similar things like stats. And you would generally have core stats grouped in one or two components at most. It's going to pan out like that for other stuff as well. In any case you never need a system or job per effect type you can consolidate that a great deal. Even if you had a lot of jobs each acting on one effect type it's easy to early out here. Your entity has an EffectType enum so jobs can early out on that.
     
  14. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
    Agree if you really just trying to model state. Using enums and just processing everything every frame is a perfectly reasonable approach. With burst you can blast through an array of ints so fast doing it for 10k ints is just only barely going to show up if you have the right early outs.
     
    bb8_1 and OldMage like this.
  15. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    988
    @snacktime I feel like what you describe is close to what I'm doing in my current implementation.



    My level of abstractions are Skill and effect.



    Yes each effect only affect one component data (hp/mp) or have a single action (pushback,...)


    That's the production phase where I create the effect command with the effect that should be applied and the context (emitter/target)


    That's the dispatcher


    That are the consumer that consume only one effect type.


    I did not get to implement the effect over time or effect over area but for effect over time I think I can span an entity with the timing and ticking logic. And for the effect over area I can either make a special effect that cast a collider and register the effect like any other producer or have each effect cast their own collider and act on all returned entities (probably less performant).


    Technically I'm outside of "unity" (or ECS at least) because I rely on native containers, and a registry, not on entities.


    Are you talking about creating an effect (authoring) or applying it to the target entity (produce/group/consume)


    The produce/group/consume all happen in the same tick through dependencies. (for now the consuming can overlap to the next frame but I can easily constrain it to the end of the simulation group.


    Have you any metrics on how many effect can be applied per tick ? I go with a wild estimation of a worst case scenario where in a MMO (like WOW) you have 1000 player all fighting the same both. If by some miracle all the player activate a skill at the same tick, I'll get 1000 effect (reduce hp) on the mob and likely 1 effect (reduce mp) per player. And that don't take into account the effect over area or other potential effects... in my system that lead to 2 consumer thread each iterating 1000 times. (I’d like to improve the dispatch logic to be able to handle entities in parallel)


    why would they need to persist ? once the command (context + effect) are consumed I don't need them. Or are you talking about the registry of all possible effects ?


    That's the dispatcher's job. And with one system er effect type, I don't run the job when there is no effect of the type to consume.



    All in all, I think that my implementation is conceptually not that bad. I just need to make sure it performs well for a realistic work load (not easy to determine what is realistic with my little experience so tyr to mai for mig unreasonable number :p).


    As for the approach I gave regarding the use of the enable/disable component feature, I don't think it will work. I still end up with some of the pitfall I thought about at the beginning.


    Using the skill is an entity with several effect component I author it easily but at coding, I have no idea how much and which effects a skill have. so I need to treat each effect separately. I could have a system that activate the skill component tag and then treat every effect that are linked to an active skill component tag, do the dispatching of the effect and force it to finish before a last system that disable the skill component tag.


    Now dispatching the effect to a buffer on the targeted entity, that seems great because I can still have one system per effect type (nice granularity) and I get to handle each entity in parallel safely because the effects are linked to only one entity for each buffer. The issue here is that it does not seem there is a way to write safely to a dynamic buffer in a concurrent manner. So I can't have several entities applying the same effect to the same target at the same time.
     
    bb8_1 likes this.
  16. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    988
    I'm now trying to go with the skill entity with each component data as an effect managed like I discribed in the previous post :
    As I said, the dynamic buffers for the target entiteis can't be writen to in a thread safe maner (as far as I know).

    So I'd like to write to a native container instead.

    Since each effect is produced only by one system, I can have a native stream per consumer taht is populated by each corresponding producer. That way there is no issue dispatching effect by type since it's directly done by the producer.
    The writing also happen in parallel (one write index per chunk).

    Then when the consumer is swhedule I use an entity for each statement taht match the component data that need to me modified (or taken into acount) by the consumption of the effect. The native stream is converted to a native array before the job so that in the job fore each potential targeted entity , I can iterrate over all effects and apply it when necessary (therest of the time I early out).

    From my understanding, this make the best used of th data layout, the consumer relies on Entities.ForEach, the producer uses IJobChunk (Entities.ForEach don't support generic) and the native stream is converted (copied) to an array so the memory loyout for that should be linear.

    I did a first test implementation and it seems to work. I'll try to perf test this and provide my result in this thread after that.

    Here is the implmentation I get for now :
    Code (CSharp):
    1.  
    2. using NUnit.Framework;
    3.  
    4. using Unity.Collections;
    5. using Unity.Entities;
    6. using Unity.Jobs;
    7.  
    8. namespace WaynGroup.MgmEvent.Tests
    9. {
    10.  
    11.     class EffectSystemTest : DotsTest
    12.     {
    13.         #region Core features
    14.  
    15.         #region Components
    16.         // Skill tag, Enabled will be replaced by the feature Joachim_Ante talked about
    17.         public struct Skill : IComponentData
    18.         {
    19.             public bool Enabled;
    20.         }
    21.  
    22.         public struct Target : IComponentData
    23.         {
    24.             public Entity Value;
    25.         }
    26.  
    27.         #endregion
    28.  
    29.         #region System
    30.         [DisableAutoCreation]
    31.         public class SkillDeactivationSystem : SystemBase
    32.         {
    33.             protected override void OnUpdate()
    34.             {
    35.                 Dependency = Entities.ForEach((ref Skill skill) =>
    36.                 {
    37.                     Skill s = skill;
    38.                     s.Enabled = false;
    39.                     skill = s;
    40.                 }).ScheduleParallel(Dependency);
    41.             }
    42.         }
    43.         #endregion
    44.  
    45.         #region Abstractions
    46.         public interface IEffect : IComponentData { }
    47.  
    48.         public struct ContextualizedEffect<EFFECT> where EFFECT : struct, IEffect
    49.         {
    50.             // Could add the emitter entity to get the needed data to apply the effect on the target
    51.             // Better to have a reference and get the data in the consumer than adding the to the context from the producer
    52.             // That way the producer reamin a simple derivation of the abstract system and the effect memory footprint is reduced
    53.             // It doe not cahgne the cost of getting the data from the emitter because the skill is a nentity in itself so there
    54.             // would be an indeirection in the producer too.
    55.             // public Entity Emitter;
    56.             public Entity Target;
    57.             public EFFECT Effect;
    58.         }
    59.  
    60.         [UpdateBefore(typeof(SkillDeactivationSystem))]
    61.         public abstract class EffectTriggerSystem<EFFECT, CONSUMER> : SystemBase where EFFECT : struct, IEffect
    62.     where CONSUMER : EffectConsumerSystem<EFFECT>
    63.         {
    64.             private EffectConsumerSystem<EFFECT> ConusmerSystem;
    65.             private EntityQuery Query;
    66.  
    67.             protected override void OnCreate()
    68.             {
    69.                 base.OnCreate();
    70.                 ConusmerSystem = World.GetOrCreateSystem<CONSUMER>();
    71.                 Query = GetEntityQuery(new EntityQueryDesc()
    72.                 {
    73.                     All = new ComponentType[]
    74.                     {
    75.                         ComponentType.ReadOnly<Skill>(),
    76.                         ComponentType.ReadOnly<Target>(),
    77.                         ComponentType.ReadOnly<EFFECT>()
    78.                     }
    79.                 });
    80.             }
    81.  
    82.             struct TriggerJob : IJobChunk
    83.             {
    84.                 [ReadOnly] public ArchetypeChunkComponentType<Skill> skillChunk;
    85.                 [ReadOnly] public ArchetypeChunkComponentType<Target> targetChunk;
    86.                 [ReadOnly] public ArchetypeChunkComponentType<EFFECT> effectChunk;
    87.                 public NativeStream.Writer ConsumerWriter;
    88.  
    89.                 public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
    90.                 {
    91.                     NativeArray<Skill> skills = chunk.GetNativeArray(skillChunk);
    92.                     NativeArray<Target> targets = chunk.GetNativeArray(targetChunk);
    93.                     NativeArray<EFFECT> effects = chunk.GetNativeArray(effectChunk);
    94.                     ConsumerWriter.BeginForEachIndex(chunkIndex);
    95.                     for (int i = 0; i < chunk.Count; i++)
    96.                     {
    97.                         if (!skills[i].Enabled) continue;
    98.                         ConsumerWriter.Write(new ContextualizedEffect<EFFECT>() { Target = targets[i].Value, Effect = effects[i] });
    99.                     }
    100.                     ConsumerWriter.EndForEachIndex();
    101.                 }
    102.             }
    103.  
    104.             protected override void OnUpdate()
    105.             {
    106.                 Dependency = new TriggerJob() // Entities.ForEach don't support generic
    107.                 {
    108.                     effectChunk = GetArchetypeChunkComponentType<EFFECT>(true),
    109.                     skillChunk = GetArchetypeChunkComponentType<Skill>(true),
    110.                     targetChunk = GetArchetypeChunkComponentType<Target>(true),
    111.                     ConsumerWriter = ConusmerSystem.GetConsumerWriter(Query.CalculateChunkCount())
    112.                 }.Schedule(Query, Dependency);
    113.             }
    114.         }
    115.  
    116.         [UpdateAfter(typeof(SkillDeactivationSystem))]
    117.         public abstract class EffectConsumerSystem<EFFECT> : SystemBase where EFFECT : struct, IEffect
    118.         {
    119.             protected NativeStream EffectStream;
    120.  
    121.  
    122.             protected void DisposeStream()
    123.             {
    124.                 if (EffectStream.IsCreated)
    125.                 {
    126.                     EffectStream.Dispose(Dependency);
    127.                 }
    128.             }
    129.  
    130.             public NativeStream.Writer GetConsumerWriter(int foreachCount)
    131.             {
    132.                 EffectStream = new NativeStream(foreachCount, Allocator.TempJob);
    133.                 return EffectStream.AsWriter();
    134.             }
    135.  
    136.             public NativeArray<ContextualizedEffect<EFFECT>> GetArray()
    137.             {
    138.                 NativeArray<ContextualizedEffect<EFFECT>> result = default;
    139.  
    140.                 if (EffectStream.IsCreated)
    141.                 {
    142.                     result = EffectStream.ToNativeArray<ContextualizedEffect<EFFECT>>(Allocator.TempJob);
    143.                     EffectStream.Dispose();
    144.                 }
    145.  
    146.                 return result;
    147.             }
    148.  
    149.             protected override void OnDestroy()
    150.             {
    151.                 base.OnDestroy();
    152.                 DisposeStream();
    153.             }
    154.         }
    155.  
    156.  
    157.         #endregion
    158.  
    159.  
    160.         #endregion
    161.  
    162.  
    163.         #region Game specific
    164.  
    165.         public struct Health : IComponentData
    166.         {
    167.             public int Value;
    168.         }
    169.  
    170.         #region Effect 1
    171.         public struct Effect1 : IEffect
    172.         {
    173.             public int Value;
    174.         }
    175.         [DisableAutoCreation] // I like to control everything in tests
    176.         public class Effect1TriggerSystem : EffectTriggerSystem<Effect1, Effect1ConsumerSystem>
    177.         {
    178.         }
    179.         [DisableAutoCreation]
    180.         public class Effect1ConsumerSystem : EffectConsumerSystem<Effect1>
    181.         {
    182.             protected override void OnUpdate()
    183.             {
    184.                 NativeArray<ContextualizedEffect<Effect1>> effectArray = GetArray();
    185.                 if (!effectArray.IsCreated || effectArray.Length == 0) return;
    186.                 Dependency = Entities.WithReadOnly(effectArray).ForEach((ref Entity entity, ref Health health) =>
    187.                 {
    188.                     for (int i = 0; i < effectArray.Length; i++)
    189.                     {
    190.                         if (!entity.Equals(effectArray[i].Target)) continue;
    191.  
    192.                         Effect1 effect = effectArray[i].Effect;
    193.                         Health hp = health;
    194.                         hp.Value -= effect.Value;
    195.                         health = hp;
    196.                     }
    197.                 }).ScheduleParallel(Dependency);
    198.  
    199.                 Dependency = effectArray.Dispose(Dependency);
    200.             }
    201.         }
    202.         #endregion
    203.  
    204.         #region Effect 2
    205.         public struct Effect2 : IEffect
    206.         {
    207.             public int Value;
    208.         }
    209.         [DisableAutoCreation]
    210.         public class Effect2TriggerSystem : EffectTriggerSystem<Effect2, Effect2ConsumerSystem>
    211.         {
    212.  
    213.         }
    214.         [DisableAutoCreation]
    215.         public class Effect2ConsumerSystem : EffectConsumerSystem<Effect2>
    216.         {
    217.             protected override void OnUpdate()
    218.             {
    219.                 NativeArray<ContextualizedEffect<Effect2>> effectArray = GetArray();
    220.                 if (!effectArray.IsCreated || effectArray.Length == 0) return;
    221.                 Dependency = Entities.WithReadOnly(effectArray).ForEach((ref Entity entity, ref Health health) =>
    222.                 {
    223.                     for (int i = 0; i < effectArray.Length; i++)
    224.                     {
    225.                         if (!entity.Equals(effectArray[i].Target)) continue;
    226.  
    227.                         Effect2 effect = effectArray[i].Effect;
    228.                         Health hp = health;
    229.                         hp.Value -= effect.Value;
    230.                         health = hp;
    231.                     }
    232.                 }).ScheduleParallel(Dependency);
    233.  
    234.                 Dependency = effectArray.Dispose(Dependency);
    235.             }
    236.         }
    237.  
    238.  
    239.         #endregion
    240.  
    241.         [DisableAutoCreation] // this system should actually be driven by user input.
    242.         public class SkillActivationSystem : SystemBase
    243.         {
    244.             protected override void OnUpdate()
    245.             {
    246.                 Dependency = Entities.ForEach((ref Skill skill) =>
    247.                 {
    248.                     Skill s = skill;
    249.                     s.Enabled = true;
    250.                     skill = s;
    251.                 }).ScheduleParallel(Dependency);
    252.             }
    253.         }
    254.  
    255.         #endregion
    256.  
    257.         [Test]
    258.         public void Test_Skill_System()
    259.         {
    260.             // Arrange
    261.             Entity target = _entityManager.CreateEntity();
    262.             _entityManager.AddComponentData(target, new Health() { Value = 100 });
    263.  
    264.             Entity entity = _entityManager.CreateEntity();
    265.             _entityManager.AddComponentData(entity, new Skill() { Enabled = false });
    266.             _entityManager.AddComponentData(entity, new Effect1() { Value = 1 });
    267.             _entityManager.AddComponentData(entity, new Effect2() { Value = 2 });
    268.             _entityManager.AddComponentData(entity, new Target() { Value = target });
    269.  
    270.  
    271.             Entity entity2 = _entityManager.CreateEntity();
    272.             _entityManager.AddComponentData(entity2, new Skill() { Enabled = false });
    273.             _entityManager.AddComponentData(entity2, new Effect1() { Value = 3 });
    274.             _entityManager.AddComponentData(entity2, new Target() { Value = target });
    275.  
    276.             Entity entity3 = _entityManager.CreateEntity();
    277.             _entityManager.AddComponentData(entity2, new Skill() { Enabled = false });
    278.             _entityManager.AddComponentData(entity2, new Effect2() { Value = 4 });
    279.             _entityManager.AddComponentData(entity2, new Target() { Value = target });
    280.  
    281.             _world
    282.                 .WithSystem<SkillActivationSystem>()
    283.                 .WithSystem<Effect1TriggerSystem>()
    284.                 .WithSystem<Effect2TriggerSystem>()
    285.                 .WithSystem<SkillDeactivationSystem>()
    286.                 .WithSystem<Effect1ConsumerSystem>()
    287.                 .WithSystem<Effect2ConsumerSystem>();
    288.  
    289.             // Act
    290.             _world.UpdateAndCompleteSystem<SkillActivationSystem>();
    291.  
    292.             // Assert
    293.             Assert.True(_entityManager.GetComponentData<Skill>(entity).Enabled);
    294.  
    295.             // Act
    296.             _world.UpdateAndCompleteSystem<Effect1TriggerSystem>();
    297.             _world.UpdateAndCompleteSystem<Effect2TriggerSystem>();
    298.             _world.UpdateAndCompleteSystem<SkillDeactivationSystem>();
    299.  
    300.             // Assert
    301.             Assert.False(_entityManager.GetComponentData<Skill>(entity).Enabled);
    302.             Assert.AreEqual(100, _entityManager.GetComponentData<Health>(target).Value);
    303.  
    304.             // Act
    305.             _world.UpdateAndCompleteSystem<Effect1ConsumerSystem>();
    306.             // Assert
    307.             Assert.AreEqual(96, _entityManager.GetComponentData<Health>(target).Value);
    308.             // Act
    309.             _world.UpdateAndCompleteSystem<Effect2ConsumerSystem>();
    310.             // Assert
    311.             Assert.AreEqual(90, _entityManager.GetComponentData<Health>(target).Value);
    312.  
    313.         }
    314.  
    315.     }
    316.  
    317.     public abstract class DotsTest
    318.     {
    319.  
    320.         protected TestWorld _world;
    321.         protected EntityManager _entityManager;
    322.  
    323.         [SetUp]
    324.         public void SetUp()
    325.         {
    326.             _world = new TestWorld();
    327.             _entityManager = _world.GetEntityManager();
    328.         }
    329.  
    330.  
    331.         [TearDown]
    332.         public void TearDown()
    333.         {
    334.             _world.CompleteAllSystems();
    335.             _world.Dispose();
    336.         }
    337.  
    338.     }
    339. }
    340.  
     
  17. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    988
    OK, here are the perf results and the updated implmentation :

    Edit 3 : the perf test code is full of mistake (sorry) the setup of the test is wrong I'm always targetting the same entity having many entities that don't actually contribute anything to the test.

    upload_2020-5-22_13-4-35.png

    Edit : Forgot the hardware spec
    I7-8650U 1,9HGz [8 Cores]
    RAM 16G
    GTX 1060



    Code (CSharp):
    1.  
    2. using NUnit.Framework;
    3.  
    4. using Unity.Burst;
    5. using Unity.Collections;
    6. using Unity.Entities;
    7. using Unity.Jobs;
    8. using Unity.PerformanceTesting;
    9.  
    10. using UnityEngine;
    11.  
    12. namespace WaynGroup.MgmEvent.Tests
    13. {
    14.  
    15.     class EffectSystemTest : DotsTest
    16.     {
    17.         private const bool TEST_PERF_ENABLED = true;
    18.  
    19.         #region Core features
    20.  
    21.         #region Components
    22.         // Skill tag, Enabled will be replaced by the feature Joachim_Ante talked about
    23.         public struct Skill : IComponentData
    24.         {
    25.             public bool Enabled;
    26.         }
    27.  
    28.         public struct Target : IComponentData
    29.         {
    30.             public Entity Value;
    31.         }
    32.  
    33.         #endregion
    34.  
    35.         #region System
    36.         [DisableAutoCreation]
    37.         public class SkillDeactivationSystem : SystemBase
    38.         {
    39.             protected override void OnUpdate()
    40.             {
    41.                 Dependency = Entities.ForEach((ref Skill skill) =>
    42.                 {
    43.                     Skill s = skill;
    44.                     s.Enabled = false;
    45.                     skill = s;
    46.                 }).WithBurst().ScheduleParallel(Dependency);
    47.             }
    48.         }
    49.         #endregion
    50.  
    51.         #region Abstractions
    52.         public interface IEffect : IComponentData { }
    53.  
    54.         public struct ContextualizedEffect<EFFECT> where EFFECT : struct, IEffect
    55.         {
    56.             // Could add the emitter entity to get the needed data to apply the effect on the target
    57.             // Better to have a reference and get the data in the consumer than adding the to the context from the producer
    58.             // That way the producer reamin a simple derivation of the abstract system and the effect memory footprint is reduced
    59.             // It doe not cahgne the cost of getting the data from the emitter because the skill is a nentity in itself so there
    60.             // would be an indeirection in the producer too.
    61.             // public Entity Emitter;
    62.             public Entity Target;
    63.             public EFFECT Effect;
    64.         }
    65.  
    66.         [UpdateBefore(typeof(SkillDeactivationSystem))]
    67.         public abstract class EffectTriggerSystem<EFFECT, CONSUMER> : SystemBase where EFFECT : struct, IEffect
    68.     where CONSUMER : EffectConsumerSystem
    69.         {
    70.             private EffectConsumerSystem ConusmerSystem;
    71.             private EntityQuery Query;
    72.  
    73.             protected override void OnCreate()
    74.             {
    75.                 base.OnCreate();
    76.                 ConusmerSystem = World.GetOrCreateSystem<CONSUMER>();
    77.                 Query = GetEntityQuery(new EntityQueryDesc()
    78.                 {
    79.                     All = new ComponentType[]
    80.                     {
    81.                         ComponentType.ReadOnly<Skill>(),
    82.                         ComponentType.ReadOnly<Target>(),
    83.                         ComponentType.ReadOnly<EFFECT>()
    84.                     }
    85.                 });
    86.             }
    87.  
    88.             [BurstCompile]
    89.             struct TriggerJob : IJobChunk
    90.             {
    91.                 [ReadOnly] public ArchetypeChunkComponentType<Skill> skillChunk;
    92.                 [ReadOnly] public ArchetypeChunkComponentType<Target> targetChunk;
    93.                 [ReadOnly] public ArchetypeChunkComponentType<EFFECT> effectChunk;
    94.                 public NativeStream.Writer ConsumerWriter;
    95.  
    96.                 public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
    97.                 {
    98.                     NativeArray<Skill> skills = chunk.GetNativeArray(skillChunk);
    99.                     NativeArray<Target> targets = chunk.GetNativeArray(targetChunk);
    100.                     NativeArray<EFFECT> effects = chunk.GetNativeArray(effectChunk);
    101.                     ConsumerWriter.BeginForEachIndex(chunkIndex);
    102.                     for (int i = 0; i < chunk.Count; i++)
    103.                     {
    104.                         if (!skills[i].Enabled) continue;
    105.                         ConsumerWriter.Write(new ContextualizedEffect<EFFECT>() { Target = targets[i].Value, Effect = effects[i] });
    106.                     }
    107.                     ConsumerWriter.EndForEachIndex();
    108.                 }
    109.             }
    110.  
    111.             protected override void OnUpdate()
    112.             {
    113.                 Dependency = new TriggerJob() // Entities.ForEach don't support generic
    114.                 {
    115.                     effectChunk = GetArchetypeChunkComponentType<EFFECT>(true),
    116.                     skillChunk = GetArchetypeChunkComponentType<Skill>(true),
    117.                     targetChunk = GetArchetypeChunkComponentType<Target>(true),
    118.                     ConsumerWriter = ConusmerSystem.GetConsumerWriter(Query.CalculateChunkCount())
    119.                 }.Schedule(Query, Dependency);
    120.                 ConusmerSystem.RegisterProducerDependency(Dependency);
    121.             }
    122.         }
    123.  
    124.         public abstract class EffectConsumerSystem : SystemBase
    125.         {
    126.             protected NativeStream EffectStream;
    127.  
    128.             private JobHandle ProducerJobHandle;
    129.  
    130.             protected void DisposeStream()
    131.             {
    132.                 if (EffectStream.IsCreated)
    133.                 {
    134.                     EffectStream.Dispose(Dependency);
    135.                 }
    136.             }
    137.  
    138.             public void RegisterProducerDependency(JobHandle jh)
    139.             {
    140.                 ProducerJobHandle = jh;
    141.             }
    142.  
    143.             public NativeStream.Writer GetConsumerWriter(int foreachCount)
    144.             {
    145.                 Debug.Log(foreachCount);
    146.                 EffectStream = new NativeStream(foreachCount, Allocator.TempJob);
    147.                 return EffectStream.AsWriter();
    148.             }
    149.  
    150.  
    151.             public NativeStream.Reader GetReader()
    152.             {
    153.                 return EffectStream.AsReader();
    154.             }
    155.  
    156.             protected override void OnDestroy()
    157.             {
    158.                 base.OnDestroy();
    159.                 DisposeStream();
    160.             }
    161.  
    162.             protected override void OnUpdate()
    163.             {
    164.                 Dependency = JobHandle.CombineDependencies(Dependency, ProducerJobHandle);
    165.             }
    166.         }
    167.  
    168.  
    169.         #endregion
    170.  
    171.  
    172.         #endregion
    173.  
    174.  
    175.         #region Game specific
    176.  
    177.         public struct Health : IComponentData
    178.         {
    179.             public int Value;
    180.         }
    181.  
    182.         #region Effect 1
    183.         public struct Effect1 : IEffect
    184.         {
    185.             public int Value;
    186.         }
    187.         [DisableAutoCreation]
    188.         [UpdateBefore(typeof(Effect1ConsumerSystem))]
    189.         [UpdateBefore(typeof(SkillDeactivationSystem))]
    190.         public class Effect1TriggerSystem : EffectTriggerSystem<Effect1, Effect1ConsumerSystem>
    191.         {
    192.         }
    193.         [DisableAutoCreation]
    194.         [UpdateAfter(typeof(Effect1TriggerSystem))]
    195.         public class Effect1ConsumerSystem : EffectConsumerSystem
    196.         {
    197.             protected override void OnUpdate()
    198.             {
    199.                 base.OnUpdate();
    200.                 NativeStream.Reader effectReader = GetReader();
    201.  
    202.                 Dependency = Entities.WithBurst().WithReadOnly(effectReader).ForEach((ref Entity entity, ref Health health) =>
    203.                 {
    204.                     if (effectReader.ComputeItemCount() == 0) return;
    205.                     for (int i = 0; i != effectReader.ForEachCount; i++)
    206.                     {
    207.                         effectReader.BeginForEachIndex(i);
    208.                         int rangeItemCount = effectReader.RemainingItemCount;
    209.                         for (int j = 0; j < rangeItemCount; ++j)
    210.                         {
    211.  
    212.                             Effect1 effect = effectReader.Read<ContextualizedEffect<Effect1>>().Effect;
    213.                             Health hp = health;
    214.                             hp.Value -= effect.Value;
    215.                             health = hp;
    216.                         }
    217.                         effectReader.EndForEachIndex();
    218.                     }
    219.  
    220.                 }).ScheduleParallel(Dependency);
    221.  
    222.                 DisposeStream();
    223.             }
    224.         }
    225.         #endregion
    226.  
    227.         #region Effect 2
    228.         public struct Effect2 : IEffect
    229.         {
    230.             public int Value;
    231.         }
    232.         [DisableAutoCreation]
    233.         [UpdateBefore(typeof(Effect2ConsumerSystem))]
    234.         [UpdateBefore(typeof(SkillDeactivationSystem))]
    235.         public class Effect2TriggerSystem : EffectTriggerSystem<Effect2, Effect2ConsumerSystem>
    236.         {
    237.  
    238.         }
    239.         [DisableAutoCreation]
    240.         [UpdateAfter(typeof(Effect2TriggerSystem))]
    241.         public class Effect2ConsumerSystem : EffectConsumerSystem
    242.         {
    243.             protected override void OnUpdate()
    244.             {
    245.                 base.OnUpdate();
    246.                 NativeStream.Reader effectReader = GetReader();
    247.                 Entities.WithBurst().WithReadOnly(effectReader).ForEach((ref Entity entity, ref Health health) =>
    248.                 {
    249.                     for (int i = 0; i != effectReader.ForEachCount; i++)
    250.                     {
    251.                         effectReader.BeginForEachIndex(i);
    252.                         int rangeItemCount = effectReader.RemainingItemCount;
    253.                         for (int j = 0; j < rangeItemCount; ++j)
    254.                         {
    255.  
    256.                             Effect2 effect = effectReader.Read<ContextualizedEffect<Effect2>>().Effect;
    257.                             Health hp = health;
    258.                             hp.Value -= effect.Value;
    259.                             health = hp;
    260.                         }
    261.                         effectReader.EndForEachIndex();
    262.                     }
    263.  
    264.                 }).ScheduleParallel();
    265.  
    266.                 DisposeStream();
    267.             }
    268.         }
    269.  
    270.  
    271.         #endregion
    272.  
    273.         [DisableAutoCreation] // this system should actually be driven by user input.
    274.         public class SkillActivationSystem : SystemBase
    275.         {
    276.             protected override void OnUpdate()
    277.             {
    278.                 Dependency = Entities.WithBurst().ForEach((ref Skill skill) =>
    279.                 {
    280.                     Skill s = skill;
    281.                     s.Enabled = true;
    282.                     skill = s;
    283.                 }).ScheduleParallel(Dependency);
    284.             }
    285.         }
    286.  
    287.         #endregion
    288.  
    289.         [DisableAutoCreation]
    290.         [UpdateAfter(typeof(Effect1ConsumerSystem))]
    291.         [UpdateAfter(typeof(Effect2ConsumerSystem))]
    292.         public class EndOfTestSystem : SystemBase
    293.         {
    294.             protected override void OnUpdate()
    295.             {
    296.                 Dependency.Complete();
    297.             }
    298.         }
    299.  
    300.  
    301.         [TestCase(1)]
    302.         [TestCase(10)]
    303.         [TestCase(100)]
    304.         [TestCase(1000)]
    305.         [TestCase(10000)]
    306.         [TestCase(100000)]
    307.         [TestCase(200000)]
    308.         [TestCase(300000)]
    309.         [TestCase(400000)]
    310.         [TestCase(500000)]
    311.         [TestCase(600000)]
    312.         [TestCase(700000)]
    313.         [TestCase(800000)]
    314.         [TestCase(900000)]
    315.         [TestCase(1000000)]
    316.         [Performance]
    317.         public void Test_Skill_System(int entityCount)
    318.         {
    319.             // Arrange
    320.  
    321.             Entity target = _entityManager.CreateEntity();
    322.             _entityManager.AddComponentData(target, new Health() { Value = 100 });
    323.  
    324.             Entity entity = _entityManager.CreateEntity();
    325.             _entityManager.AddComponentData(entity, new Skill() { Enabled = false });
    326.             _entityManager.AddComponentData(entity, new Effect1() { Value = 1 });
    327.             _entityManager.AddComponentData(entity, new Effect2() { Value = 2 });
    328.             _entityManager.AddComponentData(entity, new Target() { Value = target });
    329.  
    330.  
    331.             Entity entity2 = _entityManager.CreateEntity();
    332.             _entityManager.AddComponentData(entity2, new Skill() { Enabled = false });
    333.             _entityManager.AddComponentData(entity2, new Effect1() { Value = 3 });
    334.             _entityManager.AddComponentData(entity2, new Target() { Value = target });
    335.  
    336.             Entity entity3 = _entityManager.CreateEntity();
    337.             _entityManager.AddComponentData(entity2, new Skill() { Enabled = false });
    338.             _entityManager.AddComponentData(entity2, new Effect2() { Value = 4 });
    339.             _entityManager.AddComponentData(entity2, new Target() { Value = target });
    340.  
    341.             _world
    342.                 .WithSystem<SkillActivationSystem>()
    343.                 .WithSystem<Effect1TriggerSystem>()
    344.                 .WithSystem<Effect2TriggerSystem>()
    345.                 .WithSystem<SkillDeactivationSystem>()
    346.                 .WithSystem<Effect1ConsumerSystem>()
    347.                 .WithSystem<Effect2ConsumerSystem>()
    348.                 .WithSystem<EndOfTestSystem>();
    349.  
    350.  
    351.             // Act
    352.             _world.UpdateAndCompleteSystem<SkillActivationSystem>();
    353.  
    354.             // Assert
    355.             Assert.True(_entityManager.GetComponentData<Skill>(entity).Enabled);
    356.  
    357.             // Act
    358.             _world.UpdateSystem<Effect1TriggerSystem>();
    359.             _world.UpdateSystem<Effect2TriggerSystem>();
    360.             _world.UpdateAndCompleteSystem<SkillDeactivationSystem>();
    361.  
    362.             // Assert
    363.             Assert.False(_entityManager.GetComponentData<Skill>(entity).Enabled);
    364.             Assert.AreEqual(100, _entityManager.GetComponentData<Health>(target).Value);
    365.  
    366.             // Act
    367.             _world.UpdateAndCompleteSystem<Effect1ConsumerSystem>();
    368.             // Assert
    369.             Assert.AreEqual(96, _entityManager.GetComponentData<Health>(target).Value);
    370.             // Act
    371.             _world.UpdateAndCompleteSystem<Effect2ConsumerSystem>();
    372.             // Assert
    373.             Assert.AreEqual(90, _entityManager.GetComponentData<Health>(target).Value);
    374.  
    375.             /**************************************************
    376.              *
    377.              * PERF TEST
    378.              *
    379.              * **************************************************/
    380.             if (TEST_PERF_ENABLED)
    381.             {
    382.                 // Arrange
    383.                 NativeArray<Entity> targets = new NativeArray<Entity>(entityCount, Allocator.TempJob);
    384.                 for (int i = 0; i < entityCount; i++)
    385.                 {
    386.                     Entity tmp = _entityManager.CreateEntity();
    387.                     _entityManager.AddComponentData(target, new Health() { Value = 100 });
    388.                     targets[i] = target;
    389.                 }
    390.  
    391.                 for (int i = 0; i < entityCount - 1; i++)
    392.                 {
    393.                     Entity tmp = _entityManager.CreateEntity();
    394.                     _entityManager.AddComponentData(tmp, new Skill() { Enabled = false });
    395.                     _entityManager.AddComponentData(tmp, new Effect1() { Value = 1 });
    396.                     _entityManager.AddComponentData(tmp, new Effect2() { Value = 2 });
    397.                     _entityManager.AddComponentData(tmp, new Target() { Value = targets[i % 3] });
    398.  
    399.  
    400.                     Entity tmp2 = _entityManager.CreateEntity();
    401.                     _entityManager.AddComponentData(tmp2, new Skill() { Enabled = false });
    402.                     _entityManager.AddComponentData(tmp2, new Effect1() { Value = 3 });
    403.                     _entityManager.AddComponentData(tmp2, new Target() { Value = targets[i % 5] });
    404.  
    405.                     Entity tmp3 = _entityManager.CreateEntity();
    406.                     _entityManager.AddComponentData(tmp3, new Skill() { Enabled = false });
    407.                     _entityManager.AddComponentData(tmp3, new Effect2() { Value = 4 });
    408.                     _entityManager.AddComponentData(tmp3, new Target() { Value = targets[i % 7] });
    409.                 }
    410.  
    411.                 targets.Dispose();
    412.  
    413.                 Measure.Method(() =>
    414.                 {
    415.                     _world.UpdateSystem<SkillActivationSystem>();
    416.                     _world.UpdateSystem<Effect1TriggerSystem>();
    417.                     _world.UpdateSystem<Effect2TriggerSystem>();
    418.                     _world.UpdateSystem<SkillDeactivationSystem>();
    419.                     _world.UpdateSystem<Effect1ConsumerSystem>();
    420.                     _world.UpdateSystem<Effect2ConsumerSystem>();
    421.                     _world.CompleteAllSystems();
    422.                 }).Run();
    423.             }
    424.  
    425.  
    426.         }
    427.  
    428.     }
    429.  
    430.     public abstract class DotsTest
    431.     {
    432.  
    433.         protected TestWorld _world;
    434.         protected EntityManager _entityManager;
    435.  
    436.         [SetUp]
    437.         public void SetUp()
    438.         {
    439.             _world = new TestWorld();
    440.             _entityManager = _world.GetEntityManager();
    441.         }
    442.  
    443.  
    444.         [TearDown]
    445.         public void TearDown()
    446.         {
    447.             _world.CompleteAllSystems();
    448.             _world.Dispose();
    449.         }
    450.  
    451.     }
    452. }
    453.  
    I am both impressed and disapointed.
    It amaze me that this hold up well until 100 ~ 200 K entities but at the same time I'm disaponted that the base perf is still 2-3 ms, even for a single entity.

    Edit 2 : the perf entity count are false, I actually have 4 time that number of entity (1 target and 3 "attacker") with an average of 4 times the entity count of effect per target.

    If anyone sees a flaw in either the implementation or the perf test solgic please let me know.

    One other thing I don't get regarding dependancy between systems. I though that with system base, the automatic Dependency management and the UpdateBefore/After tag scheduling dependent job wetween system would be handle "automagically" but it does not seem to e the case.
    I still had to manually register the producer as a dependency to the consummer...

    Code (CSharp):
    1.        
    2. [...]
    3.  
    4.  protected override void OnUpdate()
    5.             {
    6.                 Dependency = JobHandle.CombineDependencies(Dependency, ProducerJobHandle);
    7.             }
    8.  
    9. [...]
    10.  
    11.       ConusmerSystem.RegisterProducerDependency(Dependency);
    12.  
    13. [...]
    14.  
    Idealy i'd like to hide the NativeStream.Reader logic to let the consumer only care about the actual effect logic. (custom job ??)
     
    Last edited: May 25, 2020
  18. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
    * Your trigger job is not using burst.

    * NativeArray<ContextualizedEffect<Effect2>> effectArray = GetArray(); seems to be doing work on the main thread can this be moved to a burst job before your entities foreach?

    * Lastly for very small entity count, using Entities.Run() on your jobs will be significantly faster. (You still want to burst all your code so that you have bursted code running on the main thread)

    * Debug.Log(foreachCount); Debug.Log is very expensive...

    * Using [DeallocateOnJobCompletion] in an already existing job is faster than Dispose on the array. Optimally you wouldn't be recreating containers every frame though. You have a system own a NativeList or array or whatever fits and reuse it. So it is only diposed on OnDestroy

    * Are you testing in standalone player? Perf in editor especially on base overhead is bigger than in standalone.

    Lastly we are aware that performance when running small entity count is not optimal, right now dots is very well optimized for the scale case but not so much the i have one entity case, scheduling job overhead etc that are being worked on.
     
    Last edited: May 24, 2020
    bb8_1 and _met44 like this.
  19. desertGhost_

    desertGhost_

    Joined:
    Apr 12, 2018
    Posts:
    259
    Hi,

    I have some feedback on this. There are cases where it is desirable to have a system run on the main thread based on some condition (if we are running as a server or if the entity count < n).

    Would it be possible to add new scheduling methods like
    ScheduleParallelWhen(bool condition)
    and
    ScheduleWhen(bool condition)
    that would schedule the code through the job system when the condition is met and run it on the main thread when it is not?
     
    brunocoimbra and Timboc like this.
  20. Timboc

    Timboc

    Joined:
    Jun 22, 2015
    Posts:
    238
    https://forum.unity.com/threads/request-scheduleauto-dep-chunkthreshold.870163/#post-5727157
     
  21. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    988
    @Joachim_Ante the previous perf test was full of mistakes (including some of the ones you pointed out) i'm currently testing with much better performance at lower entity count but it does not scale as much (still performs well under 1k target and 3k effect).

    I'll try to find a way to test with the run instead of schedule at lower entity count. I can obviously do it manualy but a method to do it dynamicly would be great.

    I also believe you or someone from unity mentioned that the scheduling was not yet burst compiled, maybe that would help?
     
  22. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    988
    Reworked a bit on the perf testing with some variants :

    Direct iterration on native stream in shceduled Foreach
    upload_2020-5-25_22-6-9.png

    Remap to a nativemultihashmap before consuming the envent un a scheduled Foreach


    upload_2020-5-25_22-5-57.png

    Remap to a nativemultihashmap before consuming the envent un a non scheduled Foreach (run)


    upload_2020-5-25_22-6-5.png



    The remapping definitly helps to make it hold longer.

    The Run instead of schedule seems to provide a 2 to 4 time perf boost up to 1K targeted entities ( with 3k effect each). That's good and up to a not so small entity number but I'm not sure it's an optimisation that can realisticly be applyed to a full game. if every ssytem is optimized that way we'll end up doing everything in the main thread...

    Anyway, the perf is satisfying to me so I'll try to package it a bit and move to the auhtoring part of things to see how it turn out to use that.

    For know the skill and effect are designed as a separate entity. Maybe it would be good to have it on the same entity as the player to avoid indirection when trigerring the skill and maybe be able to get the player data direclty in the trigerring job to populate the contextualized effect and void indirection when consuming it also (get the attack power of the player, or it's posisiton,....)



     
    nicolasgramlich likes this.
  23. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    988
    Skills are now authored through scriptable object and added to entities through a skill authoring component :

    upload_2020-5-29_11-12-29.png

    upload_2020-5-29_11-49-53.png

    During conversion, the skill are added to a skill buffer on the entity (each skill then has an index) and each effect is added to it's corresponding buffer type in the same entity (each with a reference to the skill index they correspond to.

    This provide the following advantages :
    1 ) a skill can now have several instance of the same effect type, this should allow for a skill to to provide drain like effect (restore caster's HP while dealing damage to target)
    2 ) If some data from the caster are necesary to process the effect, they can be retreived direclty from the chunk in the trigger effect job and added to the contextualized effect, this avoids any indirection on both the producer and consumer systems.

    The perfs with the test performance framework are stable with an even greater workload done :

    - 2 attacker per target
    - 2 skills per attacker
    - 5 effects of each type per skill
    - 2 types of effect each dealing 1 damage
    - 2 x 2 x 5 x 2 x 1 = 40 effect per entity per tick.

    upload_2020-5-29_11-47-12.png

    In play mode the perf are even better than in the performence test framework (not sure why.. maybe because I need to rely on reflection to force the completion of the systems)
    Here is a screen shot of the editor in play mode for 1K entity target (total for all systems of 0,37 ms in editor on a pick frame against 1,14ms in perf test)

    upload_2020-5-29_11-54-45.png
     
    Last edited: May 29, 2020
    Krajca and nico-yeet-studios like this.
  24. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    988
    Hi @Joachim_Ante
    I'm progressing well on this skill system, I got it rougthly package (see attachement).

    I was wondering if you had any pointers on how I could extend the IJobChunk (or implement a cutom job) to avoid most of the boiler plate code.

    The producers looks like that today :

    Code (CSharp):
    1.         [BurstCompile]
    2.         struct TriggerJob : IJobChunk
    3.         {
    4.             [ReadOnly] public ArchetypeChunkBufferType<SkillBuffer> skillBufferChunk;
    5.             [ReadOnly] public ArchetypeChunkComponentType<Target> targetChunk;
    6.             [ReadOnly] public ArchetypeChunkBufferType<BUFFER> effectBufferChunk;
    7.             public NativeStream.Writer ConsumerWriter;
    8.  
    9.             public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
    10.             {
    11.                 BufferAccessor<SkillBuffer> skillBufffers = chunk.GetBufferAccessor(skillBufferChunk);
    12.                 NativeArray<Target> targets = chunk.GetNativeArray(targetChunk);
    13.                 BufferAccessor<BUFFER> effectBuffers = chunk.GetBufferAccessor(effectBufferChunk);
    14.  
    15.                 ConsumerWriter.BeginForEachIndex(chunkIndex);
    16.                 for (int entityIndex = 0; entityIndex < chunk.Count; entityIndex++)
    17.                 {
    18.                     NativeArray<SkillBuffer> SkillBufferArray = skillBufffers[entityIndex].AsNativeArray();
    19.                     NativeArray<BUFFER> effectBufferArray = effectBuffers[entityIndex].AsNativeArray();
    20.                     for (int i = 0; i < SkillBufferArray.Length; i++)
    21.                     {
    22.                         Skill Skill = SkillBufferArray[i];
    23.                         if (!Skill.ShouldApplyEffects()) continue;
    24.                         for (int e = 0; e < effectBufferArray.Length; e++)
    25.                         {
    26.                             BUFFER EffectBuffer = effectBufferArray[i];
    27.                             if (EffectBuffer.SkillIndex != Skill.Index) continue;
    28.  
    29.  
    30.                             // Actual logic, the rest is boiler plate code...
    31.                             // That is where I would had to the context the data from the emiter that are needed by the effect, like power or position, ...
    32.                             ConsumerWriter.Write(new ContextualizedEffect<EFFECT>() { Target = targets[entityIndex].Value, Effect = EffectBuffer.Effect });
    33.  
    34.                         }
    35.  
    36.                     }
    37.  
    38.  
    39.                 }
    40.                 ConsumerWriter.EndForEachIndex();
    41.             }
    42.  
    43.  
    44.             protected override void OnUpdate()
    45.         {
    46.             Dependency = new TriggerJob() // Entities.ForEach don't support generic
    47.             {
    48.                 effectBufferChunk = GetArchetypeChunkBufferType<BUFFER>(true),
    49.                 skillBufferChunk = GetArchetypeChunkBufferType<SkillBuffer>(true),
    50.                 targetChunk = GetArchetypeChunkComponentType<Target>(true),
    51.                 ConsumerWriter = ConusmerSystem.GetConsumerWriter(Query.CalculateChunkCount())
    52.             }.ScheduleParallel(Query, Dependency);
    53.             ConusmerSystem.RegisterProducerDependency(Dependency);
    54.         }
    55.  

    And I'd like to have the user only define the actual contextual data for that specific effect (1 lineof code in this exemple), the rest (iterating through the buffer, defining the target(s) and checking the skill active status) would be done by the "internal" execution of the job.

    It would look something like :

    Code (CSharp):
    1.  
    2. struct TriggerJob : IJobChunkEffectTrigger
    3. {
    4.    // plus some user defined chunk component array for populating the context
    5.  
    6.    public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex,NativeStream.Writer consumerWriter,Entity target,EFFECT effect)
    7.    {
    8.       // plus populating the context from user defined chunk component array
    9.       // this will require one custom contextualized effect per effect type
    10.       consumerWriter.Write(new ContextualizedEffect<EFFECT>() { Target = target, Effect = effect });
    11.    }
    12. }

    I tryed copying the IJobChunk file in my project but some part use internals which themselves use interanls... so it don't seem I'll be able to just adapt that.

    If any one else has pointers on how to do what I need, please don't hesitate :D (@tertle, I know you did a custom job for your event system that involve NativeStream but I did not see it involve chunks, if you have any experience with that regard ;))
     

    Attached Files:

  25. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,753
    EntityManager.CompleteAllJobs()
     
  26. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,753
    Probably wouldn't be that hard to implement your own custom job for this. What particular issue did you run into trying to write your own job? It can look a bit complicated but it isn't too bad and you shouldn't really need anything internal.
     
    eizenhorn likes this.
  27. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    988
    Well, I admit I gave up quickly seeing that the IJobChunk was 280 line of code file using an internal utility class that itself does ~ 500 lines of code... which also use internals...

    I had the hope that there was a simpler way to extend the job chunk behavior.

    I want to keep all the chunk interation and filtering logic that exists in the IJobChunk, and add my additional behavior in the internal execute of the job : jobWrapper.JobData.Execute(chunk, chunkIndex, entityOffset);
     
  28. eizenhorn

    eizenhorn

    Joined:
    Oct 17, 2016
    Posts:
    2,683
    Just in addition to the source code of jobs which you can read (which is the main source of how to implement your own), also a small bit of information that can be useful for you
    https://docs.unity3d.com/Packages/com.unity.jobs@0.2/manual/custom_job_types.html As tertle said there is nothing so hard with custom jobs (and with native containers), just get it once and you'll start to make them often and it becomes routine for reusable code :)
     
  29. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
    1. struct TriggerJob : IJobChunkEffectTrigger

      Why not just implement it on top of IJobChunk using a generics?

      I wouldn't advice making a completely custom job type for this.
     
    WAYNGames likes this.
  30. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    988
    A generic job would mean that I just have the same behavior for all job (appart fro the generic parameter) but I want the user to be able to define what data he wants to put in the contextualized effect.

    Now I'm thinking of making a struct to act as an iterator. The user will just have to initialize the struct per chuck and declare a while(Next(out params)).
    The iteration, targets and skill activity check would be done in this next method in the struct so that the code is shared and not repeated for each system.
     
    Last edited: May 31, 2020
  31. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,753
    You can use the generic to give each skill a different effect

    Code (CSharp):
    1.         public interface ISkillImplementation
    2.         {
    3.             void DoSkillStuff();
    4.         }
    5.  
    6.         public struct TriggerJob<T> : IJobChunk
    7.                 where T : struct, ISkillImplementation
    8.         {
    9.             public T Implementation;
    10.  
    11.             public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
    12.             {
    13.                 // ...
    14.            
    15.                 this.Implementation.DoSkillStuff();
    16.            
    17.                 // ...
    18.             }
    19.         }
    Obviously can change the signature how you want, and load up the Implementation with whatever data you want before passing it to the job.
     
  32. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    988
    Yes but what about the parameters for the DoSkillStuff(); ? If for one implementation I need ComponentA and for another I need ComponentB which are totaly different and unrelated.
     
  33. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,753
    You store the components inside the struct.

    Code (CSharp):
    1.  
    2.             public interface ISkillImplementation
    3.             {
    4.                 void Execute(Entity entity);
    5.             }
    6.        
    7.             public struct SkillA : ISkillImplementation
    8.             {
    9.                 public ComponentDataFromEntity<Component1> Component1;
    10.            
    11.                 public void Execute(Entity entity)
    12.                 {
    13.                     var component1 = this.Component1[entity];
    14.                 }
    15.             }
    16.        
    17.             public struct SkillB : ISkillImplementation
    18.             {
    19.                 public ComponentDataFromEntity<Component2> Component2;
    20.                 public BufferFromEntity<Component2Buffer> Component2Buffer;
    21.            
    22.                 public void Execute(Entity entity)
    23.                 {
    24.                     var component2 = this.Component2[entity];
    25.                     var buffer = this.Component2Buffer[entity];
    26.                 }
    27.             }
    If you pass an entity in, you can pretty much do whatever you want.
    Or you can store a ArchetypeChunkComponentType and pass the ArchetypeChunk chunk in etc. There's really no limitations here.
     
    WAYNGames likes this.
  34. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    988
    Thanks @tertle and @Joachim_Ante, I get a much cleaner implementation for the declaration of each effect now :

    Code (CSharp):
    1.         [BurstCompile]
    2.         public struct TargetEffectWriter : IEffectContextWriter
    3.         {
    4.             [ReadOnly] public ArchetypeChunkComponentType<Target> targetChunk;
    5.  
    6.             public void WriteContextualizedEffect(ArchetypeChunk chunk, int entityIndex, ref NativeStream.Writer ConsumerWriter, EFFECT Effect)
    7.             {
    8.                 NativeArray<Target> targets = chunk.GetNativeArray(targetChunk);
    9.                 ConsumerWriter.Write(new ContextualizedEffect<EFFECT>() { Target = targets[entityIndex].Value, Effect = Effect });
    10.  
    11.             }
    12.         }
    I was concerned by the performance cost of doing the NativeArray<Target> targets = chunk.GetNativeArray(targetChunk); for every iteration but the perf test seems stable, so it's all good.
     
  35. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
    I would probably split the interface into two methods.:

    PrepareChunk & WriteContextualizedEffect

    Either you have two seperate structs, that your high level job passes, to keep the NativeArray's in. Or you keep it on the same struct and use [NativeDisableContainerSafetyRestriction] so you are allowed to keep it on the same struct without initializing it and schedule time (Because you fill them in during PrepareChunk only)

    The former is definately cleaner. But could be more boiler plate to always have two structs.
     
  36. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
    Can you share the full code you ended up with? I think it's an interesting use case that many could learn from.
     
    Krajca likes this.
  37. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    988
    Yes I made a package out of it.
    I'm cleaning up things and make the git hub repo public after that (May be later this week).

    One additional thing I'd like to make is a menu to generate a cs file containing all the necessary elements to declare a new effect (structures and trigger and consumer system). I already did somthing like that before but I could not get the same effect has unity get with the file name used as a class name... At least not without a popup before making the file.

    Edit : I think I found what I need in here : "\com.unity.entities@0.9.1-preview.15\Unity.Entities.Editor\ScriptTemplates\ScriptTemplates.cs" making use of " ProjectWindowUtil.CreateScriptAssetFromTemplateFile"
     
    Last edited: Jun 1, 2020
  38. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    988
    Last edited: Jun 7, 2020
  39. nicolasgramlich

    nicolasgramlich

    Joined:
    Sep 21, 2017
    Posts:
    231
    Fantastic work @WAYN_Group !! :)

    I combined your work with some existing work from my game (see signature) into a little demo:



    About to add some particle system "effects", projectiles (ranged attacks are currently "instant") to bring it up to par with my old simple attack system. Then the real fun begins :)

    Hopefully I can also have "animation effect" trigger animations with the hopefully soon official URP compatible Dots animation package :)
     
    Last edited: Jun 8, 2020
    lclemens and WAYNGames like this.
  40. exiguous

    exiguous

    Joined:
    Nov 21, 2010
    Posts:
    1,749
    But unfortunately it lacks some license information. I apreciate that you publish your code. But without proper license information it is almost unusable. A license states what is and what is not allowed so potential users of your code can better decide wether they can use it for their purpose or not. So please consider adding a proper licensing information.
     
    MNNoxMortem likes this.
  41. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    988
    MIT License Added.
     
  42. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    988
    Yes, much better to have some visual representation :) .
    Adding sounds, vfx and animation trigger would be nice but I think it will still require some hybrid stuff as far as my knowledge goes.
    For this kind of thing I think the effects will need to be updated to allow the m to be trigerred at different phase of the skill (start casting, activate, ...).

    PS : Thanks for the issue report and PR on the repo :)
     
    nicolasgramlich likes this.
  43. nicolasgramlich

    nicolasgramlich

    Joined:
    Sep 21, 2017
    Posts:
    231
    I don't see a reason why most of these couldn't be handled with "effects". There could be some "cast" and "castPoint" (cast actually triggers) effects.

    Btw, semi related but probably interesting still, I stumbled over this repo, where someone extracted the Dota2 unit definitions:
    https://github.com/SteamDatabase/Ga.../dota/scripts/npc/npc_units.txt#L15918-L15992
    and ability definitions:
    https://raw.githubusercontent.com/S...aster/game/dota/scripts/npc/npc_abilities.txt

    certainly wouldn't hurt to get some inspiration from how this wildly popular game handled it.

    Sample for this "mana break passive attack ability", notice the 6 different effects. Definitely on the right track here! :)
     
    lclemens likes this.
  44. nicolasgramlich

    nicolasgramlich

    Joined:
    Sep 21, 2017
    Posts:
    231
    Some more progress. Added a "ProjectileEffect", that launches a ballistic projectile:



    Next up is to have the effects apply "via" the projectiles, because right now they apply instantaneously, right when the projectile is launched. I think this can be achieved by just copying the ability effects from the ability to the projectile and have a CollisionSystem trigger the effects o_O
     
  45. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    988
    Hi,


    Nice work and thanks for the DOTA files.


    For the projectile to apply the effect, don't forget that the effect in the buffer are linked to a skill index, so you should probably add a skill buffer too. You will also need a target. Right now, I would make the projectile a prefab with it's own skill and effects, the target component and a projectile tag component then on collision, cast the skill and set the target component.

    This would also allow to have the “gun” skill to load ammunitions with different effects and decouple the “gun” skill animation, vfx, sfx from the projectile vfx and sfx (not much animation on a projectile :p).


    Plus relying on the collision to apply the effect implies that if it does not hit, it won't apply.

    That may be something you want (like in a FPS) but in case of a RPG the damage should apply even if the vfx is not perfect.


    I did not put much though into it yet but like I said, I would probably try to make some kind of signal to tell when to apply each effect (OnStartCasting, OnCast, OnVfxFinish, OnProjectileCollide,...) The skill target will probably need to be different from that actual current player target for it not to change between signals. (this would also provide decoupling between the skill system and the actual targeting/combat logic).


    For now I'll focus on basic functionality like range constraint (well in progress), resource constraint (mana, ammo, ...).

    Then I'll move to over time and area effect.

    Then I'll see what I can do for VFX, SFX and animations (may be dots will be a bit more mature in those aspect at that point).
     
  46. Ashkan_gc

    Ashkan_gc

    Joined:
    Aug 12, 2009
    Posts:
    1,117
    @Joachim_Ante Are you adding enabled to all components or we will be paying for the small but still available bitmasks in all of our code?

    Wouldn't asking users to do the enum/bool field if the frequency of events is high better because most components/systems don't need this or at least many won't? That is only if all now have the enabled bit flag
     
  47. nicolasgramlich

    nicolasgramlich

    Joined:
    Sep 21, 2017
    Posts:
    231
    Yup, that's exactly the idea that the projectile inherits all the effects (or even owned them in the first place).

    My focus was on realism (as in projectiles can miss), but I can totally see a "guaranteed hit" or "autoseeking" projectile to make sense even for my game. I'll try to noodle that in.

    Say for an RTS-ish game (which I'm focusing on), I think it makes sense for every ability to be able to have it's own target. Say a priest can heal nearby enemies, but will throw holy water on the enemy demons to cause some damage.
     
  48. nicolasgramlich

    nicolasgramlich

    Joined:
    Sep 21, 2017
    Posts:
    231
    Another update for Projectile Impacts and Visual Effects (any prefab to spawn at caster or target location. This can happen on cast or on impact.)

    Some context:
    The red "archers" fire exploding arrows that trigger a (totally hi-fi) explosion.
    The middle one of the green "archer" has a second "Heal" ability, which target units on the same team and gives them back some health every couple seconds (notice the little hearts at 0:08).


    How it was done:

    To keep everything in one place, the EntityQuery of the ProducerSystem was changed to have an
    Any
    piece for
    Projectile
    "OR"
    Ability
    . Then in the
    IJobChunk.Execute
    it's as simple as

    Code (CSharp):
    1.  if (chunk.Has(AbilityBufferElementChunk)) {
    2.    // This is a Ability Chunk, so do Ability stuff...
    3. } else {
    4.    // This is a Projectile Chunk, so do Projectile stuff...
    5. }
    The plan for AoE is to add another Component into this exact spot, where AoE has a
    TargetBufferElement : IBufferElementData
    for everything that's in radius. Those targets be set from physicscollider cast or by simple radius check, not sure yet what will perform better.
     
    WAYNGames likes this.
  49. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    988
    Nice, did you use the copy effect approche you proposed in our PM or the prefab with own skill/ability I prefered ?

    For AOE, like I said in PM, I don't think this approach will be suitable, the AOE part is not IMO at the skill/ability level but at the effect level. An ability can have a Damage AOE effect but could still have a single target (on caster) ressource cost (mana / ammo / ...)

    Ability seem to be a more generic name than Skill maybe I'll rename it later. What do you guys think ?
     
  50. nicolasgramlich

    nicolasgramlich

    Joined:
    Sep 21, 2017
    Posts:
    231
    Effects Over Time are done, weeeeee :D

    I made them a separate entity, that ticks (similar to Ability) and has the EffectBuffer of where it originated from. Basically the effects now look like this:

    Code (CSharp):
    1. public struct HealthEffect : IEffect {
    2.    public int Value;
    3.    public byte TickCount;
    4.    public float TickTime;
    5. }
    The new
    EffectOverTime
    looks like this and basically inherits the Target (on trigger of the effect) as well as the authored TickCount and TickTiming.
    Code (CSharp):
    1. public struct EffectOverTime : IComponentData {
    2.    public Entity Target;
    3.    public byte TickCount;
    4.    public Timing TickTiming;
    5. }
    on the same
    Entity
    that this
    EffectOverTime
    component is on there are is one
    EffectBuffer
    of the same time. (I can't really share the Entity, and have multiple effectbuffers of different types on it, because they're generated in the respective ConsumerSystem that doesn't know anything about the other effect types).
    The trick is to fill the
    EffectBuffer
    of the
    EffectOverTime
    with
    TickCount = 0
    , so that you don't run into an exponentially growing loop where an EffectOverTime triggers an EffectOverTime triggers an EffectOverTime triggers... :D

    In the ProducerSystem the triggering the DoT is a simple block as described in the previous post:

    Code (CSharp):
    1. } else if (chunk.Has(EffectOverTimeChunk)) {
    2.    var effectsOverTime = chunk.GetNativeArray(EffectOverTimeChunk);
    3.    for (var entityIndex = 0; entityIndex < chunk.Count; entityIndex++) {
    4.       var effectOverTime = effectsOverTime[entityIndex];
    5.       if (effectOverTime.TickTiming.IsElapsed()) {
    6.          var effectBufferArray = effectBuffers[entityIndex].AsNativeArray();
    7.          for (var effectBufferIndex = 0; effectBufferIndex < effectBufferArray.Length; effectBufferIndex++) {
    8.             var effectBuffer = effectBufferArray[effectBufferIndex];
    9.             var effect = effectBuffer.Value;
    10.             EffectContextWriter.WriteEffectContext(preparedChunk, entityIndex, ref ConsumerWriter, effect, effectOverTime.Target);
    11.          }
    12.       }
    13.    }
    14. }

    Here is a video of how it all works together:

    Notes:
    • - Red team
      • Lower/Left corner: Fireball projectile with Damage Over Time on Impact
      • Upper/Right corner: Arrow projectile with simple Damage on Impact
    • - Green team:
      • Front: Melee attack
      • Back: Heal over time (see little +++ effect), Explosion (Immediate aka no projectile) with Damage Over Time



    The combinations feel already super powerful, even without the AoE :D:)

    @WAYN_Group good point on the AoE. I'll report back once I tackled it :)