Code Review Request

Hello again fellas. So… I got myself into a bit of a funky problem :sweat_smile: (which is probably miles away from the RPG Course, but that’s as close as I can get)

Ever since I introduced NPC fighting systems, I had a problem with the old combat formula, as it would always only get the total experience when the enemy dies if the player is the one who did the last hit, which, as you can guess, is not ideal for cases where multiple enemies are involved in attacking a victim. So, I tried to re-write the ‘AwardExperience()’ algorithm (and since my game has a ‘Skilling’ system similar to RuneScape and Valheim, I had to include an ‘AddDamage()’ function for that too)

Here’s all the changes I made in ‘Health.cs’ to help fix my combat problem, followed by an explanation of what’s going on:

First, the new ‘AwardExperience()’ algorithm:

        /// <summary>
        /// The sixth, and final version of 'AwardExperience', is the most intelligent version to date. Essentially, it will
        /// use a dictionary to get the skills used by the player, to split the experience accordingly. Next, it will also
        /// investigate how much damage, out of the total damage dealt by the player and other NPCs to the victim, has been dealt
        /// by the player, so that it can cut the total experience provided by the enemy apart, and only give the player a total amount
        /// of experience that correlates to the total damage he has contributed to killing this victim
        /// </summary>
        private void AwardExperience()
        {
            // -------------------------------------------------------------------------------------------------------
            // if you want to stop cutting the total experience to give the player a fraction, and give the player
            // full experience regardless of how much damage he has done to the NPC, when the NPC dies, then
            // plug this line into the 'AddDamage()' function with 3 parameters:
            // if (!instigator.CompareTag("Player")) return;
            // -------------------------------------------------------------------------------------------------------

            Debug.Log($"Award experience called");
            AIController aiController = GetComponent<AIController>();

            if (aiController == null)
            {
                // prevents NREs when the player dies, which can lead to respawn issues,
                // and it also triggers only when the player dies 
                // (so nobody gets any experience killing the player)
                Debug.Log($"AIController not found");
                return;
            }

            float totalReward = aiController.GetXPReward(); // don't ask me why, even I'm confused why I don't want to push it out to a new script

            foreach (var participant in damageParticipants)
            {
                GameObject instigator = participant.Key;
                float damageDone = participant.Value;
                Debug.Log($"Instigator: {instigator.name}, Damage done: {damageDone}");
            }

            // gets the damage done by everyone...:
            float totalDamage = damageParticipants.Values.Sum();
            Debug.Log($"Total Damage: {totalDamage}");

            // Only get the damage done by the player:
            float totalPlayerDamage = damageParticipants.Where(participant => participant.Key.CompareTag("Player")).Sum(participant => participant.Value);
            Debug.Log($"Total Player damage: {totalPlayerDamage}");

            if (totalDamage == 0 || totalPlayerDamage == 0) 
            {
                Debug.Log($"No Player damage or total damage found");
                return;
            }

            // Calculate the player's fraction of the total damage:
            float playerDamageProportion = totalPlayerDamage / totalDamage;
            Debug.Log($"Player Damage Proportion: {playerDamageProportion}");

            // Calculate the experience to award to the player, based on their damage proportion:
            int experienceToAward = Mathf.RoundToInt(totalReward * playerDamageProportion);
            Debug.Log($"Experience to award: {experienceToAward}");

            Debug.Log($"Total damage: {totalDamage}, Player damage: {totalPlayerDamage}, Total reward: {totalReward}");
            Debug.Log($"Player damage proportion: {playerDamageProportion}, Experience to award: {experienceToAward}");

            foreach (var participant in damageParticipants)
            {
                GameObject instigator = participant.Key; // everyone involved in damaging the victim
                float damageDone = participant.Value; // the amount of damage by the instigators

                // Check against MissingReferenceException:
                if (instigator == null || instigator.CompareTag("Enemy")) continue;

                // Only process the player, and ignore the enemies:
                if (instigator.CompareTag("Player") && instigator.TryGetComponent(out SkillExperience skillExperience))
                {
                    Debug.Log($"Player Accessed");
                    float totalSkillDamage = damagePerSkill.Values.Sum();

                    if (totalSkillDamage == 0)
                    {
                        Debug.Log($"No skill damage found");
                        return;
                    }

                    foreach (var skillDamage in damagePerSkill)
                    {
                        // Skills used by the player to provoke damage are analyzed here:
                        Skill skill = skillDamage.Key; // the skill that did the damage
                        float skillDamageValue = skillDamage.Value; // the amount of damage done by the skill
                        float skillRelativeValue = skillDamageValue / totalSkillDamage; // the proportion of damage done by the skill, from all used skills

                        Debug.Log($"Skill: {skill}, Skill damage value: {skillDamageValue}, Skill relative value: {skillRelativeValue}");

                        // Two-thirds of all the experience goes to the skills of the weapons involved in the fight:
                        int skillExperienceToAward = Mathf.RoundToInt(2 * experienceToAward * skillRelativeValue / 3); // total XP awarded to player
                        skillExperience.GainExperience(skill, skillExperienceToAward); // provide the XP to the player already
                        Debug.Log($"Skill experience awarded for skill: {skill} with XP: {skillExperienceToAward}");
                    }

                    skillExperience.GainExperience(Skill.Defence, Mathf.RoundToInt(experienceToAward / 3)); // automatically, assign 1/3 of the XP to defence, by default
                    Debug.Log($"Defence experience awarded");
                }
                else 
                {
                    Debug.Log($"SkillExperience component not found on instigator");
                }
            }
        }

