How would I create Dragging 'Abilities' to Actionbars?

As the title suggests, how would I expand upon the DragItem so I could have it drag skills to the action bar and back to a skill book opposed to our inventory.

When I invest points into said ability, rather than it just miraculously appear inside one of my ActionBar slots, I would like to have the user drag said abilit(ies) into the ActionBars themselves.

I don’t want to just create an asked question and not have at least tried doing something.

So what I have tried was re-create the current drag scripts and instead of using the type “InventoryItem” I used “Ability” since my Skill’s are derivative from Ability which follows back down to InventoryItem anyways.

But when it came to trying this out in practice, I could drag the icon from this “AbilitySlot” as i called it, but it wouldn’t go inside the action bar, it just snapped back to its original source. It was cool to see that it was some-what behaving without really changing code from within the new scripts.

I might be somewhat on the right track, or maybe I’m not and maybe there’s an easier way without having to create duplicated scripts just to have it check for Abilities instead of InventoryItem’s within the IDrag interfaces and the DragItem.cs

What I expect to happen:

User opens the SkillUI and inside is comprised of a linear ability tree where when you click on a ability and have committed points, rather than it instantly creating the ability(item) and snapping it to the action bar (which is how my current set up works),

I’d like for the player to be the one to control when the ability gets slotted into an action bar by dragging the ‘ability (item)’ to the bar without removing the skill books icon simultaneously, without it being removed at the end of the placement if that makes sense…

Any ideas?

The issue is that the AbilityUI is expecting an InventoryItem (which it accepts or rejects based on it being an Action item).

This means that your AbilityStore’s UI will still want to use InventoryItems. It’s not as bad as it sounds, because it’s also going to reject anything that isn’t an Ability (in the MaxAcceptable method of your AbilityStoreItemUI (I’m assuming that’s what you’ll call it).).

Of course one issue is that the AbilityStore will probably want to hang on to that ability for future use, after all, you’ve learned the skill. This is where it gets a bit tricky. Most likely, your AbilityStore won’t be storing things like potions and scrolls that rightly belong in your inventory, but can indeed be dragged to the Inventory bar.

This is where we take a page from what we learned about IItemStore in the Shops section.

Here’s how I would go about this (in rough terms, not code)

  • Whenever you add any item to the inventory, the Inventory will make a check to see if any other inventory adjacent component can use the item via the IItemStore interface. so my AbilityStore would implement IItemStore, and store all known abilities as a Dictionary<Ability, bool>. The bool is irrellevant, it’s just because we need a “value” for every key. What we’re creating is a “does this key exist” type dictionary. So you learn a skill, then the abilitiesKnown[ability]=true;.

  • When you drag an ability from the AbilityStoreUI to the ActionStoreUI, the AbilityStoreUI’s RemoveItem should simply do nothing. The ActionStoreUI will clear its item, and the Drag system will hold a copy of both items.

  • The drag system then gives a copy of whatever is in the AbilityStoreUI to the ActionStoreUI, and that goes on as usual, no changes needed.

  • The drag system then gives a copy of what was in the ActionStoreUI to the AbilitySystemUI. If that item is an Ability that is not stackable then the AbilityStoreUI should do nothing. It should already know that you know that spell. If the item is stackable, or somehow not an Ability, then it should call the Inventory and AddToFirstEmptySlot so the Inventory can figure out what to do with the item.

1 Like

After some thinking today – I’m wondering about my design choice. Not EVERY game has drag skills into hot bars, only some of the older games. And I’m not sure if my game REALLY needs that. Sure, I’m going to be having a skill ui design with abilities (buttons) to unlock skills.

But I think there’s a better approach to this… and it might save me some time.

SkillTree.cs
This is how I get the ability to appear inside my ActionBar.

void AcquireSkill(int id_Skill)
{
    // Dictionary you helped me to use from the Unlocking Abilities thread.
    // Upon first investing a point into this inspectedSkill, it will automatically create and equip.
    if (!skillsUnlocked[id_Skill]) 
    {
        inspectedSkill.Unlocked = true;
        skillsUnlocked[id_Skill] = inspectedSkill.Unlocked;
        Inventory inventory = FindObjectOfType<Inventory>();
        Ability abilityItem = Instantiate(inspectedSkill);
        inventory.AddToFirstEmptySlot(abilityItem, 1);
    }
}

And as you would recall Brian, I did that challenge about a month ago when I was still going through the course that Sam said for us to challenge ourselves to autoequip things to our action bars or equipment respectively.

I’m wondering…

Would it be a weird design choice to instead ‘right mouse click’ on a skill to ‘enable / disable’ it inside a hot key slot instead of dragging from the skill book? I have a firm belief you’ve played games most of your life… what do you think?

