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 Learning DOTS with a Spell System Example (ECS 1.0.11)

Discussion in 'Entity Component System' started by Avangie, Aug 10, 2023.

  1. Avangie

    Avangie

    Joined:
    Aug 10, 2023
    Posts:
    5
    I'm working on a project with Unity DOTS; and I've reached my first pitfall in understanding how to structure a particular system in a way which works well with ECS.

    Where should I store state/data of the statistics of spells (or any short-lived entity) in a scalable manner? Say we have the following setup for a spell casting system.
    ---

    CasterComponent

    Stores the currently selected spell entity prefab (which has components other systems work on).

    CommandComponent
    Stores a boolean on if a command/input has been sent to cast a spell.

    Spell Casting System
    1. Loops over Entities with CasterComponent and CommandComponent.
    2. If CommandComponent.Value is true; than continue.
    3. Cast the CasterComponent.Value spell (i.e. instantiate the spell entity prefab).
    ---
    Now lets say we want to introduce the concept of mana. Easy done we just add a ManaComponent to the player and update our system to look for entities with a ManaComponent. However when it comes to the system checking if the Player's ManaComponent is high enough to cast the spell, where do we get said value for the cost of the spell? If we attached a cost component to the spell entity prefab we would not be able to query that value as a constraint, before its even instantiated. Nor does it make too much sense to me for the spell entity to even know what its cost is after its been spawned, as its simply there to resolve its effects.

    It would be trivial to attach the cost component to the player and resolve it there, however on scaling this problem up to say a library of 20, 50 or 100 spells, it doesn't feel like a correct solution to have a XSpellCostComponent for each XSpell attached to the player.

    Next I thought that if cost is immutable there are several obvious solutions, however if in addition to mana, each spell also has a boolean to indicate if its unlocked, or a double for a cooldown. Clearly these last two are states which need to be modified over the lifetime of play, so where should they be stored in a way which is; tracked for each individual spell, tied the particular caster, mutable, and scalable?

    My latest attempt was to create entities which store components for mana cost, enabled, cooldown, etc. Then we query these entities to validate spell casting to spawn a spell entity prefab. Again however this doesn't seem correct, as we have to create these entities in initialization for each spell and each caster, to ensure casters don't share cooldowns, etc on each spell.

    Perhaps there is a very trivial solution to this problem, and my OOP brain is just struggling to understand this correctly >.< however I've been unable to find a lot of helpful information when researching.

    Thank you in advance ^-^
     
  2. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    3,292
    Store data for the spell and actual spell entity separately from each other.

    Break it down like a database table and work it from there. Megastructs are always an option.
    You don't need to store them in chunk or as an entity. Any lookup will suffice by spell id or otherwise, e.g. as NativeHashMap<id, SpellData> + system singleton, blob, or DynamicHashMap.

    E.g.
    struct SpellData
    -> Id (uint, FixedString32Bytes or anything you prefer) // Could be kept as a key inside the lookup instead
    -> Mana Cost
    -> Cooldown
    -> Spell Type
    -> Spell Entity ("behaviour") to instantiate
    ... etc

    Upon "cast attempt" grab data from the lookup, perform required [generic] checks, do whatever is required.
    Store result of the test inside some temp data e.g. "SpellCastAttempt". Then in separate job handle spell creation etc.

    If its a random access problem it usually stays random access. You can work around it by "mirroring" or duplicating data, but its usually not worth doing unless you're hitting specific bottleneck by spefic jobs.

    From application logic perspective - cast attempts are rare occasion.
    Even if counts are 100+ per frame its usually less of an issue than rest of the logic.
     
    Last edited: Aug 10, 2023
    Avangie likes this.
  3. Avangie

    Avangie

    Joined:
    Aug 10, 2023
    Posts:
    5
    Hey thanks for the quick response! Will have a look at the different options you mentioned, and see what I can come up with tomorrow! ^-^
     
  4. msfredb7

    msfredb7

    Joined:
    Nov 1, 2012
    Posts:
    143
    I recommend a structure like this:
    Code (CSharp):
    1. Character entity:
    2.     Entity[] SpellSlots;
    3.  
    4. SpellSlot entity:
    5.     Entity SpellPrefab;
    6.     float RemainingCooldown;
    7.     float CurrentChannelTime;
    8.     ... etc. any other meta data
    You can put ManaCost and BaseCooldown values on the spell prefab. If you want to keep spell info separate from spell projectile, you can make that 2 entities.
     
    Avangie likes this.
  5. Opeth001

    Opeth001

    Joined:
    Jan 28, 2017
    Posts:
    1,068
    Have you checked out Unreal Engine's documentation for the Gameplay Ability System?
    I personally find it super helpful while implementing my DOTS Ability System package.
    The cool thing is, it handles anything gameplay related in a dynamic way.
    GameplayAttributes: These are numerical values that represents health, speed, and armor...
    GameplayTags: These are like yes/no Tags for things like stunned or dead.

    The good part is, you don't need separate systems for each thing. One system can dynamically handle everything. I implemented a custom `HashMap' using an ElementBuffer to keep track of attributes and a BitMask for the Tags.

    Unreal Engine has its own way of doing this using C++ and OOP. You can customize it to fit your needs, like replicating specific attributes or tags as Components. This makes things efficient, so systems only deal with data they need instead of accessing a full AttributeSet and making disasters ( cache misses .... )
     
    Avangie likes this.
  6. Avangie

    Avangie

    Joined:
    Aug 10, 2023
    Posts:
    5
    This is the solution I've currently implemented. It seems to be functioning well without any drawbacks, however as usual I am curious if any potential issues can be spotted with this approach. I have a CasterAuthoring which bakes the following components to an entity:

    LibraryReferenceComponent.cs
    Code (CSharp):
    1. public struct LibraryReferenceComponent : IComponentData
    2. {
    3.     public Entity Value;
    4. }
    PreparedSpellIDComponent.cs
    Code (CSharp):
    1. public struct PreparedSpellIDComponent : IComponentData
    2. {
    3.     public int Value;
    4. }
    PreparedSpellLookupComponent.cs
    Code (CSharp):
    1. public struct ReadiedSpellComponent : IComponentData, IEnableableComponent
    2. {
    3.     public SpellData Value;
    4. }
    The LibraryReferenceComponent entity value is set in baking from a converted GameObject reference to separate authoring script called SpellLibraryAuthoring. This other authoring bakes a DynamicHashMap* which maps int values to the following struct:

    SpellData.cs
    Code (CSharp):
    1. public struct SpellData
    2. {
    3.     public FixedString32Bytes name;
    4.     public int cost;
    5.     public EntityPrefabReference spell;
    6. }
    *You could replace the DynamicHashMap with a NativeHashMap, blob asset, or two lists linked by index. I however wanted the benefits of a HashMap, and more importantly the ability dynamically bake values from a ScriptableObject that held a list of (Int, SpellData) tuples in a managed format. This allows me to create an .asset file which contains a complete mapping of keys to spell information

    Systems
    • An unrelated system which sets the appropriate spell id value for PreparedSpellIDComponent.
    • A system which checks for a "spell cast attempt" (i.e. input), and if detected uses a BufferLookup<IDynamicHashMap> with the library entity as the index, and the spell id int as the key to the hashmap, with the lookup's result being copied to the PreparedSpellLookupComponent value. The latter component is then set to enabled through its IEnableableComponent interface.
    • Lastly, a system which queries entities with the PreparedSpellLookupComponent (note this won't include any entities with the component disabled), and just instances an entity from the prefab reference through a EntityCommandBuffer.
    ---
    Following above, any additional systems can be potentially defined to check conditions on the "spell cast attempt", and disable the PreparedSpellLookupComponent if the requisite conditions aren't met. The only requirement is that they are run before the spell casting system (the last system above). I have three such systems in my project;
    1. Disables spell casting while entities are in a particular area in the game world.
    2. Looks over entities with a cooldown buffer (another DynamicHashMap) and checks if enough time has passed since the last cast.
    3. Checks entity has mana component, and disables the lookup they don't have enough mana.
    I feel this design is very modular and allows for a range of entities such as those with/without mana or cooldown components, to be easily and dynamically defined. Another advantage I see is allowing you to just add a PreparedSpellLookupComponent to any entity and have a spell resolve automatically without any dependence on the caster system.

    Thank you heaps for the help, and references to documentation! ^-^
     
    xVergilx likes this.
  7. Avangie

    Avangie

    Joined:
    Aug 10, 2023
    Posts:
    5
    Yes I've watched the talk on it from the Unreal team, and spent a reasonable amount of time pouring through other forum posts (a lot of them involving you) discussing the the Gameplay Ability System. I definitely agree there is a lot of be learnt from it, and while it inspired my separating of gameplay state from attributes, there is still a fair bit of converting the workflow from Unreal to Unity ECS that I'm not 100% sure on. I'm curious how well my above solution post encapsulates the GAS way of doing things as you're definitely a lot better educated on the approach than I am.
     
  8. Opeth001

    Opeth001

    Joined:
    Jan 28, 2017
    Posts:
    1,068
    Certainly, reading Unreal's Gameplay Ability System documentation can provide helpful insights. However, considering the technologies they used, it serves more as a useful starting point. If you're using Unity, specifically DOTS, it's necessary to reevaluate the entire concept to make it suitable for the ECS implementation.

    Your approach appears effective and adequate for many projects. In our team, we've chosen a different direction. Our goal is to ensure high dynamism, allowing us to construct nearly any ability with minimal adjustments,and without requiring code modifications. We've practically examined various abilities, specially the complex ones, from games like League of Legends and Fortnite and plan their implementations by your package to make sure they are workable.

    If you treat elements like Mana or CoolDown as separate components rather than attributes, it becomes difficult for designers to modify their values without developer intervention for custom code creation. For instance, consider an ability that reduces its cooldown by 10% upon eliminating the target enemy. Achieving this without specific coding can be challenging. However, when treating them as simple attributes, they can be adjusted using GameplayEffects, just like any other attribute.

    In our approach, we interpret anything numerical as attributes across the board, with no exceptions. This not only provides immense flexibility for designers but also ensures high performance since ECS revolves around dynamic data processing. This involves grouping similar behaviors into one system, regardless of their context.

    In our implementation, we have two entities representing an ability:

    1. A shared Entity that contains the ability's definition and behavior stored on a shared BlobAsset.
    2. An Entity known as 'AbilityInstance', which houses the runtime data for the ability including attributes, tags, ownership, and such.
    This approach allows us to address scenarios involving persistent per-ability data. For example, consider a super powerfull ability like a bazooka with a high cooldown. If a player drops it and another picks it up, if the bazooka's data is tied to the Actor, the cooldown resets each time it's taken by a new actor. This is an issue even present in Unreal's GAS, requiring complex and Antipattern solutions. However, with our ECS approach, it feels more natural and doesn't necessitate custom solutions.
     
    Avangie likes this.
  9. Avangie

    Avangie

    Joined:
    Aug 10, 2023
    Posts:
    5
    In a sense this isn't too dissimilar to my approach would you say (If you take spell = ability)? In the sense that I have a shared entity I called a "spell library" with definitions. Other entities have a reference to this library and systems access that reference to load the definitions.

    The spell cast system uses a loaded definition to create a prefab instance entity I call a "Spell Instance". Likewise to your second entity they also handle all of their lifetime data, and have a ownership reference. Take the example of a heal spell which contains a HealOwnerComponent. A system operates on that and heals the owner for the component value.

    The persistent per-ability data does give me something to think about. As mentioned in my previous post my cooldown are stored as separate components to the caster, and not the spell instance (as attributes as you put it). While I don't have to deal with the concept of "dropping" spells, I do have to deal with spell ids changing which spell data they reference (which is adjacent to the use case you mention), and as such I could benefit from your approach.

    My initial thoughts raised some issue, as I had imagined spells to exist as a single "use" effect, rather than potentially as a persistent entity, but on further thought I can absolutely think of scenarios where I may want to implement spells that have cooldowns between multiple uses.

    Definitely gives me some thought to widen my conceptualization of a "spell instance", and some thought of how I could potentially shift all instance data to the actual instance.

    Thank you heaps for your response!
     
    Opeth001 likes this.