Essentially, this Algorithm is supposed to split the total experience that an enemy that you may kill has to offer, based on exactly how much (out of the total damage dealt. I want to swap that for the total health of the victim, but for now my priority is the inconsistent results) the player has done to the enemy in terms of damage, and then split that based on how much damage was done by each skill, which is identified by what weapon you’re holding essentially

Next, this needs an ‘AddDamage()’ to keep track of who participated in dealing the damage, and how much damage each skill has done, as follows:

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

        // TEST - 22/5/2024 (Delete if failed):
        private Dictionary<GameObject, float> damageParticipants = new Dictionary<GameObject, float>();

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


        // TEST - LATE 22/5/2024 (Delete if failed):
        private void AddDamage(Skill skill, float damage, GameObject instigator)
        {
            if (damageOrder.IndexOf(skill) < 0) damageOrder.Add(skill);

            if (!damagePerSkill.ContainsKey(skill) && /* Test --> */ instigator.CompareTag("Player"))
            {
                damagePerSkill[skill] = damage;
            }
            else
            {
                if (instigator.CompareTag("Player") /* This if statement is a test, contents are not */) {
                damagePerSkill[skill] += damage;
                }
            }

            // Extension to the original 'AddDamage' function, to allow the player to get XP based on who he hit
            // (if the test failed, delete this part below, and the 'Skill skill' parameter):
            if (damageParticipants.ContainsKey(instigator))
            {
                damageParticipants[instigator] += damage;
            }
            else damageParticipants[instigator] = damage;
        }

and finally, how it all comes together in ‘Health.TakeDamage()’:

public void TakeDamage(GameObject instigator, float damage, Skill skill = Skill.None)
{
healthPoints.value = Mathf.Max(healthPoints.value - damage, 0);
takeDamage.Invoke(damage);
OnTakenHit?.Invoke(instigator);
OnDamageTaken?.Invoke();
OnDamageInstigator?.Invoke(instigator);

AddDamage(skill, damage, instigator);

if (IsDead()) {
onDie.Invoke();
AwardExperience();
GetComponent<ActionSchedular>().CancelCurrentAction();
UpdateState();
}
else UpdateState();
}

For the most part, this algorithm does what it’s supposed to do. HOWEVER, my major problem here, is inconsistent results. In other words, if another NPC attacks my victim, sometimes I won’t get any experience at all (even though I contributed damage to killing the victim, and sometimes that’s more than what my ally/other enemy has done), and I suspect this has something to do with the logic of ‘AwardExperience()’.

To narrow the problem down, this does not happen (or at least I haven’t seen it) when the player is the only one killing his victim. It only occurs when multiple NPCs are involved in killing the victim, and it’s very much random, with about a 40-60% chance of occuring, and I can’t identify where the problem is coming from

Anyone can please help me identify the problem, and a solution to fix it? It’s been bothering me for a few days now

Again, apologies for going a little too-off topic, and I will appreciate any sort of help I can get :smiley:

I think I solved it… I don’t see the bug anymore, but let’s keep this topic open JUST IN CASE IT HAPPENS AGAIN :sweat_smile::

[EXPLANATION OF THE PROBLEM, ACCORDING TO MY UNDERSTANDING]: So here’s what I realized was causing the problem. As it turns out, it was because of a block in the code, essentially the destroyed enemies (enemies that caused damage, and were killed before their victim died) were not considered for by the list, and when we get to them in the dictionary, it had no idea how to deal with this. So I created a new list, that takes care of these enemies, and destroys them so the dictionary is free of errors