Should I give this dragging ability to an action bar an attempt or could the right click over the button also be a suitable mechanic.

For example, invested a point into Slash Attack. (This is inside the skill book ui)
The icon for Slash Attack is ‘active’ and when you mouse over it, it comes up with a pop up like “Right Mouse To Equip” and then when the user right clicks over the button, it instantiates the ability and slots it into the first action bar it finds OR it finds an ability that is exactly of that said ability and disables it from the action bar.

I’ve oft been accused of wiling away the hours in playing a game or two…

This design choice can work, as long as you still leave a mechanism for players to re-arrange their action bar as they see fit.

So for your Slash Attack example, you’re saying that right clicking in the skill book ui would either slot it into the Action bar if it’s not already there and there is a space available, or remove it from the action bar if it is there? That’s actually workable.

1 Like

So as I have drawn oh so horribly, inside the skill book is a list of skills. When a skill has a point invested, it’s icon becomes ‘active’ and then cursoring over it will prompt a message to right click to equip or right click to unequip.

I don’t want the abilities to be able to go inside the inventory so I’ll take a look at figuring out how to manipulate the AddToFirstEmptySlot so it doesn’t ever accept an ability inside its bag.

I want the abilities to still be interchange-able between the abiltty slots! So yeah hopefully that will maintain and not have to do any other additional steps to not lose that functionality.

1 Like

Okay well I have created my own solution after doing some tampering within some scripts and adjusting the way I unlock skills in the first place.

Contents:

  1. Implemented proper skill points into my progression array (mirroring Trait points).
  2. Took inspiration from our Trait set up and now have a way for the player to adjust skill point allocations before commiting the final points to unlock or empower the skill in question.
  3. Adjustments were made to the process of checking whether a skill is unlocked currently, and whether that skill is currently set as active on a hot key.
  4. Set up a very basic structure UI including the buttons to assign points to a skill, having that skill be activated and allow the user to click on an image button to either slot it or remove it from a slot.
  5. Changed the way my InventoryDropTarget works and Inventory scripts so that anything labeled as a skill cannot be removed by dragging it’s icon to the ground or into the inventory.

Proper ability points inside my Progression array that my SkillTree.cs checks before the user can attempt to invest points into any skill.

New addition to SkillTree to get the total ability points before we can ‘spend’ anything.

SkillTree.cs

        public int GetAssignablePoints()
        {
            return player.GetComponent<BaseStats>().GetStat(EStat.TotalAbilityPoints);
        }

New addition to Skill…

Skill.cs

        // Check whether the unlocked skill is currently on an ActionBar
        [SerializeField] bool skillInUse;

All of modifications are as follows…

Modification to SkillTree:

        // No longer instantiates, instead just unlocks a new ability for it's first time.
        void AcquireSkill(int id_skill)
        {
            if (!skillsUnlocked[id_skill])
            {
                inspectedSkill.Unlocked = true;
                skillsUnlocked[id_skill] = inspectedSkill.Unlocked;
            }
        }

        // This is my toggle on or off skill event on my current 'Unlocked Skill' button in my Editor.
        public void AddOrRemoveSkillFromSlot(int id_skill)
        {
            for (int i = 0; i < store.GetMaxSlots(); i++)
            {
                if (store.GetAction(i) != null && skillsDictionary[id_skill].SkillInUse)
                {
                    skillsDictionary[id_skill].SkillInUse = false;
                    store.RemoveItems(i, 1);
                    break;
                }
                else if (store.GetAction(i) == null && !skillsDictionary[id_skill].SkillInUse)
                {
                    skillsDictionary[id_skill].SkillInUse = true;
                    store.AddAction(inspectedSkill, i, 1);
                    break;
                }
            }
        }

Modification to Inventory:

        public bool HasSpaceFor(InventoryItem item)
        {
            if (item is Skill skillItem) { Debug.Log("Cannot add " + skillItem.name + " to inventory!"); return false; }
            return FindSlot(item) >= 0;
        }

This should still allow abilities like ‘scrolls / potions’ to go into my bags and slots (haven’t tested yet) since it’s specifically refusing a skill item.

Modification to InventoryDropTarget:

        void Awake()
        {
            player = GameObject.FindGameObjectWithTag("Player");
            inventory = player.GetComponent<Inventory>();
        }

        public void AddItems(InventoryItem item, int number)
        {
            if (item is Skill skillItem)
            {
                inventory.AddToFirstEmptySlot(skillItem, 1);
                return;
            }

            player.GetComponent<ItemDropper>().DropItem(item, number);
        }

I am still testing so I won’t put this as a solution quite yet… also feedback would be excellent. ^^

The best feedback is the result. How is it working?

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. ^^

Outstanding!!!
This looks like an excellent solution!

1 Like

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

Privacy & Terms