Implementing a complex RPG combat system with DOTS

Have any of you tried implementing an RPG combat system with a meaningful degree of complexity using this DOTS tech yet? I can’t seem to wrap my head around how the hell someone would go about doing it.

More specifically, the concepts that have been bothering me have been Stats, Items, Effects (Buffs/Debuffs), and Abilities. I know you can just create a Health or Vitality component but is that really the best way to go about this? What happens when you want an item which can apply an effect to it’s equipper? How do you handle an effect that might grant an ability? How would you define your systems? What is your accepted degree of granularity? And the list goes on :frowning:

All of these things, while fairly straight forward in C# with OOP, seem immensely complicated to me with the new DOTS approach. I’m sure it’s probably just me but I would LOVE your advice on how you might go about doing it.

1 Like

“RPG” is a vague term. But let’s say we’re talking about a simple JRPG for example:

Every character is an entity with a “CharacterStats” component (among other things). This contains Strength, Intelligence, Dexterity, etc… The CharacterStats component should represent the stats with all modifiers applied, and there should probably be a place where the original “base stats” are kept (either within that same component, on another component, or in blobassets, etc…).

You always need to keep the base unmodified stats somewhere so that you can recalculate their final values with an ordered stack of stat modifiers. Order is often very important when it comes to stats modifiers, because a ‘+’ followed by a ‘’ will give you a different result than a '’ followed by a ‘+’.

The character entity has a DynamicBuffer of Buffs currently affecting it. A Buff describes modifiers for character stats. Whenever a buff is added/removed/modified, the character stats are recalculated based on the reference base stats + the current buffer of buffs

Alternatively, you could recalculate the final stat value every time you need to access it, based on the Buffs array, but it would be way less performant than recalculating stats only when buffs change

Every item is an entity with an Item component to identify it as an item, and perhaps other more specific components such as “Weapon”, “Armor”, “Consumeable”, etc…

For an inventory, there could be:

  • On the Character entity, an entity reference to the Inventory entity
  • An Inventory entity contains a DynamicBuffer of ItemEntities
  • Each Item is an Entity with the components described above

And finally, for equipment, the Character entity could have a DynamicBuffer of “EquippedItems” which hold an Entity reference to the Item entity. Or maybe it should be more specific than that. Maybe you’d have a “CharacterEquipment” component that holds Entity refs to: RightHand, LeftHand, Body, Head, Accessory1, Accessory2 equipments, etc… but there could also be a “toolbelt” of equipped consumeable items which would be a DynamicBuffer of items with a max capacity

  • Characters have a DynamicBuffer of AbilityEntities.
  • An Ability entity has several components describing the ability, such as: Ability, Magic, PassiveEffect, etc…

You could also have a DynamicBuffer of Buffs (as described above) on the Item itself. When an item is equipped, trigger a recalculation of all buffs present on the character and on all equipped Items

An Item entity could hold a DynamicBuffer of Abilities. When a character wants to know which abilities it has, it looks in its own Abilities buffer, as well as in the Abilities buffers of all equipped items

Or if you want some magic spell that grants an ability, you just have to make that spell add a new Ability to the AbilityEntities buffer on the target character

In a JRPG, nearly all gameplay logic would probably be event-based. So you’d just make various DOTS event systems for everything that can happen in the game, such as: AttackEvents, EquipItemEvents, UseAbilityEvent, DamageEvent, etc…

There are various ways to create event systems in DOTS, but one way would be to create entities that represent events, and have Systems that “consume” and destroy these events.

Other approaches to event systems would be:

  • Have a system somewhere that holds a public NativeList/NativeQueue of “Event” structs that every other system can write to, and processes them every frame if there are any. The downside of this approach is that all events have to be represented with the same “Event” struct containing all possible params that your game’s events could require, which is not a limitation you have with entities-as-events. It would potentially have better performance though
  • Use NativeStream to read & write events data. This is kinda similar to how you’d send events/RPCs over network in an online game. But this is a more advanced approach and would take too long to explain for the scope of this discussion