After figuring it out it was because of a failed Debugger (I didn’t even know Debuggers can be sources of problems), I created a ‘trash List’, essentially a List that deletes any killed NPCs that were responsible for hurting an ally, and were killed, which was blocking the flow of code:

            // (TEST) Create a list to hold the keys of destroyed GameObjects (i.e: Destroyed Enemies), which contributed to killing the victim:
            List<GameObject> destroyedParticipants = new List<GameObject>();

and then I updated my list of ‘damageParticipants’ (the record dictionary that holds everyone who damaged the enemy in a list, combined with the damage they did) foreach loop, early in ‘AwardExperience()’, to add the participants, as follows:

            foreach (var participant in damageParticipants)
            {
                GameObject instigator = participant.Key;
                float damageDone = participant.Value;
                
                // (TEST)
                if (instigator == null) 
                {
                    destroyedParticipants.Add(instigator);
                    continue;
                }
                
                Debug.Log($"Instigator: {instigator.name}, Damage done: {damageDone}");
            }

the new stuff is labelled as ‘(TEST)’

and in the end, after that foreach loop, I deleted all the dead enemies almost immediately:

            // Remove the destroyed objects (which contributed damage to the victim enemy) from 'damageParticipants':
            foreach (var destroyedParticipant in destroyedParticipants) 
            {
                damageParticipants.Remove(destroyedParticipant);
            }

and this seems to have fixed the issue. I’ll update this question if anything else happens.

So far so good

This is not how Valheim works. Valheim increases the skill the moment you use it. If you hit with a sword, your ‘sword’ skill is immediately increased. If you jump, your ‘jump’ skill is immediately increased. It doesn’t wait for the enemy to die before the skills are improved. If you do that, all this code can be reduced to just adding player damage to a counter and calculating experience based on the percentage of damage the player did; var awardXP = availableXP * (playerDamage / MaxHealth);

OK I admit, I haven’t played Valheim much… I bought it, and got a little frustrated at first, and gave up on it and returned to coding my game (I gave up on purpose so I don’t get side-tracked, xD)

For me though, I like the idea of ‘give your player the XP they deserve in the end, so they feel rewarded for their hard work’

and I still need to fix a few inaccuracies with my code anyway, because right now the damage is not capped to the enemy’s health, but with how much damage it caused

In other words, you can get 100% of the XP if you hit hard enough, that it overrides the enemy’s health, even though you’re not the only one who hit the enemy :stuck_out_tongue:

Or cheated when they do the work and not get awarded for it

I mean… if you die, does it really matter in the end? :stuck_out_tongue:

Again, not to sound too arrogant, but honestly that’s just my style in that section of the game, but if there’s too much demand to change it up, I will :slight_smile:

This formula had some serious advancements in some special areas to get it to work as expected… :sweat_smile:

Anyway, it works with 100% accuracy now, exactly as it’s expected to do, and all corner cases are dealt with (but it assumes that your allies won’t level up. I’ll keep this topic for another day because I’m getting infuriated lol)

and the last thing I want is to get into a greed cycle that can potentially destroy everything

Actually, in most RPGS, death is long from the end… when the character respawns, there’s a slim chance he still learned something… In fact, this may sound crazy, but in the real world, we really do learn and earn experience in a skill as we go (except if you die, because here in reality, there are no re-spawns).

Let’s say I was attempting to record an epic rock song all by my lonesome… the first attempt was probably quite horrible, and never shown to anybody. It might even be called a complete failure… but along the way, I learned how to balance sounds, how to get the right effects and how to count to 4. So I failed in the mission (record epic rock song), but I gained experience in playing each of my instruments…in fact in the process of recording the song, I gained experience long before the project was completed…

If I get no experience until I release the perfect epic rock song, then I’ll never get the skills I need to record the perfect epic rock song. That’s how experience really works (I’ve always thought getting XP based on just the kill was unrealistic).

Ahh, you both make valid points, and now I’m debating on the whole thing, because there’s one side of good friends (on a private discord server I found from Reddit) who think experience on death is the way to go, and another side of good friends (i.e: my friends on GameDev.TV) as well thinks that experience should be given on the spot

For the moment, I’ll go with the first option (I think it’s because it’s what I got used to from video games growing up), and I’ll prototype with the idea further down the line when the game is almost done :slight_smile:

This topic was automatically closed 20 days after the last reply. New replies are no longer allowed.

Privacy & Terms