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

Question Gameplay Ability System in a data oriented approach

Discussion in 'Entity Component System' started by Opeth001, Jan 25, 2022.

  1. Opeth001

    Opeth001

    Joined:
    Jan 28, 2017
    Posts:
    1,112
    Hello everyone,

    a Gameplay Ability System goal is to be a highly flexible framework for building the types of abilities and attributes that you might find in an RPG or MOBA title. You can build actions or passive abilities for the characters in your games to use, and status effects that can build up or wear down various attributes as a result of these actions, additionally you can implement "cooldown" timers or resource costs to regulate the usage of these actions, change the level of the ability and its effects at each level, activate particle, sound effects, and more. The Gameplay Ability System can help to design, implement, and efficiently network in-game abilities from as simple as jumping to as complex as your favorite character's ability set in any modern RPG or MOBA title.

    I'm trying to design a Gameplay Ability System using DOTS to get full compatibility with DOTS projects and achieve high performance implementation by reusing similar tasks as much as possible.

    the problem is that it is quite complex to port an OOD system to DOD.

    This is how a simple Fireball implementation look like.



    I'm aware that things can get more reactive and performant here by replacing some systems with an Event System like @tertle 's Package , an event system would ensure that events are produced and consumed in the same frame and prevent structural changes, which I will do once I have an MVP.

    I'm very undecided on the approach to handle custom behaviors.
    As you can see when OnAbilityCollisionsEventSystem is first run from the projectile hitcollision event, a new entity is created to represent an explosion using ComponentDatas and custom data.
    but the second run of OnAbilityCollisionsEventSystem, new task entities are created to handle attribute modifications (health..).

    The two solutions I see now are:
    1) Create different Handlers per Ability. (OnFireballProjectileCollision, OnFireballExplosionCollision ...)
    - this approach seems very boilerplate, not scalable and not maintainable.
    2) Create functions per Ability event, which will have the same method signature but different behaviors, this can be done by passing a NativeArray of Bursted FunctionPointers and the capacity will index the correct one.

    Any suggestions are welcome.
    Thanks

    @WAYN_Games.
     
    HouinKyouma27, bb8_1 and GilbertoBitt like this.
  2. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    988
    Hello,


    Thanks for the ping ;).


    I would decompose an ability has follow:

    1) the effects: what happens when the ability is triggered (play SFX/VFX, spawn entity, apply damage)

    2) the restrictions: what prevent an ability to trigger the effects (cool down, range, player class,...)

    3) the costs: this is a combination of effect and restriction; you can cast if you don't have enough mana but if you do you will definitely consume mana (this is mainly to avoid design error where an ability could check for a restriction and forget to apply the resource consumption effect)


    That's it that is your ability.


    Then additional information is for the effect themselves:

    1) time/duration: is it an instant effect? over time effect (oneEnable/everyXsec/onDisbaled?)

    2) target(s): is it a single target effect or a multi target effect (AOE/Party members/...)

    3) when: does it apply as soon as the user hit the action button or after a delay (after cast time elapsed/...)

    4) static data: thing that can be set at edit time and only read at run time like damage type (physical/magical/...) or the power scaling of the effect. if it's an overtime effect, how often does it tick, ...


    if you split it and organize it well it should be possible to define one 'application' system per effect and one trigger system per effect/trigger type.

    Then it's a matter of composing thing to make your ability.


    That is at least the design approach I'm trying to go for.

    Implementation wise things may changes when 0.50 drops but for now I rely on native stream to avoid has much structural change as possible, blob assets for the static read only data, and maps to handle the N..N nature of the effect application (several player can hit several target and both set overlaps).


    On the authoring side I try to make thing has simple as possible for the game designer and has flexible has possible while designing (for rapid iteration) and delivering too (with the help of addressable) all the while trying to limit any potential stupid mistake that could lead to a day of hair pulling debug to figure out you forgot to check a box...


    I have not had much time recently to work on my package has I would have like but it hasn't left my mind either.

    I've set aside some time for that as part of my 2022 resolutions, and hopefully I won't have to start from scratch with 0.50.
     
    bb8_1 and Opeth001 like this.
  3. Opeth001

    Opeth001

    Joined:
    Jan 28, 2017
    Posts:
    1,112
    thank you for answer :)
    I haven't had much time lately, but I'll try to see your package and how you're trying to solve the problem.
     
  4. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    988
    I thrown together a quick "sequence diagram" to try an explain roughtly how I do things.
    Hopefully it will at least help a little understanding my spagetti code :p
    upload_2022-1-25_17-38-15.png

    For now the package has simple direct effect, base constraint (but the constraint system is not flexible/expendable enougth to my taste) and basic UI integration, it don't handle effect over time and AoE.
    I also don't handle spanwing new entities, I need to figure an elegant way to spawn an entity based on a prefab that works well with subscenes, addressable and don't require a lot of setup...
    If all goes to plan I should be able to change an ability "at runtime" through adressable if the code part don't change.
    From changing the damage dealt to swaping to a new VFX or adding an effect to an ability.
    Latest showcase video
     
    bb8_1 and Opeth001 like this.
  5. Opeth001

    Opeth001

    Joined:
    Jan 28, 2017
    Posts:
    1,112
    Thank you very much, it will definitely help to understand your package quickly.
     
  6. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    I'm having some trouble understanding this part. Can you give examples of what "custom behaviors" would mean, and rephrase what the problem is with the second run of OnAbilityCollisionsEventSystem?

    _______

    One thing I'd say on the topic of Ability systems in general, is that specific code-based approaches to abilites have grown on me as opposed to a "mix & match modular components for everything" approach. I'm not sure if your solution is going towards the former or the latter, but it's just my two cents. Basically what I like to do now is a library of static AbilityUtilities that I can use to build abilities easily in code, while still having all the power of writing specific behaviour for specific abilities. Sometimes things get so specific with abilities that tailor-made solutions end up being clearer & much less trouble than fully generic code-less solutions imo

    For a fireball ability:
    A ProjectileAbilitySystem would do:
    • if (AbilityUtilities.CheckUseInput(in abilityInput))
      • if (AbilityUtilities.CheckCooldownFinished(in ability))
        • AbilityUtilities.SpawnPrefabWithOwner(entity, projectileAbility.ProjectilePrefab)
        • AbilityUtilities.OnAbilityUsed(ref ability)
    And a ProjectileSystem would do:
    • move with velocity
    • AbilityUtilities.RaycastForHittables()
    • if hit found....
      • AbilityUtilities.TryApplyDamage(onEntity, projectile.Damage)
      • AbilityUtilities.SpawnPrefabWithOwner(entity, projectile.OnHitPrefab) // explosion prefab
      • Destroy(self)
    And a ExplosionSystem would do:
    • AbilityUtilities.CheckSphereForDamageables()
    • for each hit...
      • AbilityUtilities.TryApplyDamage(onEntity, projectile.Damage)
    • Destroy(self) in x seconds

    This is a very simple case that would lend itself well to being done in a generic way, but other abilities would be hell to implement just in pre-made ability-agnostic modular components. Like a "MindControl" ability where:
    • you click on a target unit and now you control it instead of yourself
    • very specific conditions are checked to see if you can control the target unit. Not just based on unit type/team, but also based on what the unit is doing currently
    • and your character follows that controlled character, but with a slower zombie-like walk
    • and the mindcontrol link can break under very specific conditions
    • and the mana is consumed based on very specific rules that take into account a complex combination of factors
    • and a % of the damage applied to the controlled unit is transferred to you as well, and that % grows over time
    • and once the mindcontrol link breaks, both units are stunned for a duration that depends on how long the mindcontrol was active & the source of the breakage
    • etc....
    Not saying it can't be done in a fully generic way, but I do think a generic approach will make everything unbelievably complicated as opposed to just writing that ability in code directly. A generic approach would also make debugging much harder, and introduce a lot of potential new bugs for combos of components/conditions that weren't planned to work together or for changes that unexpectedly broke other abilities.

    It may seem like the code-based approach is less designer-friendly, but in practice you'll still be able to handle the large majority of abilities with just a few pre-made components/systems either way (most abilities will be spawning a prefab, or targeted buffs/effects, or area buffs/effects, etc...). For the more complex abilities however, the code-based approach will give you all the control you need and will simplify your architecture a whole lot

    However I do think it's a good idea to make the concept of Stats & StatModifiers into a carefully-crafted generic system, that your hand-made abilities use to apply their effects
     
    Last edited: Jan 26, 2022
    CrazyD0G and bb8_1 like this.
  7. Opeth001

    Opeth001

    Joined:
    Jan 28, 2017
    Posts:
    1,112
    Thank you very much for your answer :)

    I believe this problem has already been solved by Unreal's Gameplay Ability System (GAS), which allows all abilities to communicate and manage their states through Tags and attributes in multi-phases.



    In my opinion, one of the strengths of the DOD, is to divide large tasks into small units of work and group similar units from different contexts to perform the same tasks linearly, in parallel and at large scale, which exacly what's posing problems right now.

    In the example provided above, the projectile collision event system "OnAbilityCollisionEventSystem" is executed whenever a CollisionEvent is triggered via a hit detection. it can be a projectile raycasting on its path or an explosion distance point query.
    The complexity with this approach is that the same system will run for many abilities, while some of them might have the same event handling behaviour but others not.
    for example, a fireball would explode on its first impact but a magic arrow will continue its journey and hit all enemies in its path.
    these two abilities have to use the same systems for their behaviours in common. (Propulsion, RayCast Hit Detection, Attributes Modifications, add/remove Tags ..)
    At first glance, I thought maybe I could use different combinations of components to split these different behaviours into different systems, but this will definitely increase the number of systems, which is not a good way to go.

    at the moment i plan to create function interfaces for Abilities Events, this way all Abilities can create/reuse BurstCompiled functions which will be passed through NativeArray<FunctionPointer<Delegate>> and keep all logic in a single system.

    To make things more designer-friendly, maybe at some point I'll add a way to reuse/create event-handling code through visual scripting.
    this is quite difficult at the moment as there is no way to use DOTS packages in assetstore visual scripting tools and AFAIK Unity's Visual Scripting ECS package does not support creating static functions for BurstCompiled FunctionPointers usage.
     
    Last edited: Jan 27, 2022
  8. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    988
    I agree but one should 'ot prevent the other.
    The main idea is to be able to compose your ability. The effect could be as simple as changing a stat or much more complex like you exemple in which case I would go with a simple effect that add would add a component (or store in a system map or something) to the controling and controller entity and then make dedicated system that apply the expected behavior for mind controlled entities essentially coding the mind control effect with reactive systems that would share the damage, change the camera/input target when the mind control start and apply the stun when it stops.

    The point is the aim is not to replace the coder part all together but to give tools for a designer to make most of the work by composing effect into abilities. If it becomes too complex and the designer can't make the ability or it ends up killing the performance then he can request the dev to make a dedicated effect that will do it in a simpler, more effective way.
     
    Opeth001 likes this.
  9. Opeth001

    Opeth001

    Joined:
    Jan 28, 2017
    Posts:
    1,112
    @WAYN_Games
    I read your package, it's still very early and missing some features but it's pretty good :)
    in my case it's a different path because I'm based on Unreal's GAS, which dynamically resolves abilities interactions (Cancel if, Temporary Disabled if...) in a data-driven approach.
    my plan is making all abilities based on Generics and Compositions.
    Netwoking (Prediction, Reconciliation and Lag Compensation ) must be supported by defaut as every Ability and it's Actions are represented by Ghost Entities.

    updated plan.
     
    Last edited: Jan 27, 2022
  10. Opeth001

    Opeth001

    Joined:
    Jan 28, 2017
    Posts:
    1,112
    Hello again,
    knowing that we have to pass a different data structure containing (damage, AOE radius or projectile distance...) which will be used from these Bursted FunctionPointers Callbacks. (OnInit, OnCollision, OnComplete, OnDestroy)
    what would be the correct way to access the metadata component?
    1) store data in an IElementBuffer as a byte array and reinterpret the structure for read/write?
    2) store the data in a generic componentdata (Metadata<T>) and use the DynamicComponentHandle for access?
    - this approach will have some cons as it will ensure that abilities instances will be separated into different chunks and it will require additional codgen for DynamicComponentHandles management.

    Do you guys have a preference between these 2 options or even better do you have an alternative?

    Thank you
     
    Last edited: Feb 1, 2022
  11. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    What I've been doing is something like this:
    https://forum.unity.com/threads/sources-available-dots-polymorphic-components.1132123/

    Essentially; codegen a bufferElement that's a union struct of all the structs it could assume the form of, and codegen switch statements that can convert it to the right type and call a function on it. This solution is also an alternative to function ptrs at the same time, but I haven't done any real tests to compare the performance of the two approaches (switch-statement vs function ptr). I know function ptrs have some limited burstability and apparently cannot take native collections as parameter (which I need), so I avoided them from the start

    I do think it'll perform better than the deserializable byte array approach
     
    Last edited: Feb 1, 2022
    Opeth001 likes this.
  12. Opeth001

    Opeth001

    Joined:
    Jan 28, 2017
    Posts:
    1,112
    it's a good alternative for what I'm looking for.
    I guess in a metadata case some components have to create fake methods to satisfy the interface implementation and it's the developer's responsibility to never call them, right?

    this is also a good idea, I guess for that I have to create a structure for each ability, which is not a problem because most of the structs will not contain any data but only the logic as functions override.
    but how well can the Polymorphic Components handle hundreds of switch cases?

    ________________________
    Edit:
    my plans for the ability system are all based on composition and reusibility, even for function pointers callbacks.
    My plan was to make these events call a list of functions pointer indexes to encourage logics reuse and minimize coding for designers. so they don't need to write code that already exists, but just index it with an Enum.
    but i dont see any way to do it with the Polymorphic Components approach.
     
    Last edited: Feb 1, 2022
  13. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    So you need designers to create some kind of "Ability" prefab/data, and define a list of effects (via enum) that this ability has? And I suppose that based on the selected enum, the inpector must show the editable parameters specific to that effect type?

    Not sure if "Ability" and "Effect" means the same thing for both of us, but in my case an "Ability" is a collection of modular "Effects", and an "Effect" is a modification operation on a stat

    The way I handle this is basically.... lots of codegen. For each effect struct that is part of a polymorphComponent, I generate an authoring version of it. Then I generate a custom editor for Abilities, that will allow the inspector to expose only the parameters of the selected effects when changing the effect enum value. Then I generate the code that converts this Ability editor to ECS: for each effect that is part of the ability, add it on the ability entity as a polymorphic buffer element. And then there's more codegen to be able to define stat types for your game, and make it so that a designer can just say "this effect affects the Strength stat" and the system will know how to resolve that efficiently at runtime

    Codegen is really the only way I found to make a system like this that's designer-friendly but also makes no runtime performance sacrifices. It really makes me hope that Unity will eventually release a user-friendly Source Generators package, because I feel like codegen is really crucial to solving certain problems in DOTS
     
    Last edited: Feb 1, 2022
    Krajca, WAYNGames and Opeth001 like this.
  14. Opeth001

    Opeth001

    Joined:
    Jan 28, 2017
    Posts:
    1,112
    Effects means Attributes operations (Health, Mana …)
    I’m using terms that I’ve learned from Unreal’s Gameplay Ability System. ;)
     
    Last edited: Feb 1, 2022
  15. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    988
    +1 on that
     
    Krajca and Opeth001 like this.
  16. Opeth001

    Opeth001

    Joined:
    Jan 28, 2017
    Posts:
    1,112
    Yes this seems the best way to make it generic for the Designer .
     
  17. Opeth001

    Opeth001

    Joined:
    Jan 28, 2017
    Posts:
    1,112
    How do you guys sync Abilities across the network?

    My current plan is to do a synchronization based on the NetCode package by having every ability and its actions represented by ghost entities, which will be automatically synchronized across all Clients.
     
    Last edited: Feb 4, 2022
    bb8_1 likes this.
  18. Opeth001

    Opeth001

    Joined:
    Jan 28, 2017
    Posts:
    1,112
    I plan to set all abilities and actions as owner predicted and fully interpolated for other clients.

    Server :
    * Apply Movement Prediction on Projectiles...,
    * Apply HitDetection on Projectiles/AOE...,
    * Apply Game Effects,
    * Dont Apply CUEs (VFX, Animations...)

    Owner Predicted Client (Ability Spawner) :
    * Apply Movement Prediction on Projectiles...,
    * Apply HitDetection on Projectiles/AOE...,
    * Dont Apply Game Affects,
    * Apply CUEs (VFX, Animations...)

    Interpolated Client :
    * Dont Apply Movement Prediction on Projectiles...,
    * Dont Apply HitDetection on Projectiles/AOE...,
    * Dont Apply Game Effects,
    * Apply CUEs (VFX, Animations...)

    this way, all projectile movement predictions errors will be automatically playback and corrected.
    I still have to implement a server side system to validate predicted spawns of abilities and actions from clients in order to avoid cheating and mispredictions.
     
    bb8_1 likes this.
  19. Opeth001

    Opeth001

    Joined:
    Jan 28, 2017
    Posts:
    1,112
    @lclemens I tagged you here to get a comments/advices from you because you already made your own ability/skill system :)
    the design above was done very quickly, so it may be wrong at some points.

    As I stated in this comment, for the next few weeks I will be focusing on the Gameplay Ability System, so gathering as much information as possible will give me a good start.

    I plan to create a Gameplay Ability System that is heavily inspired by Unreal's one, but in a data oriented approach.

    my goal is to create a package that can :
    * help artists easily build Abilities/Skills mostly based on composition.
    * synchronize abilities across the network by default using Netcode Package.
    * Abilities are represented by ScriptableObjects which can easily be created/modified without the need to republish the game on mobile platforms.
    * which benefits from DOTS Performance / Clean design.
    * Interactive Abilities by design, eg. a thunder spell deals more damage to Soaked players....


    Some concepts are used :
    * Attributes to manage resources/capabilities... (Health, Mana, Speed ... )
    * Tags to manage States. (Stunned, Untargetability, Burning, Soaked, Frozen, Fear, Silenced, Invulnerable ...)
    * Events that can contain any Shared/Unique behaviour(s)

    The whole concept is still incomplete for the moment.
     
    bb8_1, lclemens and Krajca like this.
  20. lclemens

    lclemens

    Joined:
    Feb 15, 2020
    Posts:
    760
    @Opeth001 I think your approach looks similar to mine. The basic philosophy is to use composition of various tags and data on an "ability entity". These two high-level discussions were invaluable to me and I studied them in detail:
    I didn't use any fancy or odd concepts - everything I used falls within the ordinary ECS programming paradigm that a beginner DOTS programmer would know. No codegen. I didn't even use tertle's events.

    In those reddit descriptions and in my implementation, each ability has a DynamicBuffer<ImpactData> where ImpactData is just Entity target and float3 hitPos. Whenever any system decides it wants to create an impact, it appends an impact to that buffer via ecb. Then in the next frame all the systems can react to that event (display pfx, play sounds, apply damages, etc) by checking if the impact buffer is not empty. As I mentioned in @PhilSA 's thread, I used Approach C for accumulating multiple damage modifiers, speed modifiers, etc in one frame, so each enemy has a DynamicBuffer<DamageModifier> - so there are also systems checking if the modifier buffers are empty and applying those modifiers. There is a system that clears the buffer at the end of each frame.

    In addition to the "impact" event, each ability also has a boolean trigger state so that any systems can react during the frame whenever a trigger event occurs. And of course, at any given time systems can operate on the ability (like performing linear movement for a bullet, applying physics gravity, etc). So in summary - there are two events that systems are interested in: trigger and impact.

    On every ability I have a TriggerValidityData, which is important because it accounts for all the variables that might modify when the ability is allowed to trip the trigger event. There is a system that continually updates this based on does the targeter have a target, is the turret/weapon pointing in the right direction, etc. In the case of a player triggered ability, the criteria would also include if the player clicked an ability button or pressed an ability hotkey.

    One difference is that all of my abilities are only for NPCs... My "player" doesn't really have abilities yet, but I think they would be similar with the only major difference being that they would require additional input from the user-input system when determining if the ability can be used or not. And they would specify an icon for the HUD.

    A second difference is that I don't use netcode, so I don't know if my system is compatible with networking or not.

    The last difference is that I used prefabs instead of scriptable objects, which I may convert to scriptables at some point (should be fairly easy if I can find the time). I guess the reason I used prefabs at the time is because some of my abilities have models with multiple child objects that need to be positioned appropriately. For example, a sniper shot ability has a muzzle flash and a beam flash (see screenshot below)

    The one problem I had with the concepts defined in two reddit posts is that they call for creating an ability whenever it is time to "fire" and then destroying it when it is done. Well I tried that, and it got ugly for a few reasons:
    1. Some abilities need some persistence. For example, one ability I have is a laser beam that fires whenever its targeter chooses a valid target. Well it's firing at say 4 per second, causing damage each time, but it also has a beam that needs to be constantly enabled and adjusting to the target as long as a target exists - and it needs audio engange/disengage/loop sounds to be played. Also, what if you want passive abilities like the ability to heal any nearby friendlies over time, or boost their stats?
    2. It was counter-intuitive to set the cooldown-interval (or fire rate) on the "spell-caster or weapon" instead of embedding it with the rest of the stats on the ability. The designer would have to look in two places.
    3. It doesn't support things like a projectile that can go out and impact an enemy, and then move to another one (like a mini-drone or something).
    So the simple solution was to embed the cooldown interval into the ability itself. That solved many issues, however I then had the issue of handling projectile abilities that need to destroy themselves. The simple solution was to make a new component to spawn a new ability when the trigger event happens. So a cannon turret has a persistent "CannonSpawner" ability that spawns "CannonBullet"s (which destroy themselves on impact).

    upload_2022-4-22_13-23-2.png

    It works great! With just with that one mechanism, I can make all kinds of crazy things like abilities that spawn multiple bullet abilities on launch, abilities that explode on impact and fire out other abilities, etc.

    One last thing - splash damage was a bit difficult to implement for abilities that destroy themselves on impact. I ultimately fixed that with proper system ordering. If you're curios about how that was solved I can go into more detail.

    It's outside of the scope of the topic, but I also discovered a pretty easy way to handle over-time effects (like damage-over-time, slowdown-over-time, etc. After one failed attempt, I'm pretty happy with my final solution and it's also very extensible and easy to add new over-time effects.

    As for performance, ECBs are used a decent amount, but I don't see any reason why my approach would have any big performance hits. I haven't benchmarked, but I see no obvious reason why it wouldn't scale to many hundreds of thousands of NPCs with a few abilities. I suspect the limiting factor would be having too many particle effects, but they are pooled game objects and are started in the main thread. There are a few places where CDFE is used like when fetching a particular targeter, getting a target's position, or maybe to flash a muzzle, but they are relatively rare. Nearly all of the systems act directly on the ability without getting parameters via CDFE.

    Overall I'd say that the most difficult thing I've built in my game so far was this ability system. It's weird because for so little code, it sure was tough coming up with a workable design that is flexible enough to handle lots of future abilities! Ultimately, I think its success can be attributed to using the composition principal. Unlike the traditional OOP approaches to skill/spell/ability systems, my system doesn't have a big huge behemoth structure with a GUI that has a zillion options. Recreating that would have taken me years. Instead, there are a bunch of little systems all reacting to abilities built via composition and as a result, it's actually very little code. I see no reason why I couldn't use this framework to build a mind-control ability.

    To keep it interesting, here are some screenshots of a cannon turret that fires bullets that cause splash damage and burn-over-time damage. Keep in mind that this composition could be done via scriptable objects instead of prefabs.

    upload_2022-4-22_14-29-56.png

    Anyhow.... That's just a summary of how I pulled it off. I'm sure there is lots of room for improvement.
     
  21. Opeth001

    Opeth001

    Joined:
    Jan 28, 2017
    Posts:
    1,112
    In my case, I think I will make few simple code generation to minimize developer interactions for creating Tags or Attributes.

    I think I'm going to use the turtle's event package to handle server-side stats calculations. (Kills, Assists, Damage Dealt...) but nothin really related to the Gameplay itself.

    from this I conclude that your movement system is not implemented using the ability system.
    for now I'm doing the same because the ability system is not implemented yet, but i still insure about this.
    i rember that Unreal's documentation stated many times that anything can be an ability, from a simple Jump/locomotion to a fireball.
    it can really help maintain interactivity between game features, for example. an ice blast can impede the user's movement locomotion, but not it's ability to aim and cast other abilities.
    it can be difficult if the locomotion system couples movement and rotation into the same system while ignoring the Ability system Tags, after all a movement system will just act as an effect modifer for the Velocity and AngularVelocity Attributes.

    I don't have much info about your game :), but our game is multiplayer and completely unplayable without networking. this is a factor that makes my plans on the Gameplay Ability System very much tied to the Dots netcode Capabilities and limitations.
    eg: structural changes on ghost entities are not synchronized across the network.
    in my current plans, Abilities are composed of actions (ghost entities), which by default synchronizes them on the network, any mispredicted action will be cancelled/Reconciliated by the netcode package, that also makes client side prediction and server side lag compensation handled by default :)

    how do you define events behaviors? what if 2 abilities share some behaviors but differ in others.
    For example: A Simple Fireball (A) and Lightning Arrow (B) both move in a straight line, but (A) explodes on first impact and deals damage to all nearby enemies, while (B) deals damage on impact and takes the closest enemy as target by selecting soaked enemies first.

    how do you handle them ? :)

    so far the most difficult package I've ever created is my Custom Hybrid Renderer, which requires rewriting URP shaders to support GLES 3.1 while supporting DOTS features such as Metrial Overrides... it also required creating Compute Shaders to process Frustum Culling and data compression on the GPU side to eliminate the CPU-GPU data transfer bottleneck.
    I really hope this package will still be the hardest one i've created after writing the Gameplay Ability System :D !

    @lclemens thanks for the share :)
     
    Last edited: Apr 23, 2022
    bb8_1 likes this.
  22. lclemens

    lclemens

    Joined:
    Feb 15, 2020
    Posts:
    760
    Yeah I guess I forgot that I use [GenerateAuthoringComponent] indirectly for authoring tools. But not I'm not doing anything fancy like with PhilSA's polymorphic struct tool.

    As for player movement, my only player movement is via camera, so for you player movement is a little more difficult. But here's the funny thing... what we and Unreal are calling an "ability" is a blurry line. In OOP there are actual classes representing an ability, so it's a clear cut-and-dry distinction that a fireball IS an ability. But here in ECS we just have components so instead of having is-a relationships, the "ability" related components we have are more like a "can-do-this-verb" relationship. So your "fireball" is not really a fireball... it's more of an entity than can-splash-on-impact, can-move-forward, can-render-a-model, can-set-things-on-fire, can-play-impact-particles, can-play-impact-sound, can-trigger-with-hotkey, etc. At no point is it actually an "ability" other than in our heads! So if you put a can-jump component on your player, does it then become an "ability"?? Only in our heads. That is why I'm putting the word "ability" in quotes.

    When I first started, I was thinking "I'm going to create an ability framework", but really all I created was a handful of can-do systems and components such as can-trigger-on-cooldown, can-trigger-on-button-press, can-impact, etc. I know I said it took me forever to build this, but really it just took my thick skull forever to realize that there is no "ability" - like in the Matrix there is no spoon. Seriously... If I knew what I know now, instead of spending 2 months agonizing over my "ability framework", I could have literally had a functioning "fireball launcher" and a "fireball" in a day! Plus maybe a couple more days to implement a handful of other "abilities", one of which could have been a can-jump. The only special-sauce was realizing that I needed a buffer of impacts on the the entity that is performing the action and not on the entities the action affects (which is a slightly counter intuitive). The things that get hit still have buffers for handling multiple damages per frame or applying different effects, but they don't need to care about the concept of an impact. In the end, I didn't build a "generic ability framework" that I could put up in a git repo for people to use... I just implemented some impact and firing data components and some systems to handle the that functionality. I re-used a lot of my older systems and all of my systems are very small and simple. I suppose I could put those systems and prefabs/scriptables into a repo and then anyone could have a fireball that works like mine or a character that jumps like mine. But there isn't really a clear separation of where the "ability" system ends and my game begins. Haha I hope I'm making sense... I can't quite figure out how to properly put what I want to say in words.


    I'd love to have multiplayer in my game but there are only so many hours in a day and I'd be an old man at game-release time if I tried to tackle that at my pace. I don't really even know what a "ghost" is but I keep reading about it every now and then. You'll have to decide what can and can't be done via whatever approach you take. All I can say is that there is no voodoo in my approach... it's all standard ECS stuff so hopefully it can translate to netcode.


    I'm not quite sure what the term "soaked" means in this context, but I'll just assume it's a certain property that some enemies have. Taking my best guess, I would make these two entities:
    • Fireball { can-move-linear, damage-amounts, can-splash-on-impact, buffer-of-impacts }
    • LightingArrow { can-move-linear, damage-amounts, can-splash-on-impact, buffer-of-impacts }
    Each of the components would have a small amount of data. So can-move-linear would have float speed. damage-amounts would just be a float damage, or it could be a fixed array of floats representing a combination of kinetic/fire/electricity/cold or whatever damage types you have. can-splash-on-impact would have float splashDistance and uint ignoreMask (or includeMask) - a filter bitmask for including certain enemy types and ignoring others.
    I would (and do) have these systems:
    • A linear-move-and-collide system to move any entity with can-move-linear and append an impact to the entity whenever the entity collides with something.
    • A splash-on-impact system that will take any entity that has impacts on it and do an AABB box overlap or whatever to fetch all entities within a radius. If the bitmask is non-zero, it would use CDFE on the target to ignore any corresponding types.
    • A impact-damage system that will take any entity with impacts and apply the damage to impact.hitEntity.
    Alternatively, you could use two separate systems - a normal splash system and a system for only splashing soaked enemies, but I'd prefer to use the mask with one system to avoid code duplication. It wouldn't be much duplication though because I use a static AppendSplashImpacts() function that takes an ECB and a radius and appends all the impacts.

    Of course, for a fireball you would probably also want components and systems for playing particle effects, explosion sounds, etc, but they're not really related to the question and once you write them they'll work for any particle effect or sound on any ability. Such systems are so easy to write - the foreach loops are like 3 lines long. I combined all the jobs into a single system since they all need to run in the main thread anyway because they deal with game-objects.

    On a slightly different topic, I should mention that I separated my enemy AI "targeter" entity from the "ability" entity (entities that have impact buffers on them). I did this because for turret weapons it didn't make sense to have 4 targeters on a turret with 4 weapons when the turrets can only aim one direction at one target. I do pass abilities the entity ID of that "targeter" in case they need it. Many "ability" related systems don't use it, but some systems like the linear-launch system will use it to set the appropriate vector for the linear-move-and-collide system. For abilities that shoot, I also pass along the root entity of the entity that shot it.


    I tried two different approaches at first, but they both were miserable to work with. My final solution might not be 100% the most efficient or anything, and you're going to laugh, but it's simple as hell and it works great. I have a prefab/scriptable for each "effect" (again "effect" is just a concept in our head). I put different combinations of components on those prefabs like damage-over-time, modify-speed, play-particle-effect, to make different behaviors like "Burn" or "Slow" or "Mute" - the possible configurations are huge. Then I just add a component to the "ability" prefab with an EffectData component, which contains a bitmask that corresponds to each type of effect (right now I use a 64 bit mask). A EffectSpawnToParentSystem will look for anything that has impacts and EffectData and an ImpactData buffer and for each impact it spawns a new instance of the effect and parents it to the target entity (via unity.transforms). The foreach loop is like 10 lines total and half of those are end brackets. Of course there are systems to implement functionality for the different types of over-time effects (damage-over-time, mute, slow, weaken, bonus-drop, etc), but they are all small and simple. Those systems apply the affects to the entity via GetComponent<Parent>().

    I don't know... maybe it's not the ultimate way to do it, but it sure is easy. I don't even have to do anything special to make the particle effects follow the target around or worry about destroying stuff when the target dies. And it supports effect stacking by default. Right now I am allowing stacking to give it multiple particle effects, which looks pretty cool because the more they get hit the brighter the fire burns, but I suppose I will probably have to eventually put a limit in there to keep from hammering the graphics card with too many particle effects.

    It's funny because EffectSpawnToParentSystem doesn't have to spawn things that we call "over-time effects". It could spawn and attach anything - "abilities", other enemies, a lamp-post, whatever!

    Holy crap, that's impressive! I skim over some of those custom renderer threads in these forums and 90% of it goes right over my head.
     
    Last edited: Apr 24, 2022
  23. Opeth001

    Opeth001

    Joined:
    Jan 28, 2017
    Posts:
    1,112
    Thank you very much @lclemens , I really like how you approach things in a very simple way.
    Your comment here is very constructive and has already made me change my mind about some implementations.

    Yes, time is the most valuable thing from the second we all knew that at some point we would die one day xD
    I already implemented some of my game networking stuff, but I'm going to switch to Dots Netcode because pretty much everything is already implemented, the rest is just configuration or api calls to fit my game custom behaviors, like sending RPCs when a player dies.

    yes but some points should be taken into account before implementing the "Ability Systems", like you should avoid using entities as reference inside components, because these are not valid across the network, it's is like sending a pointer.
    but there is an exception to this when using dots netcode. it is possible to send Ghost entities (networked predefined entity archetypes) across the network as long as the ghost entity is not destroyed when the message arrives.


    I see how your approach works and it seems functional and simple.
    at first, I planned to base the composition side of the "Abitlity System" on injecting reusable behaviors (identified by enums) into Events (OnCollision...) via Bursted function pointers or @PhilSA package.
    but now in my head, it's a more of an Event-Driven approach, Events like (OnTrigger, OnCollision, OnTimeout ...) will just create Dynamic EventRequests (Explision, ChangeTarget ....) that can be handled by specific systems later.

    I already plan to use bitmasks for pretty much every action that will be created by an "Ability", as it needs to support full/partial/temporary abilities cancellation.
    Full: If a player is stunned by an ice spell, he can be freed by an item or another spell.
    Partial: If a player is stunned and burning by a fire spell, he can be freed by an item or another spell but will still burn for x seconds.
    Temporary: if a player is burning by a fire spell, he can become invulnerable for a few seconds against fire spells or even any type of magic spells.

    i already have those :)

    that seems cool and easy :)
     
    lclemens likes this.
  24. lclemens

    lclemens

    Joined:
    Feb 15, 2020
    Posts:
    760
    It will be difficult if you can't use entity IDs as reference... My "abilities" all have TargeterEntityData with a ref to the targeter, which has a ref to the currently selected targer. Most importantly, my ImpactData struct has a ref to the entity that was hit... and that is a huge part of nearly all my abilities. I'm guessing there must be some standard tricks to getting around that limitation... hopefully because without entity reference I would have had to do things very differently.

    I think you'll be glad you went with the events approach. The only "events" I use are:
    • Trigger (attack cooldown timeout, button press, or setting it manually somewhere). It's just a standard component with a boolean where for one frame, the boolean is true and all the read systems are polling for it. The component for these events goes on the "ability" entities.
    • Impact - this is a dynamic buffer of ImpactData. Lots of systems read it and several systems write to it whenever they want to register an impact (physics collisions, insta-hit weapons, area weapons, etc). The dynamic buffer for these events go on the "ability" entities.
    • Modifier events - for doing things like applying multiple damages in one frame. The dynamic buffer for these events are on the entities that get hit.
    If you think some code would be useful when you start your implementation let me know and I'll post some here or send it to you.
     
    Opeth001 likes this.
  25. Opeth001

    Opeth001

    Joined:
    Jan 28, 2017
    Posts:
    1,112
    Fortunately the solution comes with the dots netcode package, even if it has some limitations. (For the moment)

    Thank you very much :)
     
  26. lclemens

    lclemens

    Joined:
    Feb 15, 2020
    Posts:
    760
    Help! I'm stuck on something right now... maybe some of you guys have some ideas - it's related to ScriptableObjects in an ECS paradigm.

    I started using ScriptableObjects to define things instead of prefabs. So instead of having a prefab for each enemy, I now have a ScriptableObject (SO) to define each enemy. That worked fine, so then I converted another main category and it worked worked fine as well. I integrated the SOs with addressables so the designer can add new instances easily and a function will load up all the spawnable SOs at the start of the game. Clonable prefab entity IDs are stored and a blob-asset dictionary that makes it easy to spawn any type.

    But the trouble came when I decided to convert my "abilities" prefabs to SOs. The problem stems from my inability to do composition with an SO. With the prefab, a designer can simply started dumping authoring components onto an "ability" prefab (PlayLaunchPfxData, DamageData, DestroyAfterCooldownData, EffectSpawnOnImpact, etc). All the variables display right there in the inspector and it's easy to tweak. But... with an SO it is messy. The most obvious compositional approach is something like
    Code (CSharp):
    1. public abstract class Component : ScriptableObject { }
    where Component is an interface that SOs inherit. Well I ran into several issues with that...

    Problem #1 - it looks like this
    upload_2022-7-13_21-51-27.png
    Which is a lot less user-friendly than the example screenshots using prefab composition in my previous posts.

    Problem #2 - Because each scriptable object that gets put into the list must refer to a derived instance, I have to make a scriptable object for every ability so they don't all share the same data. So I would need PlayImpactPfxFireball, PlayImpactPfxPhoton, etc. There are at least 20 components for each ability behavior type, so I'd have to create 20 for every single ability... so it would be several hundred scriptable instances in separate files.

    Problem #3 - Every time I want to make a new behavior component (like PlayImpactPfxData or something), I will have to create a scriptable object that corresponds to it and also I'll need to write a function to convert the data from that SO and make a call to entityManger.AddComponentData(). With prefabs I just make the new IComponentData and either add [GenerateComponentData] or make an authoring component and I'm done.

    -----------------------------------------------------------------------------------------------------------
    Here are potential solutions I toyed around over the past couple of days.

    Solution A - Make a scriptable object that corresponds to every possible IComponentData with a checkbox for each one that enables or disables it and hides/shows its variables. It would solve the first and second problem, but it would be a nightmare on the third problem - I would have to constantly synchronize this massive list of variables with most components in addition to ensuring that there are corresponding calls for AddComponentData().

    Solution B - The first was to use an an attribute drawer to expand the scriptable objects. Odin Inspector has
    [InlineEditor] and NaughtyAttributes has [Expandable] which allows a scriptable object list to be edited in the same SO. Combined with [SerializeReference] to make a polymorphic list via "List<IConvertGameObjectToEntity>", I can get something that looks like this:
    upload_2022-7-14_15-48-56.png
    It certainly looks better and fixes Problem #1. But unfortunately, it does nothing to fix the other two issues.

    Solution C - I realized that I can legally make any data component inherit an interface. So on a scriptable object I can do something like "public List<IEnhancedComponentData> components;" Combines with [InlineEditor]/[Expandable] this is similar to solution B, but the types are serializable so I'm not sure how I would get the data to save. A similar idea would be to make a list of any class that inherits IConvertGameObjectToEntity. In order to make a list of all available components I loaded from assembly and got the type of any class subclassing from IConvertGameObjectToEntity, then I auto-generated an enum so that I could make a pulldown box to select a type and a button to instantiate it. I gave up when I realized that I would have to make a special custom attribute drawer just to show them since Odin/NaughtyAttributes expandable/inline-editor attributes were meant for types based on game-objects. Also I would need to serialize the data. If I could get this to work, it might solve all 3 problems.
    upload_2022-7-14_15-16-48.png

    -------------------------------------------------------------------------------------------

    At this point I'm debating if it's even worth using scriptable objects - perhaps I should have just stayed with prefabs?

    I came up with this pro/con list for SOs vs Prefabs:
    • Prefabs - Prefabs are perfect for composition.
    • ??? - SOs are best for fixed data definitions that all have something in common, but worse for objects that heavily use composition.
    • SOs - multiple SOs can be made that use the same underlying prefab.
    • ??? - SOs are easier to synchronize instances because adding/removing a field of the prototype applies to all instances, but if the field has a setting, you still have to change it for any instances that don't have the default setting.
    • Prefabs - Prefabs can use the Addressables PrimaryKey as a lookup instead of making a separate identifier with SOs
    • SOs - SOs can configure an entire entity with all sub-entities in one place, but it might be difficult to display for variable child entities such as 1..4 weapons.

    My thinking at this point is that while there is a higher number of advantages for SOs, prefabs have one huge qualitative advantage - composition. One of the biggest advantages of DoD over OOP is the focus on composition over polymorphism, and I am using composition very heavily throughout the game - especially for abilities. At this point, if I can't figure out a good way to do composition with SOs, I am ready to throw away my last 4 days of work and go back to using prefabs because to me, that advantage outweighs all the advantages of SOs.

    So what are you guys using to do composition with scriptable objects? Did you find an acceptable solution that combines the best of the 3 worlds, or are you just living with the extra maintenance? Or did you determine that prefabs are better in this insane game-object-to-entity pipeline?
     

    Attached Files:

    Opeth001 likes this.
  27. Opeth001

    Opeth001

    Joined:
    Jan 28, 2017
    Posts:
    1,112
    hi @lclemens,

    Unfortunately, I had to change my plans/priorities with the Gameplay Ability System but I'll be back to work on it in few days, so I haven't worked on that phase so far.

    i had to use OdinInspector and NaughtyAttributes in my Custom Hybrid Renderer to give the designer the possibily to define material properties overrides and both were pretty easy to learn and work with.

    I also had to use them with ScriptableObjects to define Cards, Chests, Shop Offers... and all of them are composition based and very dynamic.

    if i understood your issues correctly, i had a similar case where i have a list of interfaces and depending on the elements types or values, the whole ScriptableObject structure (list element) may display differently on the inspector and can even give the possibility to define nested lists.

    I personally had a pretty good experience as I was able to reuse most of the property drawers between ScriptableObjects.

    in the Gameplay Ability System, I plan to use the Visual Scripting tool as a user-friendly way to define ScriptableObjects and bind data between AbilityCards SOs and AbilityDefinition SOs.
    at bake time, the Visual Scripting graphs will be executed to create the AbilityDefinition SOs. these SOs will be converted to Abitlities Prefab Entities at runtime.

    this way i can get:
    1) Very easy to use even for non-programmers.
    2) Default runtime performance.
    3) Abilities can be modified/created at runtime.
    4) The game saves tons of memory by only converting abilities that will be used into entities.

    I hope this answer helps you.
     
    lclemens likes this.
  28. lclemens

    lclemens

    Joined:
    Feb 15, 2020
    Posts:
    760
    I was unable to find a good way to do composition with ScriptableObjects. It just seemed like no matter what I tried, I couldn't find a way to make ScriptableObject composition as straightforward as dragging components onto a prefab. I haven't played with any Visual Scripting tools and to be honest I'm not real crazy about visual scripting solutions. So for now I am going back to using prefabs instead of ScriptableObjects.
     
    Opeth001 likes this.
  29. Opeth001

    Opeth001

    Joined:
    Jan 28, 2017
    Posts:
    1,112
    i understand.
    in my case the SOs will be only used as data containers so i can do anything that can be done in a monobehavior.
    the Visual Scripting part is more like a way to create and fill the SOs and it can go beyong the inspector/editor window limits.
    Good Luck :)