Ability database/scriptable objects for a combat system for a 2D style RPG

So, I have done a bit of looking around into multiple tutorials and works on setting up combat systems. The problem I have run into is that very few of them really go into a very deep skill creation system, for storing and creating all of the skills to be used in the game. I have puzzled over how best to handle this, and would appreciate some pointers or direction to some good tutorials on the subject.

Currently my take on handling it is using scriptable objects to create the actual skills, putting in common variables in them such as the skill name, description, cool downs and targeting information if I wanted to hit single or multiple targets, or if it was for targeting allies and such.

In the skills themselves I would hold a List of a custom effects class, and I would create different effects that inherit off of the effects class with the effects class only really holding an enumerated variable to help me determine how to parse the effect one I actually use it in the code itself. So I would have a DirectDamage effect, and it would have information on how much damage it would deal, and then I could also have an Debuff or Condition effect that would apply a negative effect to the enemy both in the same effects list. I would then run through them in the actual attack code and determine what to do based on the values stored in these effects.

I have not written the code out yet, so I do not really have examples to post and hope I am being clear enough to be understood on what I am looking at.

How has everyone else handled these kind of problems when it comes to creating a database of skills?

I’ve posted in the past about ScriptableObject-based skill systems. They have pro’s and con’s.

How many skills will your game have? How important is it to balance them? If you have a lot of skills that require balancing, a more efficient workflow might be to define them in an Excel spreadsheet. Something like:

Name    |Mana|Target|TargetMode|DirectDamage|DPS|Cooldown|Buff |BuffValue|Spawn
--------+----+------+----------+------------+---+--------+-----+---------+-----
Fireball| 50 |enemy |area      |10          |4  |8       |     |         |Fireball
Heal    | 30 |ally  |direct    |-20         |   |9       |     |         |Sparkles
Curse   | 10 |enemy |direct    |            |   |12      |AC   |-2       |Skull

Export the spreadsheet to CSV, and parse it in Unity to create the skills. This means your CSV parser/skill builder will have to know how to handle every type of condition and effect.

I’m throwing this out there as an option because, in the long run, you’ll probably spend most of your time building and tweaking skills after writing the underlying code, so it’s good to make the workflow as easy as possible.

1 Like

I can share some of my experiences from a past project:

  • We’ve separated data (ScriptableObjects) from Behaviour (Components), because often, some values were shared by different skills or users, but a lot of code requires access to scene references (basically GetComponent calls or access to Transform and Colliders).

  • Another pattern would be to pass references to skill objects (maybe the caster and list of affected GameObjects and let each skill decide how to retrieve additional references), or use a hub-component, which provides several references.

  • Skill usually have to be updated and access the scene, therefore we had manager components which would update a list of active abilities on the caster (to check for input and conditions), but also on every entity, that could be affected by skills.

  • We’ve added ability effects (e.g. poison damage, shield for x seconds, etc) to a component on the affected unit. This would then update the currently active effects every frame, add everything up and resolve the final outcome per frame or given tick time. This is important for over-time-effects, since you don’t want the original caster to be performing work applying the desired effect each time, since he might be deleted etc (depends on the game, but think MOBA damage over time). The ability-effect-resolver can be very simple (as in our case), and just pass simple math instructions from one effect to the next and then apply the result per type like this:

  • Caster adds ‘Poison’ to Character A’s list of effects

  • Character A already has ‘Shield’ applied to it

  • Character A updates the list every frame

  • The poison effect checks every second and forwards the type “ApplyDamage” with a value of 0.1. This can either be just a simple enum or could be it’s own complex object with behaviour, but all in all it’s just an objectified delegate (method/instruction as an object which can be passed around)

  • The shield effect receives ApplyDamage and subtracts it’s own shield value from it, then returns it

  • The manager component has reached the end of the ApplyDamage loop and sends the final effect to the DamageResolver, where the final value gets applied to the corresponding health component, visual FX are triggered, etc.

  • From time to time, you can check your design by removing or adding one of the abilities from the game. You should only have to delete files and change one part of the code. In our case, when removing poison from the game, no code needs to change, only the inspector data is deleted. When removing the entire damage system, only the DamageResolver needs to be removed from the skill behaviour manager. Of course, basic behaviour like ‘over time’ or ‘apply one time’ are always part of the framework and the manager system of updating skill state was never changed much.

  • Generally, each hero unit in our game had a set of components which provided the literal ability to do something like “ShootProjectile” or “HealAlly”. Units, which could receive certain effects had components like “Health” or whatever could be changed by a skill. If units shared the same ability, the components where duplicates to both prefabs, but their shared data was moved into a ScriptableObject. Per-prefab data still stayed on the component. This worked fine, but lead to quite some changes. Probably a good idea to use a more generic system, where a value can either be stored locally or as an asset, without having to move the variables (and serialized data) around a lot.

2 Likes