Traits not updating - Awake being called twice?

Hi all,
Spent the last couple of hours trying to debug this to no avail.

My traits don’t impact my actual health / mana (I’m guessing the rest of the stats as well), despite being changed in the UI (and the committed changes being persistent through saves).

I followed the advice in the following threads:

Traits not working

Living Nightmare

IMPORTANT/Puzzling: When I have a value in my additive value, the value DOES get changed, and the additive value is being multiplied by percentage apparently. That happens with the code from the github as well, and i can’t really find any differences.

What I Tried:

-I added “[DisallowMultipleComponent]”

  • switched my Awake() with the suggested awake with the Debug logs (from brian’s post), getting my awake called Twice.
    -Deleted saves,
    -Opened and closed Unity,
    -Redid the lesson entirely,
    -Made sure my player has a single Instance of TraitStore.cs on it,
    -Made sure I have a single instance of Player in the scene, and it is the only TraitStore component in the scene,
    -Made sure the player’s BaseStats are the only once with “Use Modifiers” checked,
    -Removed and reapplied TraitStore.cs / BonusConfigs to the prefab
    -getting the code from the github

Don’t know what else could cause this, perhaps I am getting some weird race condition?
(Unity 2022.3.10f1)
Any help in resolving would be greatly appreciated.

using System;
using System.Collections.Generic;
using GameDevTV.Saving;
using UnityEngine;

namespace RPG.Stats
{
	[DisallowMultipleComponent] //Prevents multiple components on your GameObject
	public class TraitStore : MonoBehaviour, IModifierProvider, ISaveable
	{
		[SerializeField] private TraitBonus[] bonusConfig;

		[System.Serializable]
		class TraitBonus
		{
			public Trait trait;
			public Stat stat;
			public float additiveBonusPerPoint = 0;
			public float percentageBonusPerPoint = 0;
		}

		private Dictionary<Trait, int> assignedPoints = new Dictionary<Trait, int>();
		private Dictionary<Trait, int> stagedPoints = new Dictionary<Trait, int>();

		private Dictionary<Stat, Dictionary<Trait, float>> additiveBonusCache;
		private Dictionary<Stat, Dictionary<Trait, float>> percentageBonusCache;

		// private void Awake()
		// {
		// 	additiveBonusCache = new Dictionary<Stat, Dictionary<Trait, float>>();
		// 	percentageBonusCache = new Dictionary<Stat, Dictionary<Trait, float>>();
		//
		// 	foreach (var bonus in bonusConfig)
		// 	{
		// 		if (!additiveBonusCache.ContainsKey(bonus.stat))
		// 		{
		// 			additiveBonusCache[bonus.stat] = new Dictionary<Trait, float>();
		// 		}
		//
		// 		if (!percentageBonusCache.ContainsKey(bonus.stat))
		// 		{
		// 			percentageBonusCache[bonus.stat] = new Dictionary<Trait, float>();
		// 		}
		//
		// 		additiveBonusCache[bonus.stat][bonus.trait] = bonus.additiveBonusPerPoint;
		// 		percentageBonusCache[bonus.stat][bonus.trait] = bonus.percentageBonusPerPoint;
		// 	}
		// }


		private void Awake()
		{
			additiveBonusCache = new Dictionary<Stat, Dictionary<Trait, float>>();
			percentageBonusCache = new Dictionary<Stat, Dictionary<Trait, float>>();
			foreach (var bonus in bonusConfig)
			{
				Debug.Log(
					$"Processing Bonus {bonus.stat}/{bonus.trait} +{bonus.additiveBonusPerPoint} +{bonus.percentageBonusPerPoint}% + {gameObject.name}");
				if (!additiveBonusCache.ContainsKey(bonus.stat))
				{
					Debug.Log($"Creating Additive lookup for {bonus.stat}");
					additiveBonusCache[bonus.stat] = new Dictionary<Trait, float>();
				}

				if (!percentageBonusCache.ContainsKey(bonus.stat))
				{
					Debug.Log($"Creating Percentage lookup for {bonus.stat}");
					percentageBonusCache[bonus.stat] = new Dictionary<Trait, float>();
				}

				additiveBonusCache[bonus.stat][bonus.trait] = bonus.additiveBonusPerPoint;
				percentageBonusCache[bonus.stat][bonus.trait] = bonus.percentageBonusPerPoint;
				Debug.Log(
					$"Setting Additive [{bonus.stat}][{bonus.trait}] to {additiveBonusCache[bonus.stat][bonus.trait]}");
				Debug.Log(
					$"Setting Percentage [{bonus.stat}][{bonus.trait}] to {percentageBonusCache[bonus.stat][bonus.trait]}");
			}
		}

		public int GetProposedPoints(Trait trait)
		{
			return GetPoints(trait) + GetStagedPoints(trait);
		}

		public int GetPoints(Trait trait)
		{
			return assignedPoints.ContainsKey(trait) ? assignedPoints[trait] : 0;
		}

		public int GetStagedPoints(Trait trait)
		{
			return stagedPoints.ContainsKey(trait) ? stagedPoints[trait] : 0;
		}

		public void AssignPoints(Trait trait, int points)
		{
			if (!CanAssignPoints(trait, points))
			{
				return;
			}

			stagedPoints[trait] = GetStagedPoints(trait) + points;
		}

