Ok, I went and implemented this in my current project, using what I like to call “Follow the Errors”…
First, the easy part was adding a skill to WeaponConfig along with a GetSkill() method that returns the skill associated with that WeaponConfig.
Next, in Fighter.cs in Hit(), I added WeaponConfig.GetSkill() to the end of the TakeDamage routine.
if (currentWeaponConfig.HasProjectile())
{
if (currentWeaponConfig.GetAmmunitionItem())
{
GetComponent<Quiver>().SpendAmmo(1);
}
currentWeaponConfig.LaunchProjectile(rightHandTransform, leftHandTransform, target, gameObject, damage);
}
else
{
target.TakeDamage(gameObject, damage, currentWeaponConfig.GetSkill());
}
You’ll note that I didn’t add this to currentWeaponConfig.LauchProjetile because currentWeaponConfig already knows it’s skill.
At this point, the compiler started complaining that TakeDamage doesn’t have a method signature that matches this one. I’m using JetBrains Rider, and it offered to add the Skill skill to the TakeDamage method, so I did. This resolved the Fighter error, and we’re done in Fighter altogether.
Off we go to TakeDamage(), where I added “,skill” to the AwardExperience method
public void TakeDamage(GameObject instigator, float damage, Skill skill)
{
healthPoints.value = Mathf.Max(healthPoints.value - damage, 0);
if(IsDead())
{
onDie.Invoke();
AwardExperience(instigator, skill);
GetComponent<ActionScheduler>().CancelCurrentAction();
}
else
{
takeDamage.Invoke(damage);
}
UpdateState();
}
Now at this point, the compiler is complaining that AwardExperience doesn’t have a skill parameter, and Projectile is complaining that it’s not adding skill to the TakeDamage.
First, to AwardExperience, which was easy. Since I’m not trying to completely break my existing project, I chose to award both regular experience and skill experience (this is actually a common practice in many skill based games, but can choose to ignore regular experience)
private void AwardExperience(GameObject instigator, Skill skill)
{
if (instigator.TryGetComponent(out Experience experience))
{
if (instigator.TryGetComponent(out Health otherHealth) && !otherHealth.IsDead())
{
experience.GainExperience(GetComponent<BaseStats>().GetStat(Stat.ExperienceReward));
}
}
if (instigator.TryGetComponent(out SkillExperience skillExperience))
{
if (instigator.TryGetComponent(out Health otherHealth) && !otherHealth.IsDead())
{
skillExperience.GainExperience(skill, Mathf.RoundToInt(GetComponent<BaseStats>().GetStat(Stat.ExperienceReward)));
}
}
}
Now to Projectile.cs, which is complaining about not having a skil parameter passed along to TakeDamage. First off, Projectile needs a skill to use in the first place, so when we look at Projectile, we see that all of the data it needs is passed in through WeaponConfig.Spawn, and that there are actually THREE SetTarget methods. Two are pass throughs to the main one. This allows for location and target based shooting… so I added the Skill to each of the SetTarget() methods and in the two pass through methods, I passed along the skill to the main method. I also added a global Skill skill; where I declared damage, instigator, and target.
public void SetTarget(Health target, GameObject instigator, float damage, Skill skill)
{
SetTarget(instigator, damage, target, Vector3.zero, skill);
}
public void SetTarget(Vector3 targetPoint, GameObject instigator, float damage, Skill skill)
{
SetTarget(instigator, damage, null, targetPoint, skill);
}
public void SetTarget(GameObject instigator, float damage, Health target=null, Vector3 targetPoint=default, Skill skill = Skill.Ranged)
{
this.target = target;
this.targetPoint = targetPoint;
this.damage = damage;
this.instigator = instigator;
this.skill = skill;
Destroy(gameObject, maxLifeTime);
}
Now this immediately caused the compiler to complain that two more scripts don’t have the right calls… so I followed the breadcrumbs… First to Weaponconfig’s LaunchProjectile. Since the WeaponConfig already knows the skill, this was a relatively quick fix, just adding GetSkill() to the SetTarget method
public void LaunchProjectile(Transform rightHand, Transform leftHand, Health target, GameObject instigator, float calculatedDamage)
{
Projectile projectileInstance = Instantiate(projectile, GetTransform(rightHand, leftHand).position, Quaternion.identity);
projectileInstance.SetTarget(target, instigator, calculatedDamage, GetSkill());
}
At this point, we still have two more scripts complaining, and it’s something I hadn’t thought of earlier… These are our Ability scripts… HealthEffect and SpawnProjectileEffect…
HealthEfffect was easy… I could have added a SerializeField to the HealthEffect, but I chose instead to hard code Skill.Casting to the TakeDamage call
public override void StartEffect(AbilityData data, Action finished)
{
foreach (var target in data.GetTargets())
{
var health = target.GetComponent<Health>();
if (health)
{
if (healthChange < 0)
{
health.TakeDamage(data.GetUser(), -healthChange, Skill.Casting);
}
else
{
health.Heal(healthChange);
}
}
}
finished();
}
That just leaves SpawnProjectileEffect, which was a little trickier because in this method, we could be casting a target point or a physical target. In both cases, I chose Skill.Casting but again, you could make this a SerializeFied
private void SpawnProjectileForTargetPoint(AbilityData data, Vector3 spawnPosition)
{
Projectile projectile = Instantiate(projectileToSpawn);
projectile.transform.position = spawnPosition;
projectile.SetTarget(data.GetTargetedPoint(), data.GetUser(), damage, Skill.Casting );
}
private void SpawnProjectilesForTargets(AbilityData data, Vector3 spawnPosition)
{
foreach (var target in data.GetTargets())
{
Health health = target.GetComponent<Health>();
if (health)
{
Projectile projectile = Instantiate(projectileToSpawn);
projectile.transform.position = spawnPosition;
projectile.SetTarget(health, data.GetUser(), damage, Skill.Casting);
}
}
}
And that’s it. It took longer to type all this out than it did to factor in the changes.
This BreadCrumb approach is something used often in refactoring. One change often leads to another change, etc.
In terms of the Skill itself being passed around, this approach lets us pass that information to the only method that actually acts on it (Health.AwardExperience) without reaching back.
I also want to point out that the approach I just used only adds ONE dependency to most of the scripts… and that’s RPG.Skills, just to get the definition for the enum Skill. Health is the only component that relies on another new component, as it must call the SkillStore to award the experience. Otherwise, the pre-existing dependencies (Fighter depends on WeaponConfig, WeaponConfig depends on Projectile, etc) are completely unchanged.