Skills based system

Well… And it also cuts the animation out when the player finished resource gathering, but when it’s placed in the right spot xD

Respectfully… NOPE, JUST NOPE. DO NOT SET THE TARGET TO NULL WHEN THE RESOURCE DIES. ONLY DO SO IN ‘IAction.Cancel()’. Doing so when the resource dies will only tell the player not to target anything else the mouse points on, making him unmovable (P.S: I tried it, it did exactly what I described)

[NEW TOPIC, but relevant to skills, hence why it’s here]

hi again @bixarrio and @Brian_Trotter. Something I noticed during combat, considering that we shifted the whole game to a ‘Skill-based system’, is that no matter what you do so far, your final XP runs to whatever XP is relevant to which weapon you used to cast the final blow. In other words, you can have a ranged fight that is 99% done in ranged, but the last 1% is done in attack, then attack gets all the experience, and ranged gets nothing… kind of like the kid that gets all the credit in high school, but originally he copied the smart guy’s homework, and the smart guy unfortunately got no credit for his work

To counter this, I want to develop an algorithm that keeps track of what weapon did what damage, and average the damage out in the end, for each combat skill (the skills will be attack, ranged and magic). The Skill that dealt the most damage will get the XP, the rest gets nothing. Any suggestions on how can I start working on this? (I know this is not a topic that is as easy as it sounds)

Edit: OK so here’s what I tried doing, but this was a complete, utter failure:

  1. I created a new script, called ‘DamageComponent’, which starts with a private dictionary, which I attached to my player, and that script contains two functions.

The first is a dictionary, which I called ‘DealDamage()’, which adds damage dealt to a dictionary responsible for carrying the damage dealt by the player

The second function was known as ‘GetMostDamagingSkill()’, and that one accumulates the damage of the values in the dictionary, adds them up, and returns the mostDamagingSkill, and both functions are shown below:

using System.Collections.Generic;
using RPG.Skills;
using UnityEngine;

namespace RPG.Combat {

public class DamageComponent : MonoBehaviour {

    private Dictionary<Skill, float> damagePerSkill = new Dictionary<Skill, float>();

    public void DealDamage(Skill skill, float damage) {

        if (damagePerSkill.ContainsKey(skill)) {
            damagePerSkill[skill] += damage;
        }
        
        else damagePerSkill.Add(skill, damage);

    }

    public Skill GetMostDamagingSkill() {

        Skill mostDamagingSkill = Skill.None;    // I know this is supposed to be null, I'm just not sure how to create a null Skill type... the compiler returns errors
        float maxDamage = 0;

        foreach (var keyValuePair in damagePerSkill) {

            if (keyValuePair.Value > maxDamage) {
                maxDamage = keyValuePair.Value;
                mostDamagingSkill = keyValuePair.Key;
                }

            }

        return mostDamagingSkill;

        }

    }

}
  1. Next up, I introduced a variable called ‘playerDamageComponent’, which I initialized in ‘Awake()’, and the function known as ‘GetDamage()’ in ‘Fighter.cs’ about a week ago was tuned, and this function was responsible for splitting up the XP to reward whoever had the final hit the experience needed, and it’s eventually called in ‘Fighter.Hit()’. I tuned this function a little bit, to compensate for the new function, as shown below:
public DamageComponent playerDamageComponent;

private void Awake() {
// some code here...
playerDamageComponent = GetComponent<DamageComponent>();
// some other code...
}