		public bool CanAssignPoints(Trait trait, int points)
		{
			if (GetStagedPoints(trait) + points < 0)
			{
				return false;
			}

			if (GetUnassignedPoints() < points)
			{
				return false;
			}

			return true;
		}

		public int GetUnassignedPoints()
		{
			return GetAssignablePoints() - GetTotalProposedPoints();
		}

		public int GetTotalProposedPoints()
		{
			int total = 0;
			foreach (int points in assignedPoints.Values)
			{
				total += points;
			}

			foreach (int points in stagedPoints.Values)
			{
				total += points;
			}

			return total;
		}

		public void Commit()
		{
			foreach (Trait trait in stagedPoints.Keys)
			{
				assignedPoints[trait] = GetProposedPoints(trait);
			}

			stagedPoints.Clear();
		}

		public int GetAssignablePoints()
		{
			return (int)GetComponent<BaseStats>().GetStat(Stat.TotalTraitPoints);
		}

		public IEnumerable<float> GetAdditiveModifiers(Stat stat)
		{
			if (!additiveBonusCache.ContainsKey(stat))
			{
				yield break;
			}

			foreach (Trait trait in additiveBonusCache[stat].Keys)
			{
				float bonus = additiveBonusCache[stat][trait];
				yield return bonus * GetPoints(trait);
			}
		}

		public IEnumerable<float> GetPercentageModifiers(Stat stat)
		{
			if (!percentageBonusCache.ContainsKey(stat))
			{
				yield break;
			}

			foreach (Trait trait in percentageBonusCache[stat].Keys)
			{
				float bonus = percentageBonusCache[stat][trait];
				yield return bonus * GetPoints(trait);
			}
		}

		public object CaptureState()
		{
			return assignedPoints;
		}

		public void RestoreState(object state)
		{
			assignedPoints = new Dictionary<Trait, int>((IDictionary<Trait, int>)state);
		}
	}
}

I can’t see what the original TraitBonus stats are on the Player to know for sure if these are being set correctly (though the code looks correct). A Screenshot of the TriaitStore may help here.

This part is confusing me… are you saying the Health is increased in the UI but now on the character? Is this confirmed via debugs? Or are you just meaning that the purchased Traits are showing up in the UI?

The dual Awakes aren’t making a great deal of sense, as that’s simply not how Unity works… Just to see what might be going on, turn off collapsing and add {gameObject.name} to each of the Debugs.

  • Are the names the same?
  • Do you get all the messages at once, then another set all at once, or are they interleaved?

Solved, Thank you Brian! :slight_smile:

I found the culprit, It wasn’t a very obvious bug, it’s something that slipped past me during the Core course.
It was missing parentheses in my BaseStats.cs script. So a mistype from 3 monthes ago came back to haunt me.

I had this:

		public float GetStat(Stat stat)
		{
			return GetBaseStat(stat) + GetAdditiveModifiers(stat) * (1 + GetPercentageModifiers(stat) / 100);
		}

instead of this:

		public float GetStat(Stat stat)
		{
			return (GetBaseStat(stat) + GetAdditiveModifiers(stat)) * (1 + GetPercentageModifiers(stat) / 100);
		}

I am however still getting the debug logs from awake printed twice, (not at the same time, subsequently) i don’t think it should be printed that way - or am I wrong? I can’t really tell if that foreach loop is the reason?

//
I logged here what I tried while following your instructions, so I am posting it below perhaps it would aid anyone trying to debug a similiar issue in the future :slight_smile:
//

Here’s the TraitStore on the Player Instance:

I checked the Prefab asset in my IDE as well and couldn’t find a duplicate or anything there (checked TraitStore by component GUID first to see if there’s anything I could search for duplicates of, and then searched for the “BonusConfig” string) and and there’s a only single instance of the component present.

If I set the Additive modifier to 0, there’s no change at all (both in the character and Debug UI).

If I set the modifier to 1 to show you what I meant by the UI bug (was getting ready to film it in OBS), the percentage bonus was applied to the additive bonus, and not the entire sum. So it finally dawned on me that this is a calculus problem, I went to check the additive/percentage calculations in the BaseStats.cs on the gdtv github.

I compared both methods GetAdditiveModifier / GetPercentageModifier to the ones in my project, saw they are pretty much the same (slight naming changes to make it more explicit).

Then I followed up for where it is called (Rider is a great IDE btw I love it and can’t recommend it enough),
The order seemed wrong, took me about 30 seconds to revisit 3rd grade and facepalm my way to a solution. :joy:

Thank you for taking the time to help with it :slight_smile:

I’ve been using it for about 4 years. It’s worth every penny you pay for it. (The same can be said about VS Code, though… it’s worth exactly what it costs…) :stuck_out_tongue:

I must have been asleep at the wheel last night, as the Awake() thing is normal. When the game first starts, Awake() is called, but in that same overall cycle, the SavingWrapper loads the last scene (even if there is no save file, the scene is reloaded as soon as SavingWrapper is instantiated, causing the TraitStore in the new scene to fire it’s Awake() again (which it has to do because the old TraitStore was destroyed.

Good job working out the PEMDAS issue. While the internet will forever argue about whether 3 + 5 * 2 is 13 or 16, the answer is crystal clear in c#.

2 Likes

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

Privacy & Terms