Hello again fellas. So… I got myself into a bit of a funky problem (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