The important thing to remember is that there’s nothing wrong with using ComponentDataFromEntity<> and “entity references”. When you use these concepts, working in DOTS can become just as simple as working in OOP, and it’s still pretty darn performant. You still want to work in a data-oriented way where performance matters, but you don’t HAVE to do it absolutely everywhere

17 Likes

Hi

For me ECS is holly grail exactly for for system you describe.

In OOP in C# any small component (i.e. buff) eat unnormus amount of memory and laid far from mob it belongs so performance is slow.

So I just end up creating super lightweight version of ECS.

Separation was (ECS terminology):

  • Mob is Entity with main component MobData

  • Each Attribute of Mob is Separate Component (Health, Armor, Energy, BaseAttackPower…)

  • For Acual Stats we have separate components (runtime) that computed(and recomputed) from actual items, buffs debuffs…

  • Each type of mob is designed by GameDesigners and end up EntityArchetype

  • We dont have dynamic abilities on mobs, so cant say something

  • Buffs/Debuffs can be multiple on on mob so in terms of Unity ECS you want to use IBufferElementData or create logic of fusing multiple same effect into one component

  • Buffs can alter any attribute on entity in some ways but it do nothing with attribute if entity dont have it. Systems witch EntityQueryFilter help there.

  • Items in hand and on doll affect stats either (but only if stat exist)

  • Same with damage. Different type of damage deal damage to different attributes but if attribute is absent damage not taken. So to create immortal entity just dont add health attribute to it.

  • Mob Actions like Move, Attack, Heal just use stats and dont pay attention to other components.

And so on :slight_smile:

4 Likes

Just wanted to say I really appreciate posts like this. It’s tough for me to visualize ways to solve complex interactions like this in ECS, sometimes it’s easy to get mindflooded and forget that sometimes it’s okay to just define relationships with Entity references.

As a side note I feel like having better query options would make these types of problems much easier to solve, too.

4 Likes

I guess another thing that might be confusing you is that ECS is designed to mainly loop over all data every frame but if you’re doing something turn based you may only want to apply buffs on a si gle frame. For that, I would recomend using an ECS based event system.

1 Like

A few lessons learned off the top of my head.

Logic that modifies stats/effects/abilities is usually in a very few places. Logic that needs to access it to factor it into other decision making, tends to cover a very large surface area. For the latter lookup tables tend to work well. Everything as a separate component that you pass into IJobForeach will quickly become problematic.

Most logic associated with stats, effects, combat actions, and abilities doesn’t need to run every frame.

You almost always have a working set of data that is different from ‘all’. Equipped items, equipped abilities. Max number of effects on a logical entity. There is no need for your combat logic to have access to full item inventory, or be aware of every ability a character knows. Working sets can easily fit into components or dynamic buffers.

Things like items, abilities, even effects usually have a notion of instance vs constant/static data. You absolutely should keep those separate, leverage that difference. The former in components/buffers, the latter in say blob assets.

Rpg systems tend to have a lot of ‘effective’ stats. Like effective health based on core stats plus any number of effects. So a not uncommon pattern is where stats/effects change, build out readonly structures for consumption by features that just need to read the data.

For static data, like your global list of items/abilities, having scriptable objects as the source for those and exporting them to blob assets works well. I highly suggest a scriptable object container, and then just custom classes/structs inside that for the actual data.

For effect systems, understanding how it’s been done in other games helps a lot. Rpg mechanics boil down to a fairly small set that’s just been reused over and over in different combinations and relatively minor variants. You have stat modifiers, heal, damage, one time vs over time. Aoe that itself has a few variants. Effect stacking where you have 2-3 common variants. Probably missing a couple but the point is these are all well known, so you should reduce them all to their core abstractions. Do that and pretty much anything new you add will fit.

7 Likes