public int GetDamage() {
        if (currentWeaponConfig.GetSkill() == Skill.Attack) {
            // return (int)(GetComponent<BaseStats>().GetStat(Stat.Attack) + (currentWeaponConfig.GetDamage() * (1 + currentWeaponConfig.GetPercentageBonus() / 100)));
            int attackDamage = (int) (GetComponent<BaseStats>().GetStat(Stat.Attack) + (currentWeaponConfig.GetDamage() * (1 + currentWeaponConfig.GetPercentageBonus()/100)));
            playerDamageComponent.DealDamage(Skill.Attack, attackDamage);
            return attackDamage;
        }
        else if (currentWeaponConfig.GetSkill() == Skill.Ranged) {
            // return (int) (GetComponent<BaseStats>().GetStat(Stat.Ranged) + (currentWeaponConfig.GetDamage() * (1 + currentWeaponConfig.GetPercentageBonus()/100)));
            int rangedDamage = (int)(GetComponent<BaseStats>().GetStat(Stat.Ranged) + (currentWeaponConfig.GetDamage() * (1 + currentWeaponConfig.GetPercentageBonus() / 100)));
            playerDamageComponent.DealDamage(Skill.Ranged, rangedDamage);
            return rangedDamage;
            }
        else {
            // return (int) (GetComponent<BaseStats>().GetStat(Stat.Magic) + (currentWeaponConfig.GetDamage() * (1 + currentWeaponConfig.GetPercentageBonus()/100)));
            int magicDamage = (int)(GetComponent<BaseStats>().GetStat(Stat.Magic) + (currentWeaponConfig.GetDamage() * (1 + currentWeaponConfig.GetPercentageBonus() / 100)));
            playerDamageComponent.DealDamage(Skill.Magic, magicDamage);
            return magicDamage;
            }
        }
  1. Finally, in ‘Health.AwardExperience()’, I tried to award the experience to whoever did the most damage, as shown below:
        private void AwardExperience(GameObject instigator, Skill skill, Skill otherSkill = Skill.Defence) {

            if (instigator.TryGetComponent(out SkillExperience skillExperience)) {

                if (instigator.TryGetComponent(out Health otherHealth) && !otherHealth.IsDead()) {

                    // This line sends 2/3rd of the XP Reward to whichever skill is associated to the Players' weapon:
                    // skillExperience.GainExperience(skill, 2*Mathf.RoundToInt(GetComponent<AIController>().GetXPReward()/3));
                    skillExperience.GainExperience(GetComponent<DamageComponent>().GetMostDamagingSkill(), 2*Mathf.RoundToInt(GetComponent<AIController>().GetXPReward()/3));
                    // This line sends 1/3rd of the XP Reward to the defence XP, which is always 1/3rd of whatever the AI Reward XP is assigned to:
                    skillExperience.GainExperience(otherSkill, Mathf.RoundToInt(GetComponent<AIController>().GetXPReward()/3));

                }

            }

        }

These are all the changes I tried doing, but the system somehow still failed. Where did I go wrong…?

