Arrow Quiver

OK so, the next step of my game is, I want to add an Arrow Quiver into my Equipment UI, something that takes care of what type of arrows are in my quiver, the number of arrows (arrows will be stackable, probably fall in quantities (on the ground), and a clickable pickup too), and it will consider what type of bow is in my players’ hand. If they’re not a match (i.e: this arrow is not designed to work with this bow), or I don’t have enough arrows (of the correct breed for the bow in hand), then I can’t fire a ranged (arrow and bow) attack. I am aware that we’ll need to setup more UI for that, but apart from that, how do we go around properly implementing this?

(heads-up: I have an Equipment drop algorithm as well in place, that only considers dropping items that have a value of ‘1’ (non-stackable), and arrows are stackable, soo…)

And if we’re speaking of ranged attacks, maybe we should consider putting a limit on the magic attacks by the fireball as well, but this is probably best left as another topic for now.

The only reason I’m asking this now rather than later is because the Equipment Drop system is still fresh, figured we’d get the whole thing done in one go

Rather than completely upending the Equipment system, adding more bugs than a New York Apartment building, it makes more sense to keep the Arrows in a Quiver component. This doesn’t mean you can’t use the same UI, it’s the backing component that would be different.

This is important because, as I said, adding a quantity to the Equipment.cs will send you into refactoring hell.

I’m sorry, but this really gave me a good laugh… Wait till you see what Dubai has :stuck_out_tongue_winking_eye:

How do we go around this though? Do we create a brand new system for that…? (As long as the UI doesn’t drift off, I don’t think the players will notice a thing, and that’s all that really matters in the end)

Yes, something like our Purse(), but holding arrows/ammo instead of coins…

I would start with a subclass of InventoryItem to represent the ammunition…

using GameDevTV.Inventories;
using UnityEngine;

namespace RPG.Inventories
{
    [CreateAssetMenu(fileName = "Ammunition", menuName = "RPG/Ammunition/New Ammunition", order = 0)]
    public class AmmunitionItem : InventoryItem
    {
        
    }
}

It doesn’t have to do much, it’s just to differentiate it from other InventoryItems you don’t want in your Quiver…

Your WeaponConfig would then have a reference to the AmmunitionItem which can be checked with Fighter.

Then you’ll need a Quiver component. I’ve fleshed out a basic quiver component here:


using GameDevTV.Inventories;
using GameDevTV.Saving;
using Newtonsoft.Json.Linq;
using UnityEngine;

namespace RPG.Inventories
{
    public class Quiver : MonoBehaviour, IJsonSaveable
    {
        public event System.Action quiverUpdated;
        
        private AmmunitionItem currentAmmunition=null;
        private int amount = 0;
        

        public bool HasSufficientAmmunition(AmmunitionItem requiredAmmunition, int requiredAmount = 1)
        {
            return currentAmmunition != null && (requiredAmmunition == currentAmmunition) && amount >= requiredAmount;
        }

        public void SpendAmmo(int amountToSpend)
        {
            amount -= amountToSpend;
            if (amount <= 0)
            {
                currentAmmunition =null;
                amount = 0;
            }
            quiverUpdated?.Invoke();
        }

        public AmmunitionItem GetItem() => currentAmmunition;
        public int GetAmount() => amount;

        public void RemoveAmmunition(int removeAmount = -1)
        {
            if (removeAmount < 0) removeAmount = amount;
            amount -= removeAmount;
            if (amount <= 0)
            {
                currentAmmunition = null;
                amount = 0;
            }
            quiverUpdated?.Invoke();
        }

        public void AddAmmunition(AmmunitionItem item, int number)
        {
            currentAmmunition = item;
            amount = number;
            quiverUpdated?.Invoke();
        }

        public JToken CaptureAsJToken()
        {
            JObject state = new JObject
            {
                [nameof(currentAmmunition)] = currentAmmunition != null ? currentAmmunition.GetItemID() : "",
                [nameof(amount)] = amount
            };
            return state;
        }

        public void RestoreFromJToken(JToken state)
        {
            currentAmmunition = null;
            amount = 0;
            if (state is JObject stateDict)
            {
                if (stateDict.TryGetValue(nameof(currentAmmunition), out JToken token))
                {
                    var item = InventoryItem.GetFromID(token.ToString());
                    if (item is AmmunitionItem ammunitionItem)
                    {
                        currentAmmunition = ammunitionItem;
                        if (stateDict.TryGetValue(nameof(amount), out JToken amountToken) &&
                            int.TryParse(amountToken.ToString(), out int number))
                        {
                            amount = number;
                        }
                    }
                }
            }
            quiverUpdated?.Invoke();
        }

        public int SavePriority()
        {
            return 1;
        }
    }
}

