Getting Attributes from Equip/Unequip (UIS)

r@t

New member
Hey, I'm having difficulty setting up a system of attribute adding/removal.

When equipping an item I want to "add stats".
When removing the item, I want to "remove stats".
When swapping items, I want to "remove original item stats, then replace it with the new items stats".
When dropping, I want to "if Equipped, then remove stats when dropped, else don't remove stats when dropped". Currently it removes stats when UnEquipped since I don't know how to perform a "isEquipped" from the drop item action. (that means value goes negative when no value should change, since it was never equipped)

The code below was a custom solution that doesn't use the Event system. We don't know how to properly implement the feature above. We're just making an attempt to get it working.

1688821125317.png
1688821153358.png

Here is my custom PlayerStatsManager that receives the data and updates the Stats/UI. It interacts with the UIS item action (see next class below). It calls "ApplyAtributeStats" when called through Equip/Unequip item actions.

It cycles through the entire list of attributes and checks for specific strings like "AttackRating, Defense Rating, TemperatureRating", and if any of those are true (multiple can be true) then it executes an Add/Remove of that type. Not the best solution but it seems to work.

C#:
using UnityEngine;
using Item = Opsive.UltimateInventorySystem.Core.Item;

namespace GoblinQuest
{
    public class PlayerStatsManager : MonoBehaviour
    {
        // DESC: CONTAINS ALL PLAYER STATS
        public static PlayerStatsManager Instance;

        [Header("Armor")]
        public int defenseRating;

        [Header("Body Temperature")]
        public int temperatureRating;

        [Header("Attack")]
        public int attackRating;
        public int attackSpeed;

        [Header("Movement Speed")]
        public int defaultMoveSpeed;

        private void Awake()
        {
            Instance = this;
        }

        // ATTRIBUTES CALLED BY ITEM ACTION (SEE "EQUIP/UNEQUIP" OR "DROP" ITEM ACTION)
        public void ApplyAttributeStats(Item item, bool isAdding)
        {
            // Loops through the Item attributes.
            var includeItemDefinitionAttributes = true;
            var includeItemCategoryAttributes = false;
            var attributeCount = item.GetAttributeCount(includeItemDefinitionAttributes, includeItemCategoryAttributes);

            for (int i = 0; i < attributeCount; i++)
            {
                SetAttribute(item, isAdding, i);
            }
        }

        // SETS ATTRIBUTE FOR THE FOR LOOP
        private void SetAttribute(Item item, bool isAdding, int i)
        {
            var attribute = item.GetAttributeAt(i, true, true);

            if (attribute.Name == "AttackRating")
            {
                int attackValue = (int)attribute.GetValueAsObject();
                //Debug.Log("Found Attack: " + attackValue);

                if (isAdding)
                {
                    //Debug.Log("Added Attack: " + attackValue);
                    AddAttack(attackValue);
                }
                else
                {
                    //Debug.Log("Removed Attack: " + attackValue);
                    RemoveAttack(attackValue);
                }
            }

            if (attribute.Name == "DefenseRating")
            {
                int attackValue = (int)attribute.GetValueAsObject();
                //Debug.Log("Found Attack: " + attackValue);

                if (isAdding)
                {
                    //Debug.Log("Added Attack: " + attackValue);
                    AddDefense(attackValue);
                }
                else
                {
                    //Debug.Log("Removed Attack: " + attackValue);
                    RemoveDefense(attackValue);
                }
            }

            if (attribute.Name == "TemperatureRating")
            {
                int attackValue = (int)attribute.GetValueAsObject();
                //Debug.Log("Found Attack: " + attackValue);

                if (isAdding)
                {
                    //Debug.Log("Added Attack: " + attackValue);
                    AddTemperature(attackValue);
                }
                else
                {
                    //Debug.Log("Removed Attack: " + attackValue);
                    RemoveTemperature(attackValue);
                }
            }
        }

        //
        // CALLS FUNCTION BASED ON DATA RECIEVED ABOVE!
        //

        // BODY TEMPERATURE -> TOO HOT/COLD = DAMAGE!
        public void AddTemperature(int temperature)
        {
            temperatureRating += temperature;
            AttackArmorDisplayUI.Instance.UpdateRatingsUI();
        }
        public void RemoveTemperature(int temperature)
        {
            temperatureRating -= temperature;
            AttackArmorDisplayUI.Instance.UpdateRatingsUI();
        }

