- Home /
Spells and abilities system
Hi. Have some trouble with abilities architecture. I started with base class
class Ablity{
public int mana_cost;
public int cool_down;
}
class DirectDamageAbilit:Ability{
virtual int calc_damage(){}
}
let's say I've got 3 abilities : Swift Strike, Power Strike, Calculated strike. They share common pattern : got mana cost, cooldown, and damage output. However the method calc_damage is different for each one (first scale with dexterity, second with strength, last with weapon damage). So, what is the best way to organize this? Simplest decision is to inherit :
class SwiftStrike:DirectDamageAbility{
override int calc_damage(){}
}
But I will end with 40++ files, one for each ability. It doesn't look like good idead to me. I can't see how ScriptableObject can help here, because of dynamic function for damage calcalution.
Having one class per ability is not a problem and I would encourage to go this way.
Depending on the size of the classes, you can gather them in the same file since they are plain C# classes.
This is quite a standard pattern. I often end up with a 1 to 1 relationship of classes to ScriptableObject instances, simply because each instance of (in this case an ability) needs to override a certain method.
In general, having a scriptable object per ability is quite a good position to be in. It'll let you directly reference abilities through inspectors, as well as easily create variations (eg. a fireball which uses the same class as the other fireball, just with a lower cost and lower damage).
Answer by BradyIrv · May 07, 2020 at 12:39 PM
You'll still end up with 40+ Scriptable files with this method, but to clarify your last concern you could use a scriptable and still have a dynamic function for the damage calculation. But it could help with organizing a bucket load of abilities in the project and simplify balancing them.
.
The benefit to this is that Scriptable objects can be easily created and Ability will just become a script you can slap on when you have a character that needs another ability, as opposed to creating a class for each one. Also it gives you access to change damage,cast times, mana cost...etc in the project window for very quick balancing.
.
Also note that I added a public string id to the scriptable object. You could create a Singleton Instance for an AbilityManager that has a public Dictionary< string,Ability>(), and then reference the Manager whenever you need an ability in the game from a character or mob. (Instead of having the abilities on the object directly). This will help manage the large amount of files!
.
So, the scriptable:
[CreateAssetMenu(filename = "New Ability", menuName = "Ability")]
class AbilityData : ScriptableObject
{
public string id;
public int baseDamage;
public int elementalDamage;
[0,100] public float critChance;
public int damageMultiplier;
public int manaCost;
public int cooldown;
public GameObject castEffectPrefab;
public GameObject hitEffectPrefab;
public GameObject critEffect;
// I'll put this here but you'll probably want the enum publicly reference-able elsewhere
public enum ElementalDamageType { Lightning, Fire, Ice, Bludgeoning, Slashing }
public ElementalDamageType myElementalDamageType;
}
And then the ability class which you can drag onto an empty gameObject in heirarchy either on to the player/enemy directly or into an AbilityManager.
class Ablity : MonoBehaviour
{
public AbilityData data;
private bool weCrit;
// If AbilityManager
private void Start()
{
// Make sure we don't add the same ability twice
if(AbilityManager.Instance.Abilities.Contains(data.id) == false)
AbilityManager.Instance.Abilities.Add(data.id, this);
else Debug.LogError("[Ability] Attempted to add a duplicate of " + data.id);
}
// What to do if we Cast
public int Cast(/*params CastPoint, Caster*/)
{
if(Player.totalMana > data.manaCost)
{
Caster.totalMana -= data.manaCost;
Instantiate(data.castEffectPrefab, CastPoint.pos, CastPoint.rot);
return data.cooldown;
}
else return 0;
}
// What to do if we hit something
public int OnHit(/*params TransformHit, HitResistances, Caster*/)
{
int Damage = CalculateDamage(HitResistances, Caster);
Instantiate(data.hitEffectPrefab, TransformHit.pos, TransformHit.rot);
if(weCrit) Instantiate(data.critEffectPrefab, TransformHit.pos, TransformHit.rot);
// Or maybe a sparkle on the caster
//Instantiate(data.critEffectPrefab, Caster.transform.pos, Caster.transform.rot);
weCrit = false; // reset
return Damage;
}
// Example Calculation
private int DamageCalculation(/*params Resistances, Caster*/)
{
// int damage = data.baseDamage;
// if not resistant to data.myElementalDamageType
// damage += resultantElementalDamage;
// else Calculate residual damage after resistance from elemental?
// float baseCritChance = data.CritChance
// Maybe add some crit chance from items: baseCritChance += Caster.CritFromItems;
// if(Random.Range(0, 100) < data.critChance)
// damage *= damageMultiplier;
// weCrit = true;
// else didn't crit
return damage;
}
}
Thank you. That clarify a lot. However, I still don't get how to make each ability calc damage logic different for different abilities? I mean, variables like damage, cooldown and etc can be configured through AbilityData but not damage calculation? For example "firebolt" must scale with caster.Int, and "bash" scales with weapon damage and caster.strength
I decided to end with such structure. For example, firebolt wiill be ability with carrier FireBoltPrefab, which has effects [Damage, Burn] which both scales with character method "GetIntBonus" * 0.75 (weight).
Hmmm that's a good point, I didn't account for statistics of the Caster (int or str), but now that I understand what you mean by dynamic, maybe this could help:
.
You could create default params in the calculations and override them when making the call from the specific caster. So basically if a caster has 12 intelligence, wants to shoot a firebolt, and you calculate 12 intelligence to add 12% damage, you can pass "1.12" as the ability$$anonymous$$od to the calculation. If a warrior somehow casts a fireball and has negative intelligence, you could also do things like pass ".80" which will reduce the base damage by 20%. If no mod is applied then it will just use the default base damage of the spell.
.
Ability $$anonymous$$anager could include a function for handling this stat modification, to make things easier for each call.
.
Also note that now we've included another float calculation, so you'll likely want to change damage, crit...etc to float values to not lose information in the calculation.
// Example Calculation
private int DamageCalculation(/*params Resistances, Caster, ability$$anonymous$$od = 1*/)
{
// int damage = data.baseDamage;
damage *= ability$$anonymous$$od;
// if not resistant to data.myElementalDamageType
// damage += resultantElementalDamage;
// else Calculate residual damage after resistance from elemental?
// float baseCritChance = data.CritChance
// $$anonymous$$aybe add some crit chance from items: baseCritChance += Caster.CritFromItems;
// if(Random.Range(0, 100) < data.critChance)
// damage *= damage$$anonymous$$ultiplier;
// weCrit = true;
// else didn't crit
return damage;
}
Your answer
Follow this Question
Related Questions
An OS design issue: File types associated with their appropriate programs 1 Answer
Multiple Cars not working 1 Answer
Distribute terrain in zones 3 Answers
if object is destroyed 2 Answers