In Fighter, before setting the animator trigger to attack, you’ll need to check to see if the config is a projectile, if it has an ammo requirement, and if there is sufficient ammo of that type in the quiver:

        private void AttackBehaviour()
        {
            transform.LookAt(target.transform);
            GetComponent<Mover>().MoveTo(transform.position, 0);
            if (timeSinceLastAttack > timeBetweenAttacks && CheckForProjectileAndSufficientAmmo())
            {
                // This will trigger the Hit() event.
                TriggerAttack();
                timeSinceLastAttack = 0;
            }
        }

        bool CheckForProjectileAndSufficientAmmo()
        {
            if (!currentWeaponConfig.HasProjectile()) return true;
            if (currentWeaponConfig.GetAmmunitionItem() == null) return true; //Edited
            if (GetComponent<Quiver>().HasSufficientAmmunition(currentWeaponConfig.GetAmmunitionItem())) return true;
            return false;
        }

And when we spawn the projectile, we’ll need to deduct one from the Quiver

if (currentWeaponConfig.HasProjectile())
            {
                if (currentWeaponConfig.GetAmmunitionItem())
                {
                    GetComponent<Quiver>().SpendAmmo(1);
                }
                currentWeaponConfig.LaunchProjectile(rightHandTransform, leftHandTransform, target, gameObject, damage);
            }

All that’s left at this point is the EquipmentUI…
You’ll need a new QuiverItemUI, which will be modelled after EquipSlotUI, but will be quite a bit stripped down…