        // ARMOR DEFENSE RATING!
        public void AddDefense(int armor)
        {
            defenseRating += armor;
            AttackArmorDisplayUI.Instance.UpdateRatingsUI();
        }
        public void RemoveDefense(int armor)
        {
            defenseRating -= armor;
            AttackArmorDisplayUI.Instance.UpdateRatingsUI();
        }

        // ATTACK WEAPON RATING!
        public void AddAttack(int attack)
        {
            attackRating += attack;
            AttackArmorDisplayUI.Instance.UpdateRatingsUI();
        }
        public void RemoveAttack(int attack)
        {
            attackRating -= attack;
            AttackArmorDisplayUI.Instance.UpdateRatingsUI();
        }
    }
}
 
Last edited:
Here is the custom item action "EquipWeaponArmorAction" based off MoveToCollectionItemAction.

C#:
using Opsive.UltimateInventorySystem.ItemActions;
using Opsive.UltimateInventorySystem.Core.AttributeSystem;
using Opsive.UltimateInventorySystem.Core.DataStructures;
using Opsive.UltimateInventorySystem.Core.InventoryCollections;
using UnityEngine;
using GoblinQuest;
using System.Runtime.CompilerServices;
using System.Collections.Generic;

/// CUSTOM!! CHECKS FOR EQUIP STATE!
/// Item action used to Move an item from one collection to another. It can be used to equip/unequip items too.

[System.Serializable]
public class EquipWeaponArmorAction : ItemAction
{
    [Tooltip("The first original item collection.")]
    [SerializeField] protected ItemCollectionID m_FirstCollectionID = new ItemCollectionID(null, ItemCollectionPurpose.Main);
    [Tooltip("The second item collection.")]
    [SerializeField] protected ItemCollectionID m_SecondCollectionID = new ItemCollectionID(null, ItemCollectionPurpose.Equipped);
    [Tooltip("The name for the action to move the item from the first to the second item collection.")]
    [SerializeField] protected string moveFromFirstToSecondActionName = "Equip";
    [Tooltip("The name for the action to move the item from the second to the first item collection.")]
    [SerializeField] protected string m_MoveFromSecondToFirstActionName = "Unequip";

    protected bool moveFromFirstToSecond;

    public EquipWeaponArmorAction()
    {
        textField = "Equip Custom";
    }

    /// <param name="itemInfo">The item.</param>
    /// <param name="itemUser">The item user (can be null).</param>
    /// <returns>True if it can be invoked.</returns>
    protected override bool CanInvokeInternal(ItemInfo itemInfo, ItemUser itemUser)
    {
        var item = itemInfo.Item;
        var inventory = itemInfo.Inventory;

        if (inventory == null)
        {
            return false;
        }

        var secondCollection = inventory.GetItemCollection(m_SecondCollectionID);

        if (secondCollection == null) { return false; }

        if (secondCollection.HasItem((1, item)))
        {
            moveFromFirstToSecond = false;
            textField = m_MoveFromSecondToFirstActionName;
        }
        else
        {
            moveFromFirstToSecond = true;
            textField = moveFromFirstToSecondActionName;
        }
        return true;
    }

    /// <param name="itemInfo">The item info.</param>
    /// <param name="itemUser">The item user (can be null).</param>

