What a rabbit hole I went through just to get this up and running. Some of this post isn’t relevant but it was needed to ensure the functionality would be correct for my skill tree. It might be a bit convoluted and a bit of a hacky way in certain places. (I’m still unsure how to spot these)
The first thing as I think i briefly mentioned was that I basically set up a simple structural 'Trait/TraitRow/TraitUI and duplicated to convert them to be used for a new category dubbed ESkill.
These are the ‘fake’ Trait points that keep track of which skill has points into them. I use the same priniciple from the course. A Confirm button, a Minus ESkill point / Plus ESkill point buttons. Then a AbilityRow to keep track of all these.
In addition to the AbilityRow I added a serializefield for the skill itself that row belongs to, as well as an image that will be interactive when the skill has committed points to it.
The issue originally from the last post was that I wasn’t able to track the ability in question’s skill configurations when unlocking via the AbilityRow. So if I have Slash Attack depend on another skill being unlocked, and that skill wasn’t unlocked, then I could still invest a point and unlock Slash Attack.
Obviously not the correct behaviour so that’s why I added the addition of the skill itself to the Row so I can use that in these scripts to check for their conditions.
One difference inside my UnlockSkill() method is that now I assign the skill’s level equal to the amount of points invested into that ESkill.
inspectedSkill.SkillLevel = Mathf.Min(abilityStore.GetPoints(inspectedSkill.ESkill), inspectedSkill.SkillCap);
The way my set up currently works is that when the GetProposedPoints(ESkill) is equal to the amount a skill can level up (the cap limit) then the plus button from AbilityRowUI.cs will no longer be interactable. Preventing the user from dropping points when it no longer makes sense.
It’s also true that if a skill is dependent on another skill and it’s not unlocked, it will not allow the plus button from being interactable altogether.
Finally if you cannot assign any more points to that particular ESkill, this will also disable the buttons.
// Button is interactable dependent on these conditions.
// 1. Can only be interactable if there's points that can be assigned.
// 2. Can only be interactable if CheckSkillConditions() returns true.
// 3. Can only be interactable if the ESkill poiints is less than our activeSkill's level cap.
plusButton.interactable = abilityStore.CanAssignPoints(activeSkill.ESkill, +1) && CheckSkillConditions()
&& abilityStore.GetProposedPoints(activeSkill.ESkill) < activeSkill.SkillCap;
AbilityRowUI / AbilityUI / AbilityStore scripts in their entirety.
public class AbilityRowUI : MonoBehaviour
{
[SerializeField] Skill activeSkill;
[SerializeField] TMP_Text skillName;
[SerializeField] TMP_Text skillValue;
[SerializeField] Button minusButton;
[SerializeField] Button plusButton;
[SerializeField] Button unlockButton;
[SerializeField] Button imageInteractable;
bool conditionStatus = false;
AbilityStore abilityStore = null;
List<Skill.SkillConfig> skills = new List<Skill.SkillConfig>();
void Awake()
{
abilityStore = GameObject.FindGameObjectWithTag("Player").GetComponent<AbilityStore>();
}
void Start()
{
skillName.text = activeSkill.ESkill.ToString();
minusButton.onClick.AddListener(() => Allocate(-1));
plusButton.onClick.AddListener(() => Allocate(+1));
foreach (Skill.SkillConfig config in activeSkill.SkillConfigs)
{
skills.Add(config);
Debug.Log(config);
}
}
public Skill AbilityRowActiveSkill()
{
return activeSkill;
}
void Update()
{
// Button is interactable dependent on if there's assigned points to the ESkill.
minusButton.interactable = abilityStore.CanAssignPoints(activeSkill.ESkill, -1);
// Button is interactable dependent on these conditions.
// 1. Can only be interactable if there's points that can be assigned.
// 2. Can only be interactable if CheckSkillConditions() returns true.
// 3. Can only be interactable if the ESkill poiints is less than our activeSkill's level cap.
plusButton.interactable = abilityStore.CanAssignPoints(activeSkill.ESkill, +1) && CheckSkillConditions() &&
abilityStore.GetProposedPoints(activeSkill.ESkill) < activeSkill.SkillCap;
// The amount of points that has been invested into the ESkill.
skillValue.text = abilityStore.GetProposedPoints(activeSkill.ESkill).ToString();
// Commit button for this AbilityRowUI and is enabled depending on CheckSkillConditions();
unlockButton.gameObject.SetActive(UnlockButtonActive());
// Allow the user to equip or unequip this skill when it's unlocked.
if (activeSkill.SkillLevel > 0 && activeSkill.Unlocked)
{
imageInteractable.interactable = conditionStatus;
}
}
public void Allocate(int points)
{
abilityStore.AssignPoints(activeSkill.ESkill, points);
}
public bool CheckSkillConditions()
{
// Check our skill's dependencies to see whether this should be unlockable.
if (skills.Count > 0)
{
foreach (var skill in skills)
{
if (skills.All((skill) => skill.dependency.Unlocked && skill.dependency.SkillLevel >= skill.skillLevelRequired))
{
// Return true that all found skills were satisfied.
return conditionStatus = true;
}
// Return false if all or any prerequisities were not satisfied.
return conditionStatus = false;
}
}
// Return true if there was no skill configuratiions found.
return conditionStatus = true;
}
public bool UnlockButtonActive()
{
// If there's no assigned points, disable the commit button.
if (!abilityStore.CanAssignPoints(activeSkill.ESkill, -1))
{
return false;
}
// If our 'activeSkill' required points condition is '1' but we have '1' or greater proposedpoints, enable the commit button.
else if (activeSkill.RequiredPointsToUnlock <= abilityStore.GetProposedPoints(activeSkill.ESkill))
{
return CheckSkillConditions();
}
return false;
}
AbilityStore.cs I commented '// NEW’ above any code that is added specifically for the AbilityRowUI or changes I needed to make. Since most is just from the lectures when we implemented the Traits.
public class AbilityStore : MonoBehaviour, IPredicateEvaluator, ISaveable
{
SkillTree skillTree = null;
AbilityRowUI[] abilityRowUI;
Dictionary<ESkill, int> assignedPoints = new Dictionary<ESkill, int>();
Dictionary<ESkill, int> stagedPoints = new Dictionary<ESkill, int>();
// NEW
void Awake()
{
abilityRowUI = AbilityRowUICollection();
skillTree = FindObjectOfType<SkillTree>();
}
public int GetProposedPoints(ESkill skill)
{
return GetPoints(skill) + GetStagedPoints(skill);
}
public int GetPoints(ESkill skill)
{
return assignedPoints.ContainsKey(skill) ? assignedPoints[skill] : 0;
}
public int GetStagedPoints(ESkill skill)
{
return stagedPoints.ContainsKey(skill) ? stagedPoints[skill] : 0;
}
public void AssignPoints(ESkill skill, int amount)
{
if (!CanAssignPoints(skill, amount)) return;
stagedPoints[skill] = GetStagedPoints(skill) + amount;
}
public bool CanAssignPoints(ESkill skill, int points)
{
if (GetStagedPoints(skill) + points < 0) return false;
if (GetUnallocatedPoints() < points) return false;
return true;
}
public int GetUnallocatedPoints()
{
return GetAssignablePoints() - GetTotalProposePoints();
}
public int GetTotalProposePoints()
{
int total = 0;
foreach (int points in assignedPoints.Values)
{
total += points;
}
foreach (int points in stagedPoints.Values)
{
total += points;
}
return total;
}
// NEW
public void Commit()
{
foreach (ESkill skill in stagedPoints.Keys)
{
assignedPoints[skill] = GetProposedPoints(skill);
for (int i = 0; i < abilityRowUI.Length; i++)
{
Skill activeSkill = abilityRowUI[i].AbilityRowActiveSkill();
if (activeSkill.ESkill == skill)
{
skillTree.CanBeUnlocked(activeSkill.SkillID);
}
}
}
stagedPoints.Clear();
}
// NEW
static AbilityRowUI[] AbilityRowUICollection()
{
return FindObjectsOfType<AbilityRowUI>();
}
public int GetAssignablePoints()
{
return GetComponent<BaseStats>().GetStat(EStat.TotalAbilityPoints);
}
public object CaptureState()
{
return assignedPoints;
}
public void RestoreState(object state)
{
assignedPoints = new Dictionary<ESkill, int>((IDictionary<ESkill, int>)state);
}
public bool? Evaluate(EPredicate predicate, List<string> parameters)
{
if (predicate == EPredicate.MinimumTrait)
{
if (Enum.TryParse<ESkill>(parameters[0], out ESkill skill))
{
return GetPoints(skill) >= Int32.Parse(parameters[1]);
}
}
return null;
}
AbilityUI.cs
public class AbilityUI : MonoBehaviour
{
[SerializeField] TMP_Text unassignedPoints;
[SerializeField] List<Button> commitButtons = new List<Button>();
AbilityStore abilityStore = null;
void Start()
{
abilityStore = GameObject.FindGameObjectWithTag("Player").GetComponent<AbilityStore>();
// Each button needs the 'CanBeUnlocked(index) set up with it's skillID.
// A single commit button would mean we cannot find skills from each row,
// And that would mean only the single indexed commit button would ever be called.
foreach (Button button in commitButtons)
{
button.onClick.AddListener(abilityStore.Commit);
}
}
void Update()
{
unassignedPoints.text = abilityStore.GetUnallocatedPoints().ToString();
}
}
PLAYTEST EXAMPLE:
So now after all of these changes I could finally do the following:
I set up two simple AbilityRow’s and added two different skills to the ActiveSkill field. Assigned the buttons to their appropriate places and on the commit button I added a UnityEvent for the ‘CanBeUnlock(index)’ according to that skill’s ID.
Gave myself 6 TotalAbilityPoints from the Stats asset.
AbilityRow 1: Has no dependencies, but is required to have at least 1 GetProposedPoint(ESkill) point before it can be unlocked if the user wishes.
AbilityRow 2: Has 1 dependency: Requires AbilityRow 1’s skill to be unlocked and level 5 minimum. (Ability 1’s cap)
During runtime when I open my Skill book my AbilityRow 2’s buttons are all inactive and it’s commit button is not visible. But my AbilityRow 1’s ‘plus’ button is interactable. I invested some points into it.
One thing that immediately becomes apparent (as intended) is a new (button) then becomes active and from that point on, whatever AbilitiyRow’s skill is unlocked, the player can freely click on that new button to toggle the skill into the action bar or untoggle it from the action bar.
But because my AbilityRow 1’s skill doesn’t have 5/5 points invested, my AbilityRow 2’s plus button still doesn’t allow the user to interact.
Since the ActiveSkill’s level is according to the amount of points invested to that ESkill.
That’s just one of many examples I’ve tested over the last few days, everything is hunky dory. Any code feedback would be appreciative, else, if there’s none to give, kindly place this as the solution to my thread. ^^