Race condition found in Experience vs. BaseStats

There is a race condition like follows:

  • The PersistentObjectSpawner spawns the PersistentObjects in Awake. --> SavingWrapper is spawned.
  • Also in Awake in the SavingWrapper LoadLastScene is called. --> Calls LoadLastScene in the SavingSystem.
  • LoadLastScene in SavingSystem calls RestoreState which restores states in SavableEntities.
  • One of that saveables is the Experience.cs which in RestoreState calls the event onExperienceGained.

And now the race condition:

  • BaseStats is subsribing to that event in Experience, but not before in Start-method!

So at the time the onExperienceGained event is called the first time, nothing has described to that event, which causes an error. That was the reason why for me, in lecture 144 putting LoadLastScene in Awake, didn’t work.

1 Like

Well, after lecture 150, I deleted the call of onExperienceGained in RestoreState again (I have adde it to fix the bug in lecture 144 and it’s not in Sam’s scripts). With the new LoadLastScene, where the scene is loaded in any case it’s working now.

Are you null checking onExperienceGained? A simple way to do this is with the ? operator: onExperienceGained?.Invoke(); This will ensure that onExperienceGained is only invoked if there is a subscriber. (Can also be written with the full if(onExperienceGained!=null) onExperienceGained?.Invoke(); the compiler will treat these statements identically)

2 Likes

This is why I made a static GameEvents class

public static delegate ExperienceGainedDelegate(GameObject sender, float experience, 
    float totalExperiece);

public static event ExperienceGainedDelegate OnExperienceGained;

public static void InvokeOnExperienceGained(GameObject sender, float experience,
    float totalExperience){
    OnExperienceGained?.Invoke(sender, experience, totalExperience);
}

When you want to listen to the event subscribe and unsubscribe in the OnEnable and OnDisable

private void OnDisable() {
    GameEvents.OnExperienceGained += ExperienceGained;
}

private void OnDisable() {
    GameEvents.OnExperienceGained -= ExperienceGained;
}

private void ExperienceGained(GameObject sender, float experience, float totalExperience){
    if(sender!=gameObject) return; //this was sent for a different character
    //do your magic
}

when you want to trigger the event you call the invoke method

GameEvents.InvokeOnExperienceGained(gameObeject, gainedXp, totalXp);

Since the GameEvents is static you do not need to worry if it has been instantiated yet.

I do have the null check so I do not get the error, but the problem is the Experience experience = GetComponent(); is never resolved so onGetExperience is never called so player does not level. I have tried adding the experience = GetComponent() in different methods such as awake and start but it is always null even though the player has the script. Also, when the enemy dies, the debug.log shows the correct experience from the enemy type, but it is never processed due to the previous issue. I use the GetComponent<>(); all the time, but this is the first time it is always null? Any ideas?

So the fix for me was to remove the register event from onEnable and put it in Awake
if (experience != null)
{
experience.onExperienceGained += UpdateLevel; //register event
Debug.Log("Registered for onExperienceGained for: " + this.name);
}

Since the Experience and BaseStats are on the same component, it’s fine to subscribe in Awake, but there is still the matter of why Experience is null in the other methods to contend with. I suspect it may be because of a common mistake that even after many years of doing this, I’ve made myself many times.

You have the global variable

Experience experience;

towards the top of your class. In Awake() it’s easy to accidentally assign it like this:

Experience experience = GetComponent<Experience>();

This creates a temporary local variable experience that is only valid within the Awake() method, completely ignoring the global variable experience, leaving it null.
If this is the case, simply changing this line to

experience = GetComponent<Experience>();

should fix the issue.