    protected override void InvokeActionInternal(ItemInfo itemInfo, ItemUser itemUser)
    {
        var item = itemInfo.Item;
        var inventory = itemInfo.Inventory;
        var firstCollection = inventory.GetItemCollection(m_FirstCollectionID);
        var secondCollection = inventory.GetItemCollection(m_SecondCollectionID);

        var originalCollection = moveFromFirstToSecond ? firstCollection : secondCollection;
        var destinationCollection = moveFromFirstToSecond ? secondCollection : firstCollection;

        //This action used to give the item one way and then the other.
        //The action now removes the item, before it adds it to the other collection to allow restrictions to work properly

        var originalItem = originalCollection.RemoveItem(itemInfo);
        var movedItemInfo = ItemInfo.None;

        //Debug.Log("Invoke: " + originalItem.Item.name);

        // CHANGE COLLECTIONS/ETC
        if (destinationCollection is ItemSlotCollection itemSlotCollection)
        {
            var slotIndex = itemSlotCollection.GetTargetSlotIndex(item);
            if (slotIndex != -1)
            {
                var previousItemInSlot = itemSlotCollection.GetItemInfoAtSlot(slotIndex);

                if (previousItemInSlot.Item != null)
                {
                    //If the previous item is stackable don't remove it.
                    if (previousItemInSlot.Item.StackableEquivalentTo(originalItem.Item))
                    {
                        previousItemInSlot = ItemInfo.None;
                    }
                    else
                    {
                        previousItemInSlot = itemSlotCollection.RemoveItem(slotIndex);
                    }
                }

                movedItemInfo = itemSlotCollection.AddItem(originalItem, slotIndex);

                if (previousItemInSlot.Item != null)
                {
                    firstCollection.AddItem(previousItemInSlot);
                    Debug.Log("Item Swapped Out: " + previousItemInSlot.Item.name);
                    PlayerStatsManager.Instance.ApplyAttributeStats(previousItemInSlot.Item, false);

                    Debug.Log("New Item Added: " + item.name);
                    PlayerStatsManager.Instance.ApplyAttributeStats(item, true);
                }
                else
                {
                    Debug.Log("Equipped Item: " + originalItem.Item.name);
                    PlayerStatsManager.Instance.ApplyAttributeStats(item, true);
                }
            }
        }
        else
        {
            movedItemInfo = destinationCollection.AddItem(originalItem);
            Debug.Log("UnEquipped Item: " + originalItem.Item.name);
            PlayerStatsManager.Instance.ApplyAttributeStats(item, false);
        }

        //Not all the item was added, return the items to the default collection.
        if (movedItemInfo.Amount != originalItem.Amount)
        {
            var amountToReturn = originalItem.Amount - movedItemInfo.Amount;
            firstCollection.AddItem((ItemInfo)(amountToReturn, originalItem));
        }
    }
}
 
Here is the custom drop item action, based off the UIS drop item action.