The class will need a reference to the player’s Quiver (you could use Inventory.GetPlayerInventory().GetComponent<Quiver>(); to get this…
As the quiver is just one slot, you don’t need to worry about a slot, so the methods will simply have to call methods on Quiver, for example, to get the item, simply use quiver.GetItem() (or quiver.GetNumber())…
You’ll need to implement the interfaces that we use in InventorySlotUI and EquipSlotUI to allow dragging/dropping as well as tooltips…

In the editor, you’ll need to build a prefab with a QuiverItemUI component. You might start by duplicating an EquipmentSlot prefab and replacing the EquipSlotUI with QuiverItemUI. Once this is completed, you can add the QuiverItemUI to the Equipment canvas.

So that’s your challenge

One thing I just noticed… it might be better to start with an InventoryItemUI instead of an EquipSlotUI, so you have the number bubble…

Wait, this goes in the ‘Fighter.Hit()’ function, right? I found a similar if statement, so I replaced it with this one :slight_smile:

I didn’t fully understand where this comes into play either tbh. Is this indicating that for my challenge, I do the changes based on ‘InventoryItemUI’ instead of ‘EquipSlotUI’…?

Oh, and one last question, ‘Fighter.cs’ keeps complaining about a missing ‘GetAmmunitionItem()’ function from ‘WeaponConfig.cs’. Is that part of my coding challenge, or did we miss something out?

That is when we spawn the projectile, yes (well, when we instruct the weaponConfig to spawn one

You’re going to, for the most part, base this on EquipSlotUI rather than the InventoryItemUI script, because we don’t need all the excess baggage of InventoryItemUI…
But the prefab itself is easier to make from an InventorySlot… but… I further realized that Sam didn’t actually have you prefab that in the course, but he did prefab the ActionSlot prefab… This means the ActionSlot Prefab has a quantity number GameObject as well…
So what I did was:
Duplicate the ActionSlot prefab
Add the QuiverSlotUI script
Remove the ActionSlotUI script
Hook up everything to the QuiverSlotUI script.

I’m going to throw you a bone, as there’s a lot of moving parts here and give you my QuiverSlotUI script:

using System;
using GameDevTV.Core.UI.Dragging;
using GameDevTV.Inventories;
using RPG.Inventories;
using TMPro;
using UnityEngine;

namespace GameDevTV.UI.Inventories
{
    public class QuiverSlotUI : MonoBehaviour, IItemHolder, IDragContainer<InventoryItem>
    {
        [SerializeField] InventoryItemIcon icon = null;
        [SerializeField] private GameObject itemCounter;
        [SerializeField] private TextMeshProUGUI number;
        
        private Quiver quiver;

        private void Awake()
        {
            quiver = Inventory.GetPlayerInventory().GetComponent<Quiver>();
            quiver.quiverUpdated += RedrawUI;
        }

        void RedrawUI()
        {
            icon.SetItem(GetItem());
            itemCounter.SetActive(GetNumber()>0);
            number.text = $"{GetNumber()}";
        }

        public InventoryItem GetItem()
        {
            return quiver.GetItem();
        }

        public int GetNumber()
        {
            return quiver.GetAmount();
        }

        public void RemoveItems(int number)
        {
            quiver.RemoveAmmunition(number);
        }

        public int MaxAcceptable(InventoryItem item)
        {
            if (item is AmmunitionItem ammunitionItem)
            {
                return Int32.MaxValue;
            }

            return 0;
        }

        public void AddItems(InventoryItem item, int number)
        {
            if(item is AmmunitionItem ammunitionItem)
            {
                quiver.AddAmmunition(ammunitionItem, number);
            }
        }
    }
}

(I hadn’t written it yet when I posted the above walkthrough)…
Now it just needs to be hooked up.

Alright I’ll give this whole thing a go, but we still have the problem of the non-existing function in ‘WeaponConfig.cs’, which is ‘GetAmmunitionItem()’

If I had a [SerializeField] private AmmunitionItem ammunitionItem; in my WeaponConfig… what would be a good way to expose it publicly?..

Playtesting revealed a slight miscalculation in one of the Fighter methods, I was returning true if the weaponConfig had the Ammunition… here’s the revised version of that method:

        bool CheckForProjectileAndSufficientAmmo()
        {
            if (!currentWeaponConfig.HasProjectile()) return true;
            if (currentWeaponConfig.GetAmmunitionItem() == null) return true; //Edited
            if (GetComponent<Quiver>().HasSufficientAmmunition(currentWeaponConfig.GetAmmunitionItem())) return true;
            return false;
        }

I tried coding the getter to solve the issue. Based on everything I learned so far from this wild game I’m trying to code, is this how we do it?

public AmmunitionItem GetAmmunitionItem() {

            return ammunitionItem;

        }

It’s the only Data type inheritor that doesn’t result in any wild errors, but I want to double check as well :slight_smile:

Or, in the fancy advanced style :stuck_out_tongue_winking_eye: - more like this:

public AmmunitionItem GetAmmunitionItem() => ammunitionItem;

You might have noticed Sam mentioning things like “and of course, we’ll need a getter” throughout the course, and this is generally what he does. Your answer is correct.

And I was just typing out a quick lesson on =>, well done!

You can use this whenever you’re simply returning a value that only requires one line of code (i.e. where the method body is simply return somevalue.

Ahh, I started picking up on the signals. Sure, I might not be the smartest programmer, but the more functions I ask for and code side by side with you, or anyone in this case, the more I learn :smiley: - matter of fact, @bixarrio 's Crafting system (before he changed it, and tbh, I want the newer version xD. I can think of a crazy scenario that might find that useful xD) was a great chance for me to get a better idea of how coding generally flows, and it was one of the many ‘Aha!’ moments that hit me (and it lowkey helps that my new daytime job is being a JavaScript and Python teacher :stuck_out_tongue_winking_eye: - coding my game is not only fun, but it also reinforces the stuff I teach)

But if there’s something I want to learn, after I finish implementing this quiver, is the ‘lambda’ function values that can be stored and restored later thing that we mentioned yesterday, in the ‘Equipment Drop’ function we coded… I didn’t really catch that

=> can also be used in other situations, as long as the code in the brackets would only be one line… That being said, because I have a background in Functional Programming, my preference is to treat these sorts of getters as “pure” functions… This isn’t a hard and fast rule, just my personal guidelines… I will only ever use the => if it’s a pure function. A pure function takes in no inputs, and returns a value based only on data within the class.
So while you could do something like this:

public int RollDice(int numberOfSides) => Random.Range(0, numberOfSides)+1;

it would not be a pure function, because A) it takes in an input, and B) the answer will be different every call even if nothing in the class changed.

public void SetAmmunitionItem(AmmunitionItem item) => ammunitionItem = item;

is also not pure because it changes a value within the class. This means it’s really an action, because it acts upon the class.

To be clear, in C#/Unity, both of the above statements are 100% legal, and programmers do it all the time. I enforce the “pure” rule on myself to clearly differentiate between “pure functions” and “actions”.

Is this the correct assignment for the ‘QuiverSlotUI’ variables? I also want to change the icon to another one of the Equipment icons

Now I see how you’d be able to solve a problem that would take me weeks to figure out, if not months or years, in a single night :stuck_out_tongue_winking_eye:

Got it. Personally though, my brain hasn’t fully caught up with sugarcoats like this one, so it’ll probably take me seeing it in future code we program together multiple times before I fully grasp the idea. For now though, I memorized 2 (3) things (I’m guessing them):

  1. A ‘=>’ sugarcoat is only applied only when we have no inputs (not necessary, but preferrable)
  2. The function must have values relying on the class values (so a randomizer gives a different output everytime we give in an input, so it’s wrong)
  3. The function must be a one line function (i.e: Function ‘on the fly’, if I remember correctly from my Engineering University days)

Ahh, I didn’t catch that…

That looks right!

OK so let me guess, the next step to get this to work is to create an InventoryItem Arrow, assign that to the Quiver and fill up all it’s basic information and then test the system using ranged weapons, right? No more undercover Unity connections?

I’ll proceed to actually placing the ‘QuiverSlot’ in the ‘Equipment’ Slots as well :slight_smile:

Pretty much… don’t forget to assign the AmmunitionItem to the bow’s WeaponConfig

I can tell you that after adding it to the course project, it works just fine.
When I move this functionality into Spellborne Hunter, however, I’m going to have the bow check inventory for the appropriate ammunition rather than dragging it into the Equipment dialogue. Basically my personal preference is, if you equip the bow, then you can use the bow as long as the ammo is in your inventory.

Privacy & Terms