“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