Problem with this setup is that it is unable to detect if item was "equipped" when dropped. So it's removing stats when it shouldn't.
Stats should only go down if item was equipped (as if you've unequipped it, but also dropped it at the same time).

Without a "Remove" function, the stats will persist even when item is dropped. So it requires this remove function- it's just missing the "isEquipped" check for the item that is dropped.

C#:
using Opsive.UltimateInventorySystem.Core.InventoryCollections;
using Opsive.UltimateInventorySystem.Core.DataStructures;
using UnityEngine;
using ObjectPool = Opsive.Shared.Game.ObjectPoolBase;
using Opsive.UltimateInventorySystem.Core;
using GoblinQuest;

namespace Opsive.UltimateInventorySystem.ItemActions
{

    [System.Serializable]
    public class DropItemActionArmorWeapon : ItemAction
    {
        [Tooltip("The pickup item prefab, it must have a ItemPickup component.")]
        [SerializeField] protected GameObject m_PickUpItemPrefab;
        [Tooltip("Drop One item instead of the item amount specified by the item info.")]
        [SerializeField] protected bool m_DropOne;
        [Tooltip("Remove the item that is dropped.")]
        [SerializeField] protected bool m_RemoveOnDrop;
        [Tooltip("The radius where the item should be dropped around the item user.")]
        [SerializeField] protected float m_DropRadius = 2f;
        [Tooltip("The center of the random drop radius.")]
        [SerializeField] protected Vector3 m_CenterOffset;

        public GameObject PickUpItemPrefab { get => m_PickUpItemPrefab; set => m_PickUpItemPrefab = value; }
        public bool DropOne { get => m_DropOne; set => m_DropOne = value; }
        public bool RemoveOnDrop { get => m_RemoveOnDrop; set => m_RemoveOnDrop = value; }
        public float DropRadius { get => m_DropRadius; set => m_DropRadius = value; }
        public Vector3 CenterOffset { get => m_CenterOffset; set => m_CenterOffset = value; }

        protected GameObject m_PickUpGameObject;
        public GameObject PickUpGameObject => m_PickUpGameObject;

        /// <summary>
        /// Default constructor.
        /// </summary>
        public DropItemActionArmorWeapon()
        {
            textField = "Drop";
        }

        /// <summary>
        /// Check if the action can be invoked.
        /// </summary>
        /// <param name="itemInfo">The item.</param>
        /// <param name="itemUser">The item user (can be null).</param>
        /// <returns>True if the action can be invoked.</returns>
        protected override bool CanInvokeInternal(ItemInfo itemInfo, ItemUser itemUser)
        {
            return true;
        }

        /// <summary>
        /// Invoke the action.
        /// </summary>
        /// <param name="itemInfo">The item.</param>
        /// <param name="itemUser">The item user (can be null).</param>
        protected override void InvokeActionInternal(ItemInfo itemInfo, ItemUser itemUser)
        {
            // IF DROPPED AND NOT EQUIPPED: THEN DO NOT REMOVE STATS!

            // PROBLEM: STATS REMOVED ON DROP! WORKS FOR FIXING STATS STAYING WHEN DROPPING.
            // BUT GOES NEGATIVE BECAUSE NO STATE CHECK IS MADE!

            Debug.Log("Dropped Item: " + itemInfo.Item.name);
            PlayerStatsManager.Instance.ApplyAttributeStats(itemInfo.Item, false); // FALSE = REMOVE STATS

            if (m_PickUpItemPrefab == null)
            {
                Debug.LogWarning("Item Pickup Prefab is null on the Drop Item Action.");
                return;
            }

            var gameObject = itemUser?.gameObject ?? itemInfo.Inventory?.gameObject;

            if (gameObject == null)
            {
                Debug.LogWarning("The game object where the Item Pickup should spwaned to is null.");
                return;
            }

            if (m_DropOne) { itemInfo = (1, itemInfo); }

            if (m_RemoveOnDrop)
            {
                itemInfo.ItemCollection?.RemoveItem(itemInfo);
            }

            m_PickUpGameObject = DropItemAction.DropItem(itemInfo, m_PickUpItemPrefab,
                gameObject.transform.position + m_CenterOffset + new Vector3(
                Random.value * m_DropRadius - m_DropRadius / 2f,
                Random.value * m_DropRadius,
                Random.value * m_DropRadius - m_DropRadius / 2f));
        }

        public static GameObject DropItem(ItemInfo itemInfo, GameObject prefab, Vector3 position)
        {
            var pickupGameObject = ObjectPool.Instantiate(prefab, position, Quaternion.identity);
            var itemObject = pickupGameObject.GetComponent<ItemObject>();

            if (itemObject != null)
            {
                itemObject.SetItem(itemInfo.Item);
                itemObject.SetAmount(itemInfo.Amount);
                return pickupGameObject;
            }

            var inventory = pickupGameObject.GetComponent<Inventory>();
            if (inventory != null)
            {
                inventory.MainItemCollection.RemoveAll();
                inventory.AddItem(itemInfo);
                return pickupGameObject;
            }

            Debug.LogWarning("The Item Drop Action could not find the ItemPickup or Inventory Pickup component on the dropped pickup.");
            return pickupGameObject;
        }
    }
}
 
You really should use the event system for this.
Or potentially make a custom ItemCollection... but the event system makes more sense in my opinion.

The Item Actions aren't really appropriate for this "stat" use case, exactly for the reason you descrbribed above. There are so many ways an item can be added/removed from an itemcollection other than ItemCollection. Depending on your game there could be quests, story events, save/load, etc...
You would need to add your function in all of those, not very scalable. So instead use events.


You know that your items are "equipped" when they are part of the Equipment ItemCollection.
So that means when and Item is added to that collection it becomes equipped. And when it is removed from that collection it is "Equipped".

Also one other things I highly recommend is re-computing the stats from scratch each time there is a change. The reason for that is floating point precistion errors. I explain this in the ItemStats page:

I remember an article that was explaining a bug in Wow where repeating the equipping/unequipping action a 100+x times could get all your stats to max or to 0 depending on the item you were equipping.

Coming back to your use case of computing stats

What you should do is recompute your stats from scratch by using the list of items in the equipment collection.

Are you using the "Equipper" component?
If so I highly recommend you use the event "EventNames.c_Equipper_OnChange" like shown the the "ExampleCharacterStats" shown in the documentation page linked above. You can do that in your PlayerStatsManager script and have a function to recompute stats given the list of items within the equipment collection. This can be done in one line of code using the Equipper script
Code:
m_Equipper.GetEquipmentStatInt("Attack");
Make sure the check the ItemStat page, since it shows that example.

If you do not use the Equipper Component I recommend making your own version of it. What it does is monitor items coming in or out of an ItemCollection to know when an item is equipped or not.
Read through that code to understand how you could set up your own.

Also read through the events documentation page:

If you need any more guidance do let me know
 
Top