Try using actionScheduler.CancelCurrentAction() (though setting the target to null should do the trick, since the Update() loop should be starting with if(target==null) return;

It might make more sense to have this component on each enemy. The enemy is the one taking the damage, and if you do some damage to enemy A, and then for tactical reasons switch to enemy B, you don’t want to comingle their hit stats…

I’m not entirely sure we really need a new class for this purpose… this may be something we can manage right in Health itself…

        private Dictionary<Skill, float> damagePerSkill = new Dictionary<Skill, float>();

        private void AddDamage(Skill skill, float damage)
        {
            if (!damagePerSkill.ContainsKey(skill)) damagePerSkill[skill] = damage;
            else damagePerSkill[skill] += damage;
        }

        private Skill GetMostUsedSkill()
        {
            return damagePerSkill.OrderByDescending(s => s.Value).First().Key;
        }

Then TakeDamage() can handle dispensing the damage…
After making any damage adjustments (if any), add

AddDamage(skill, damage);

Finally, when awarding experience, pass in GetMostUsedSkill()

AwardExperience(instigator, GetMostUsedSkill());

Yup, that worked beautifully. Thank you again Brian :slight_smile: (I’m still struggling to understand dictionaries a bit, how to get and assign values context is something I had in my head for a moment… then it vanished. I’ll re-read this to properly get an idea)

This is a fairly standard pattern for adding to a counter in a Dictionary. The first line checks to see if the key is in use, and if it isn’t, it sets the value. if it is in use, then we add to it. I think you are familiar with that pattern, as I see you used it above.

This one is a little bit more complicated, I’ll confess. This takes advantage of the fact that Dictionaries implement an IEnumerable pattern that returns a KeyValuePair with the Key and Value types matching the Dictionary. So I applied a Linq expression to Dictionary. The tricky part of Linq expressions is that a good understanding of SQL is important to understand them.
This particular expression sorts the entries in DamagePerSkill by the value from Highest to Lowest. (OrderByDescending(s=>s.Value), Then the first value (which will be highest is returned (First()), and finally, the Key from the First is returned.

Frankly speaking, when I was trying this out on my own at first, the first thing I thought of was populating a dictionary for some reason, as it seemed like the easiest approach (keys that contain corresponding values, so…), so I went and tried copying some stuff from ‘SkillExperience.GainExperience()’

For this one, I took a wild guess that it utilized the fact that we have a dictionary that’s already filled by ‘AddDamage()’, as it was called AFTER ‘GetMostUsedSkill()’ in ‘Health.TakeDamage()’, and then when I saw how you went around it, I suddenly remembered what we did in the inventory… where we got the value of the items to know what stuff to keep on the players’ death. The only difference here is, we are getting the top of the list, as that’s what caused the most damage (but I didn’t know that we can access a dictionaries’ key through the keyword ‘Key’ in the end). Quite an interesting approach you got there

P.S: I never learned SQL, as I’m not a computer scientist :sweat_smile: - just trying to develop a passion project here, and gaining a lot of coding XP along the way

btw, whenever you have some time, please have a look at my minimap issue :slight_smile: (before we head back to the bank issue)

Well, then you have a bug somewhere because this is exactly what Fighter does, and that works. When the enemy you are attacking dies, it tries to find a new enemy in the vicinity and if it can’t, target is set to null. Attack over.

I think you may be looking at these skills from the wrong side (unless this is really what you want).

I personally wouldn’t award skill xp when the enemy dies. I’d rather award a small amount of xp each time a skill is successfully used. If I shoot the enemy with my bow and hit, my bow skill is awarded +1xp. If I then switch to a sword and whack him 2 more times, my sword skill gets +1xp for each hit. If I miss a swing, 0xp. If the enemy took 10 hits to kill and I shot him 4 times and hit him 6 times, my bow skill will have received +4xp and my sword skill +6xp. No need to keep track of which weapon was used the most. The amount of xp awarded is not linked to any enemy. It’s just 1 (or some other small value you would like to use). Killing an enemy that takes 10 hits to kill with a sword will result in a total of 10xp gained for that enemy. An enemy that takes 25 hits to kill will award a total of 25xp. And all this xp is neatly distributed across the skills I used to defeat said enemy.

If you do want to only award xp after the fight, I’d go with what Brian said and you’d have to remember that in some cases (like the 10 hit enemy above) you may have shot him 5 times and hit him 5 times and now you have a tie for the top spot. Which one gets the xp? Do you share? Does it not matter and you go with whichever the algorithm returns? Basically, there’s a huge chance that the weapon you used first will be the one that gets returned; 2 shots with bow, 3 hits with sword, 3 shots with bow, 2 hits with sword - tie for first, bow wins the xp because bow is first in the dictionary.

I just don’t want my player gaining XP in a battle he lost, so my solution was just to give off the xp ONLY IF HE WINS (what’s the point of getting XP if you’re losing the fight…? At least that’s how I want it in my game). For the case of two weapons that did exactly the same amount of damage, then the rewarding XP will be for the one that started it first. So if, for example, your ranged weapons did a total damage of 50, and your melee weapons also did a total damage of 50, but the ranged weapons started the fight/came up first, then the ranged weapons will get the XP (believe me, I have no idea how to implement that last part, but I’m down to give it some thoughts…)

It’s not xp for winning a fight. You don’t have that system anymore. You have a skill based system that gives xp for using a skill.

I just told you, it’s automatic. The first weapon will be first in the dictionary and therefore the one that will be returned

For combat, at least in my case, the skill is done when the player wins the fight… Any skill you basically only get the experience if you successfully accomplish the task, when you get the reward (ores for mining, logs for woodcutting, drops on the enemies’ death in combat, etc). A fight in progress is not a task done, it’s a task-in-progress, hence why I don’t want to give the player any XP yet :stuck_out_tongue_winking_eye: (otherwise I’d be giving the player woodcutting xp just for swinging a hatchet, mining xp for picking a rock but not getting resources, etc… Not the approach my game is doing tbh)

I have learned more from my failures than I have ever learned from my successes… In a skill based system, you should gain skill points by using the skill, even if the skill did not succeed. I spent nearly a year trying to play Rush’s La Villa Strangiato both on guitar and on bass. There are still sections I cannot play. In other words, technically, I failed. I did not slay the 8 minute masterpiece instrumental. But along the way, I learned incredible techniques that were used by Alex and Geddy, and it made me a MUCH better guitarist and bassist. If there was a Skill.GuitarPlaying and a Skill.Bass, I earned 50 levels in each one without ever accomplishing playing the whole thing without a flaw…

But the essential act of cutting a tree begins and ends with swinging a hatchet… The essential act of mining begins and ends with swinging a pick… Something to consider.

You could take the first result of the damage dictionary’s Keys property (an IEnumerable in this case).

private Skill GetFirstUsedSkill() => damagePerSkill.Keys.FirstOrDefault();

I don’t mean to come along as passive aggressive or defensive, but the whole idea was to make the game a little more difficult by doing so, by keeping the reward at the end. In other words, rewarding the player with XP in the end gives me more freedom to calculate how much experience they deserve for this fight, to ensure a smooth gameplay, or at least that’s my thought for now :slight_smile:

thank you for this as well by the way :slight_smile:

I think @bixarrio and I understand this quite well. Just pointing out a way that is actually used in many skills based systems.

Just remember you would only use this in the event that there was a tie between two skills… that being said, my original formula would not tell you if there was a tie, you would have to manually tally the values and check for a tie, then call this…
That being said, I’m 99.995% positive that the original query would return the first instance of damage in the event of a tie.

There is one other formula that might “spread the wealth” a bit…

         private void AwardExperience()
         {
                if (instigator.TryGetComponent(out SkillExperience skillExperience))
            {
                if (instigator.TryGetComponent(out Health otherHealth) && !otherHealth.IsDead())
                {
                    float totalAward = GetComponent<BaseStats>().GetStat(Stat.ExperienceReward);
                    float totalDamage = damagePerSkill.Values.Sum();
                    foreach (var pair in damagePerSkill)
                    {
                        float relativeValue = pair.Value / totalDamage;
                        skillExperience.GainExperience(pair.Key, Mathf.RoundToInt(totalAward/relativeValue));
                        
                    }
                }
            }
        }

This would divide the experience amongst all of the skills used, but still wait until the enemy has been well and truly slain to give experience.

Just a quick question on the fly… if ‘many skills based systems’ use it, don’t you think making it a little different makes for a good selling point? Again, I’m just looking for and listening to different perspectives here

For this one, I honestly replaced the new line you provided me with with the old one, so now my ‘GetMostUsedSkill()’ is like this (I renamed it to ‘GetMostDamagingSkill()’):

private Skill GetMostDamagingSkill() {
// return damagePerSkill.OrderByDescending(s => s.Value).First().Key;
return damagePerSkill.Keys.FirstOrDefault()
}

As I thought it was a replacement

As for the ‘AwardExperience()’ reward, my formula is slightly different, as I assigned each enemy his individual reward, rather than relying on the progression system, as I want various enemies, at various levels, to reward the player with various levels of experience, so mine right now will look like this:

private void AwardExperience(GameObject instigator, Skill skill, Skill otherSkill = Skill.Defence) {

            if (instigator.TryGetComponent(out SkillExperience skillExperience)) {

                if (instigator.TryGetComponent(out Health otherHealth) && !otherHealth.IsDead()) {

                    // This line sends 2/3rd of the XP Reward to whichever skill is associated to the Players' weapon:
                    skillExperience.GainExperience(skill, 2*Mathf.RoundToInt(GetComponent<AIController>().GetXPReward()/3));
                    // This line sends 1/3rd of the XP Reward to the defence XP, which is always 1/3rd of whatever the AI Reward XP is assigned to:
                    skillExperience.GainExperience(otherSkill, Mathf.RoundToInt(GetComponent<AIController>().GetXPReward()/3));

                }

            }

        }

This is before I tuned the function (again, I split my XP to 66% goes to the most damaging skill, and 33% goes to defence, so by default defence is now always being trained, and I might compensate for this benefit in other ways, like making levelling a little harder perhaps). After tuning, I believe it will probably look like this:

private void AwardExperience(GameObject instigator, Skill skill, Skill otherSkill = Skill.Defence) {

            if (instigator.TryGetComponent(out SkillExperience skillExperience)) {

                if (instigator.TryGetComponent(out Health otherHealth) && !otherHealth.IsDead()) {

                    float totalAward = GetComponent<AIController>().GetXPReward();
                    float totalDamage = damagePerSkill.Values.Sum();

                    foreach (var pair in damagePerSkill) {
                        float relativeValue = pair.Value / totalDamage;
                        skillExperience.GainExperience(pair.Key, 2*Mathf.RoundToInt(totalAward/relativeValue)/3);
                    }

                    // This line sends 2/3rd of the XP Reward to whichever skill is associated to the Players' weapon:
                    // skillExperience.GainExperience(skill, 2*Mathf.RoundToInt(GetComponent<AIController>().GetXPReward()/3));
                    // This line sends 1/3rd of the XP Reward to the defence XP, which is always 1/3rd of whatever the AI Reward XP is assigned to:
                    skillExperience.GainExperience(otherSkill, Mathf.RoundToInt(GetComponent<AIController>().GetXPReward()/3));

                }

            }

        }

In testing though, the mathematical values are completely out-of-whack, and I have no idea why (P.S: I want to keep this formula commented for now, I might want to swap systems later down the line… for now though, what’s wrong with it?)

That would, effectively, grant experience for the FIRST attack used, and wouldn’t be any fairer than using the LAST attack used…

Note that you have cached totalReward at the beginning of the if block. You should take advantage of that when dealing with the defense gain, and use Mathf.RoundtoInt(totalReward/3)

Can you be more specific. Provide examples of what is out of whack…
You might start by logging out each of the values as they are calculated.

Well… I did punch a guy a small punch, relevant to his total health, and somehow get 295 XP, when the enemy should not be giving me more than 20XP combined… I’ll test the mathematics for this again

How do you check for a tie though, do you just write this line in ‘GetMostDamagingSkill()’? :

private Skill GetMostDamagingSkill() {

return damagePerSkill.OrderByDescending(s = > s.Value).First().Key;

if (/*what do I write here, to compare between dictionary values...?*/) {
return damagePerSkill.Keys.FirstOrDefault
}
}

Any chance this punch was the last punch?
One thing I like to do is to change the damage from a killing blow to be the amount of health the big bad has left…
For example:

        public void TakeDamage(GameObject instigator, float damage, Skill skill)
        {
            damage = Mathf.Min(damage, healthPoints.value);
            healthPoints.value = Mathf.Max(healthPoints.value - damage, 0);

(P.S: Please check my previous comment, it’s edited) It’s not just the punch, it’s generally the entire system has been multiplied by something I guess… For instance, half the damage I did was in ranged, and the other half was in magic. Although my total XP was supposed to be 20, that is 7 for defence and 13 for the other two, somehow my magic got 30 XP (I’m guessing it did slightly more damage) and my Ranged got 24 XP, which is a total of 54 XP, 41 more experience points above my expected 13 XP points… is that foreach loop responsible for something…?

After a little more testing, the attack is specifically buffed out for some reason… because it did less than 40% of the total damage and somehow got 40 XP on its own…

This won’t get you there…
In fact, my original damagePerSkill.Keys.First would fail because you could start with one magic punch, and then do an equal amount of Melee and Ranged, and these would be the winner…

You’d have to have yet another tracking system

private List<Skill> damageOrder = new List<Skill>();

        private void AddDamage(Skill skill, float damage)
        {
            damageOrder.AddUnique(skill); //This will make a list in order from the first skill used to the last
            if (!damagePerSkill.ContainsKey(skill)) damagePerSkill[skill] = damage;
            else damagePerSkill[skill] += damage;
        }

       private Skill GetMostUsedSkill()
       {
            var pairs = damagePerSkill.OrderByDescending().ToList<>();
            if(pairs.Count==1) return pairs.Key;
            if(pairs[0]==pairs[1])
            {
                 for(int i=-;i<damageOrder;i++)
                 if(damageOrder[i] == pairs[0].Key) return pairs[0].Key);
                 if(damageOrder[i] == pairs[1].Key) return pairs[1].Key);
            }
            return pairs[0].Key; //It should never get here, but won't compile without it
       }

Here is where you gain experience from my error… :slight_smile:
The way to allocate proportion in a reward based on a proportion in effort is

ratio = effort/totalEffort;  (this will always be a value between 0 and 1, it's a percentage
reward = totalReward * ratio; (as Ratio is a percentage of the effort, award that percentage

Hence my error… That line should have read

 skillExperience.GainExperience(pair.Key, Mathf.RoundToInt(totalAward*relativeValue));

Privacy & Terms