Trait To Increase Experience Reward

If you want to have a trait that increases the Experience Reward you get when you douse enemies while assigning points to it, we can’t do that due to our current setup, as the enemies would try to find modifiers for it on their components, instead of on the player, because it doesn’t exist:

So this is where the player gains experience when the enemies die, as they invoke this method & ask the BaseStat for specific modifiers for that:

void AwardExperience(GameObject instigator)
{
    if (!instigator.TryGetComponent<Experience>(out var experience)) return;

    float experiencePoints = baseStats.GetStat(Stat.ExperienceReward);
    experience.GainExperience(experiencePoints);
}

For this reason I modified the BaseStat this way to reflect the change & it works, but I’m worried if I introduced some new unwanted errors or misconduct as I’m not familiar with that class. Perhaps there is a better way of doing it?

BaseStat:

// Introduced a new variable
GameObject player;

void Awake()
{
    experience = GetComponent<Experience>();
    currentLevel = new(CalculateLevel);
    // Getting reference to the Player GameObject
    player = GameObject.FindWithTag("Player");
}

public float GetStat(Stat stat)
{
    (float additiveModifier, float percentageModifier) = GetModifiers(stat);
    return (GetBaseStat(stat) + additiveModifier) * (1 + percentageModifier * 0.01f);
}

(float, float) GetModifiers(Stat stat)
{
    float additiveModifier = 0f;
    float percentageModifier = 0f;
    // Changed the if statetement
    if (!shouldUseModifier || (stat != Stat.ExperienceReward && gameObject != player)) return (additiveModifier, percentageModifier);
    // Iterating over the Player's Component instead
    foreach (IModifierProvider provider in player.GetComponents<IModifierProvider>())
    {
        foreach (float modifier in provider.GetAdditiveModifiers(stat))
        {
            additiveModifier += modifier;
        }

        foreach (float modifier in provider.GetPercentageModifiers(stat))
        {
            percentageModifier += modifier;
        }
    }

    return (additiveModifier, percentageModifier);
}

This is still something that can be handled entirely at the AwardExperience method…

void AwardExperience(GameObject instigator)
{
    if (!instigator.TryGetComponent<Experience>(out var experience)) return;

    float experiencePoints = baseStats.GetStat(Stat.ExperienceReward);
    float experienceModifier = instigator.GetComponent<BaseStats>().GetStat(Stat.ExperienceModifier);
    experiencePoints *= (1+(experienceModifier/100f));
    experience.GainExperience(experiencePoints);
}

I’m treating experienceModifier in this case as each point being 1% increase in experience, hence division by 100f.

You’ll need:

  • A trait responsible for governing experienceModifier (Wisdom, perhaps?)
  • A Stat.ExperienceModifier
  • To add that stat to the Player’s progression (even if it means filling it with zeroes)

That’s it.

You’ll also be able to add that Stat.ExperienceModifier to any equipment you like.

Hat Of Quick Study
Increases experience gained when in battle.
ExperienceGained + 5

I’m noticing a misbehaviour myself:
If an instance in the world other than the player can use modifiers, it doesn’t have its own, but uses the modifiers for the player instead. In my game I don’t want them to use modifiers anyway but this method cannot be adapted to other projects which is why doing it in the AwardExperience() method is a more valid approach

Two things I do not understand is what you mean with “filling it with zeroes”?:

  • To add that stat to the Player’s progression (even if it means filling it with zeroes)

And about that Hat Of Quick Study, when equipped & if the player eliminates an enemy in battle, the experience reward gets increased by 5?

Kind regards,

Generally speaking, it should be using it’s own modifiers… It certainly is in our codebase (in fact, since I give enemies an assortment of gear that affects their stats on the regular, I can guarantee it).

Let’s see your complete BaseStats.cs

By doing 1+the bonus/100, it should get increased by 5%, not 5 points…

Generally speaking, it should be using it’s own modifiers… It certainly is in our codebase (in fact, since I give enemies an assortment of gear that affects their stats on the regular, I can guarantee it).

Yes, this is the misbehaviour I’m speaking of that I have noticed in my solution

Trying yours, I’m still not getting it right:. Following your advice, I get to this result:

void AwardExperience(GameObject instigator)
{
    if (!instigator.TryGetComponent<Experience>(out var experience)) return;

    float experiencePoints = this.baseStats.GetStat(Stat.ExperienceReward);

    if (instigator.TryGetComponent<BaseStats>(out var baseStats))
    {
        float experienceModifier = baseStats.GetStat(Stat.ExperienceModifier);
        experiencePoints *= 1 + experienceModifier * 0.01f;
    }

    experience.GainExperience(experiencePoints);
}

And my BaseStat class now pretty much is the same as in the RPG course.

Now I made these changes in my scripts:

Screenshot 2024-03-19 031511

So that would mean that experience points are modified by 50% by default, and that assigning traits to Cunningness here adds another 20% to that 50%.
Is that what you’re suggesting?

For this calculation, I had to work a bit differently than the standard stat, so that Additive Modifiers really become Percentage Modifiers, and Percentage Modifiers are a percentage of that Additive Modifier…

So if Levels is 50, that means that you’ll get a 50% boost (awarded experience * (1 + experienceModifier/100))

So suppose we got 100 experience points, we would multiply that by (1 + .5f) or 1.5f so we would get 150 experience.
With your percentage bonus, assuming you had 1 cunningness, you would get an Experience Modifier of 50 * 1.2f (20% of 50 tacked on), or a 60% boost, so 160 experience.
If, instead, you made it an Additive bonus of 20, then you would get 50 + 20 or a 70% boost, so 170 experience.

Of course, these numbers get extreme very quickly. I definitely wouldnt’ start with a 50% boost. :slight_smile:

Indeed, I do recognize the issue that occurs when the designer tries to assign some additiveModifier's to it…

Since this is a special case, I modified the GetStat(Stat stat) method over in BaseStats.cpp like this:

public float GetStat(Stat stat)
{
    (float additiveModifier, float percentageModifier) = GetModifiers(stat);

    if (stat == Stat.ExperienceModifier)
    {
        return 1 + additiveModifier * (1 + percentageModifier) * 0.01f;
    }

    return (GetBaseStat(stat) + additiveModifier) * (1 + percentageModifier * 0.01f);
}

This 1 here is simply to prevent default experience modifiers from being used. I only want to apply it if the player actually awards a point for the corresponding trait, in this case Cunningness.

And over in AwardExperience(GameObject instigator), I get the following result:

void AwardExperience(GameObject instigator)
{
    if (!instigator.TryGetComponent<Experience>(out var experience)) return;

    float experiencePoints = this.baseStats.GetStat(Stat.ExperienceReward);

    if (instigator.TryGetComponent<BaseStats>(out var baseStats))
    {
        float experienceModifier = baseStats.GetStat(Stat.ExperienceModifier);
        print(experienceModifier);
        experiencePoints *= experienceModifier;
    }

    experience.GainExperience(experiencePoints);
}

That should do the job & this way the enemies keep their own modifiers

Of course, these numbers get extreme very quickly. I definitely wouldnt’ start with a 50% boost.

Hahah, I know it’s just a prototype for me to clearly see the effect of these modifiers on the experience gained

It’s me again,

After watching the lecture about the Shop Barter system, I think I’d best leave the GetStat(Stat stat) method in the BaseStats as well as the AwardExperience(GameObject instigator) method in the Health class as they were before, to keep things consistent, & instead pass the task on to the Experience class:

public void GainExperience(float experience)
{
    if (TryGetComponent<BaseStats>(out var baseStats))
    {
        float experienceModifier = baseStats.GetStat(Stat.ExperienceModifier);
        experience += experience * (experienceModifier * 0.01f);
    }

    experiencePoints += experience;
    onExperienceGained();
}

And still leave the dictionary entry for the ExperienceModifier blank, which is handled by the Progression class, as discussed in that lecture:

public float GetStat(Stat stat, CharacterClass characterClass, int level)
{
    BuildLookup();

    if (!lookupTable[characterClass].ContainsKey(stat))
    {
        return 0;
    }

    // ...
}

As the instructor pointed out already, the percentage bonus per point allocated to the, Cunningness trait won’t do anything, but the additive modifier would make more sense in this context, but you can choose to do it differently.

My only concern here is that this sets up a cross-dependency between BaseStats and Experience. BaseStats is already depending on Experience, and now the Experience component is depending on the BaseStats component. This is one reason my solution was centered in the Health component itself. It already depends on both the BaseStats and Experience components, while neither component relies on the Health.

As written, this shoudln’t cause any direct issues, but is something to be aware of. We try to avoid cross dependencies because it’s very easy to lead to unintended consequences.

1 Like

Then there is a misunderstanding. I thought circular dependencies occur when two different namespaces use each other’s namespace.
Thanks to you, it has been clarified that the mere use of a namespace does not create a dependency, but when a class uses another class in its implementation.

So using an interface could then be a viable solution to break this circular dependencies, like this:

namespace RPG.Stats
{
    public interface IStatProvider
    {
        float GetStat(Stat stat);
    }
}

In BaseStats we simply inherit from it because the method has already been implemented:

public class BaseStats : MonoBehaviour, IStatProvider

And in the Experience class we call the IStatProvider instead of BaseStats to get the specific stat:

public void GainExperience(float experience)
{
    if (TryGetComponent<IStatProvider>(out var statProvider))
    {
        float experienceModifier = statProvider.GetStat(Stat.ExperienceModifier);
        print(experienceModifier);
        experience += experience * (experienceModifier * 0.01f);
    }

    experiencePoints += experience;
    onExperienceGained();
}

So I hope this is fine now

As written, this shoudln’t cause any direct issues, but is something to be aware of. We try to avoid cross dependencies because it’s very easy to lead to unintended consequences.

And I think what you mean with “unintended consequences” are infinite loops which in turn causes stack overflow errors due to excessive recursion?

Namespaces exist mainly to prevent naming conflicts (see Unity.Random vs System.Random), but also to help us better identify cross-dependencies. They can occur within a namespace as easily as between namespaces. Whenever two classes depend on each other to operate, they are tightly coupled.

Yes, though this can trick you as well, and be harder to spot if not careful. As a pure hypothetical, if GetStat() were to verify the level by checking experience we would find ourselves in a hidden recursion loop.

Recursion is one risk. The other is code maintainability. For this, we head back to the original lesson on IAction… While Sam doesn’t introduce other IActions until the final course, many of us have other classes that are also considered IActions. For example, in my personal version of the RPG, if you click on a Pickup, a PickupCollector acts like Fighter and instructs Mover to move the character to the pickup. Same with Shops and Dialogue Speakers. Now imagine that every time you add a new action of this type, you had to go and update the code everywhere it’s referenced so that you were sure to cancel all the different possible actions. When things are small (like this experience example we’re discussing), the risk of issue is low. When things get more complicated, like with 6 classes that all handle character movement and interactions, there’s a good chance you’ll miss something.

Whenever two classes depend on each other to operate, they are tightly coupled.

But tightly coupled as well as when it comes to implementation details. When two classes depend on each other to operate, this can make the code harder to maintain and modify, as changes in one class might require changes in the other.

Yes, though this can trick you as well, and be harder to spot if not careful. As a pure hypothetical, if GetStat() were to verify the level by checking experience we would find ourselves in a hidden recursion loop.

This is evil, I mean a typical example of a hidden recursion would be something along those lines, right?, which can be handled via proper termination:

public float GetStat(Stat stat)
{
    CheckLevel();

    (float additiveModifier, float percentageModifier) = GetModifiers(stat);

    return (GetBaseStat(stat) + additiveModifier) * (1 + percentageModifier * 0.01f);
}

void CheckLevel()
{
    // Pretty much any stat
    float someStat = GetStat(Stat.Health);
}

Now imagine that every time you add a new action of this type, you had to go and update the code everywhere it’s referenced so that you were sure to cancel all the different possible actions. When things are small (like this experience example we’re discussing), the risk of issue is low. When things get more complicated, like with 6 classes that all handle character movement and interactions, there’s a good chance you’ll miss something.

This is where I’m starting to appreciate our ActionScheduler, which mitigates this problem by using a centralized system for managing actions. I may have to revise this lecture, which I will do anyway, as I intend to repeat the RPG course once I am done with the Shops & Abilities course, but try to come up with an unique series of Point-and-Click game

1 Like

This is, IMO, where the real learning starts. The first time, you run through a course, it’s keeping up with Sam and Rick. The 2nd time, it’s letting those ideas flow with “What if?”

2 Likes

Privacy & Terms