RASMUS GÖRANSSON

GAME PROGRAMMER

Inventory Shop System

The Inventory Shop System is as it sounds, the project consist of an Inventory System and a Shop System.
There is also an added bonus of a Saving system that keeps track of all different inventories: Player, Chests and Shops.

The Inventory System is inspired by Minecraft's inventory. You can pick up, place and split an item slot.

The Shop System is fully customizable and allows you to choose what items should be sold and how many are available. Additionally you can set how much gold the shop keeper should have and if items should have a markup or not.

Both systems are made to be very flexible and easily adapted into a different style.

                    
[Serializable]
public class InventorySystem
{
    [SerializeField] private List<InventorySlot> _inventorySlots;
    private int _gold;

    
    #region Properties

    public List<InventorySlot> InventorySlots => _inventorySlots;

    public int InventorySize => _inventorySlots.Count;

    public int Gold => _gold;

    #endregion

    // event for changes in inventory slot
    public UnityAction<InventorySlot> OnInventorySlotChanged;


    // Constuctor
    public InventorySystem(int inventorySize)
    {
        _gold = 0;
        CreateInventory(inventorySize);
    }

    public InventorySystem(int inventorySize, int gold)
    {
        _gold = gold;
        CreateInventory(inventorySize);
    }

    private void CreateInventory(int inventorySize)
    {
        _inventorySlots = new List<InventorySlot>(inventorySize);

        for (int i = 0; i < inventorySize; i++)
        {
            _inventorySlots.Add(new InventorySlot());
        }
    }



    public bool AddItemToInventory(InventoryItemData inventoryItemData, int amount)
    {
        // item exist in inventory - add amount to item stack
        if (ContainsItem(inventoryItemData, out List<InventorySlot> inventorySlots))
        {
            // check all inventory slots with item - add to first available slot
            foreach (InventorySlot inventorySlot in inventorySlots)
            {
                if (inventorySlot.HasAvailableAmountInStack(amount))
                {
                    inventorySlot.AddToStack(amount);
                    OnInventorySlotChanged?.Invoke(inventorySlot);
                    return true;
                }
            }
        }
        
        // item doesn't exist in inventory - has available slot exist in inventory - add item(s) to first available inventory slot
        if (HasAvailableInventorySlot(out InventorySlot availableInventorySlot))
        {
            if (availableInventorySlot.HasAvailableAmountInStack(amount)) // available inventory slot has available amount
            {
                availableInventorySlot.UpdateInventorySlot(inventoryItemData, amount);
                OnInventorySlotChanged?.Invoke(availableInventorySlot);
                return true;
            }
        }

        return false;
    }

    // inventory has item - return all inventory slots with items
    public bool ContainsItem(InventoryItemData inventoryItemData, out List<InventorySlot> inventorySlots)
    {
        inventorySlots = _inventorySlots.Where(slot => slot.InventoryItemData == inventoryItemData).ToList();

        if (inventorySlots.Count > 1) return true;
        
        return false;
    }

    public bool HasAvailableInventorySlot(out InventorySlot availableInventorySlot)
    {
        // get first available inventory slot
        availableInventorySlot = _inventorySlots.FirstOrDefault(slot => slot.InventoryItemData == null);
        
        if (availableInventorySlot != null) return true;

        return false;
    }


    public bool HasAvailableInventorySlots(Dictionary<InventoryItemData, int> shoppingCart)
    {
        var tempInventorySystem = new InventorySystem(InventorySize);

        // clone current inventory
        for (int i = 0; i < InventorySize; i++)
        {
            tempInventorySystem.InventorySlots[i].AssignItemData(InventorySlots[i].InventoryItemData, InventorySlots[i].StackSize);
        }

        foreach (var kvp in shoppingCart)
        {
            for (int i = 0; i < kvp.Value; i++)
            {
                if (!tempInventorySystem.AddItemToInventory(kvp.Key, 1)) return false;
            }
        }

        return true;
    }

    public void UseGold(int priceTotal)
    {
        _gold -= priceTotal;
    }
    public void GetGold(int finalPrice)
    {
        _gold += finalPrice;
    }

    public Dictionary<InventoryItemData, int> GetAllInventoryItems()
    {
        // collection of specific item
        Dictionary<InventoryItemData, int> distinctItems = new Dictionary<InventoryItemData, int>();

        foreach (var slot in _inventorySlots)
        {
            if (slot.InventoryItemData == null) continue;

            if (!distinctItems.ContainsKey(slot.InventoryItemData))
            {
                distinctItems.Add(slot.InventoryItemData, slot.StackSize);
            }
            else
            {
                distinctItems[slot.InventoryItemData] += slot.StackSize;
            }
        }

        return distinctItems;
    }

    

    public void RemoveItemsFromInventory(InventoryItemData inventoryItemData, int amount)
    {
        if (ContainsItem(inventoryItemData, out List<InventorySlot> inventorySlots))
        {
            foreach(var slot in inventorySlots)
            {
                int stackSize = slot.StackSize;

                if (stackSize > amount)
                {
                    slot.RemoveFromStack(amount);
                }
                else
                {
                    slot.RemoveFromStack(stackSize);
                    amount -= stackSize;
                }

                OnInventorySlotChanged?.Invoke(slot);
            }
        }
    }
}

                    
                

Item Slot

The InventoryItemSlot class is a base class for managing individual inventory slots. It handles items assignment and stacking.

Item management allows assigning items to a slot by either adding to an existing item stack or completely overwriting it with a new item. It also tracks the item type, it's stack size and ID.

Stacking manages adding and removing items from an item stack, and clearing the slot when the stack size reach zero.

                    
[Serializable]
public class InventorySlot : InventoryItemSlot
{
    //Constructors
    public InventorySlot(InventoryItemData inventoryItemData, int amount)
    {
        _inventoryItemData = inventoryItemData;
        _itemID = _inventoryItemData.ID;
        _stackSize = amount;
    }

    public InventorySlot()
    {
        ClearInventorySlot();
    }


    // update inventory slot with new values
    public void UpdateInventorySlot(InventoryItemData inventoryItemData, int amount)
    {
        _inventoryItemData = inventoryItemData;
        _itemID = _inventoryItemData.ID;
        _stackSize = amount;
    }


    // can add amount to stacksize - return amount left in stack
    public bool HasAvailableAmountInStack(int amount, out int availableAmount)
    {
        availableAmount = InventoryItemData.MaxStackSize - _stackSize;
        return HasAvailableAmountInStack(amount);
    }

    // can add amount to stacksize
    public bool HasAvailableAmountInStack(int amount)
    {
        if (_inventoryItemData == null || _inventoryItemData != null && _stackSize + amount <= _inventoryItemData.MaxStackSize) return true;
        return false;
    }


    // splits item stack in 2
    public bool SplitItemStack(out InventorySlot splitInventorySlot)
    {
        if (_stackSize <= 1)
        {
            splitInventorySlot = null;
            return false;
        }

        int halfStackSize = Mathf.RoundToInt(_stackSize / 2);
        RemoveFromStack(halfStackSize);

        splitInventorySlot = new InventorySlot(_inventoryItemData, halfStackSize);
        return true;
    }
}

                    
                

Inventory Slot UI

The InventorySlotUI class manages the visual representation of an inventory slot.

It displays an item's sprite and stack size in the UI and is linked to an InventorySlot, which holds the actual item data.

The UI slot is initialized and updated based on the values of the underlying InventorySlot. It can also be refreshed without assigning new values, ensuring the UI is synced properly.

When a slot is clicked, the appropriate callback is triggered in the InventoryDisplay class.

                    
public abstract class InventoryDisplay : MonoBehaviour
{
    [SerializeField] SelectedItemData _selectedInventoryItem;

    protected InventorySystem _inventorySystem;

    protected Dictionary<InventorySlotUI, InventorySlot> _inventorySlotDictionary;


    #region Properties

    public InventorySystem InventorySystem => _inventorySystem;

    public Dictionary<InventorySlotUI, InventorySlot> InventorySlotDictionary => _inventorySlotDictionary;

    #endregion


    protected virtual void Start()
    {

    }

    public abstract void AssignInventorySlot(InventorySystem inventorySystem, int offset);

    public virtual void UpdateInventorySlot(InventorySlot updatedInventorySlot)
    {
        foreach(var inventorySlot in _inventorySlotDictionary)
        {
            // if value of inventory slots match - update UI inventory slot with new value
            if (inventorySlot.Value == updatedInventorySlot) 
            {
                inventorySlot.Key.UpdateInventorySlotUI(updatedInventorySlot);
            }
        }
    }

    public void InventorySlotClicked(InventorySlotUI clickedInventorySlotUI)
    {
        // inventory slot has item - player has NO selected item - select item
        if (clickedInventorySlotUI.AssignedInventorySlot.InventoryItemData != null && _selectedInventoryItem.InventorySlot.InventoryItemData == null)
        {
            // player hold shift key - split item stack
            bool isShiftPressed = Keyboard.current.leftShiftKey.isPressed;
            if (isShiftPressed && clickedInventorySlotUI.AssignedInventorySlot.SplitItemStack(out InventorySlot splitInventorySlot))
            {
                _selectedInventoryItem.UpdateSelectedInventorySlot(splitInventorySlot);
                clickedInventorySlotUI.UpdateInventorySlotUI();
                return;
            }
            else
            {
                _selectedInventoryItem.UpdateSelectedInventorySlot(clickedInventorySlotUI.AssignedInventorySlot);
                clickedInventorySlotUI.ClearInventorySlotUI();
                return;
            }
        }


        // inventory slot has NO item - player has selected item - assign item
        if (clickedInventorySlotUI.AssignedInventorySlot.InventoryItemData == null && _selectedInventoryItem.InventorySlot.InventoryItemData != null)
        {
            clickedInventorySlotUI.AssignedInventorySlot.AssignItemData(_selectedInventoryItem.InventorySlot);
            clickedInventorySlotUI.UpdateInventorySlotUI();
            _selectedInventoryItem.ClearInventorySlot();
            return;
        }


        // inventory slot has item - player has selected item - assign / combine / swap
        if (clickedInventorySlotUI.AssignedInventorySlot.InventoryItemData != null && _selectedInventoryItem.InventorySlot.InventoryItemData != null)
        {
            // not the same item - swap with eachother
            if (clickedInventorySlotUI.AssignedInventorySlot.InventoryItemData != _selectedInventoryItem.InventorySlot.InventoryItemData)
            {
                SwapInventorySlots(clickedInventorySlotUI);
                return;
            }

            // same item - combine
            if (clickedInventorySlotUI.AssignedInventorySlot.InventoryItemData == _selectedInventoryItem.InventorySlot.InventoryItemData)
            {
                // has available amount in clicked inventory slot - no overflow
                if (clickedInventorySlotUI.AssignedInventorySlot.HasAvailableAmountInStack(_selectedInventoryItem.InventorySlot.StackSize))
                {
                    clickedInventorySlotUI.AssignedInventorySlot.AssignItemData(_selectedInventoryItem.InventorySlot); // combine inventory slots in to clicked inventory slot
                    clickedInventorySlotUI.UpdateInventorySlotUI(); // update inventory slot UI
                    _selectedInventoryItem.ClearInventorySlot(); // clear inventory slot
                    return;
                }
                // had not enought available amount - overflow
                else if (!clickedInventorySlotUI.AssignedInventorySlot.HasAvailableAmountInStack(_selectedInventoryItem.InventorySlot.StackSize, out int availableAmount))
                {
                    if (availableAmount < 1) // stack size if full - can Not combine - swap items
                    {
                        SwapInventorySlots(clickedInventorySlotUI);
                        return;
                    }
                    // add until max stack size - remove added amount from current selected item
                    else
                    {
                        clickedInventorySlotUI.AssignedInventorySlot.AddToStack(availableAmount); // add available amount to clicked inventory slot
                        clickedInventorySlotUI.UpdateInventorySlotUI(); // update clicked inventory slot

                        int newAmountOnSelectedInventoryItem = _selectedInventoryItem.InventorySlot.StackSize - availableAmount; // create new size for current selected item

                        InventorySlot newSelectedInventoryItem = new InventorySlot(_selectedInventoryItem.InventorySlot.InventoryItemData, newAmountOnSelectedInventoryItem); // create new selected inventory item with new values
                        _selectedInventoryItem.ClearInventorySlot(); // clear selected item
                        _selectedInventoryItem.UpdateSelectedInventorySlot(newSelectedInventoryItem); // update selected item with new selected inventory item
                        return;
                    }
                }
            }

        }


    }

    private void SwapInventorySlots(InventorySlotUI clickedInventorySlotUI)
    {
        // new temporary(cloned) inventory slot
       InventorySlot tempInventorySlot = new InventorySlot(_selectedInventoryItem.InventorySlot.InventoryItemData, _selectedInventoryItem.InventorySlot.StackSize);
       
        _selectedInventoryItem.ClearInventorySlot(); // clear inventory slot
        _selectedInventoryItem.UpdateSelectedInventorySlot(clickedInventorySlotUI.AssignedInventorySlot); // update inventory slot with new values
        

        clickedInventorySlotUI.ClearInventorySlotUI(); // clear inventory slot UI
        clickedInventorySlotUI.AssignedInventorySlot.AssignItemData(tempInventorySlot); // assing inventory slot UI with cloned values
        clickedInventorySlotUI.UpdateInventorySlotUI(); // update inventory slot UI
    }

}

                    
                

Shop System

The ShopSystem class manages shop inventory slots, inherited from InventoryItemSlot, and handles item transactions.

It maintains a list of inventory slots containing specific items, tracks available gold, and supports buying and selling with adjustable price markups.

Gold and inventory are updated accordingly when transactions occur.

The system checks if an item already exists in stock, allowing it to add, merge or remove items based on the action taken.

                    
public class ShopKeeperDisplay : MonoBehaviour
{
    [SerializeField] private ShopInventorySlotUI _shopSlotInventoryPrefab;
    [SerializeField] private ShoppingCartItemUI _shoppingCartItemPrefab;


    [SerializeField] private Button _buyTabButton;
    [SerializeField] private Button _sellTabButton;

    [Header("Shopping Cart:")]

    [SerializeField] private TextMeshProUGUI _priceTotalText;
    [SerializeField] private TextMeshProUGUI _playerGoldText;
    [SerializeField] private TextMeshProUGUI _shopGoldText;

    [Space(10)]

    [SerializeField] private Button _buySellButton;
    [SerializeField] private TextMeshProUGUI _buySellButtonText;


    [Header("Item Preview Section:")]

    [SerializeField] private Image _itemPreviewSprite;
    [SerializeField] private TextMeshProUGUI _itemPreviewName;
    [SerializeField] private TextMeshProUGUI _itemPreviewDescription;

    [SerializeField] private GameObject _itemListContentPanel;
    [SerializeField] private GameObject _shoppingCartContentPanel;


    private ShopSystem _shopSystem;
    private PlayerInventory _playerInventory;

    private Dictionary<InventoryItemData, int> _shoppingCart = new Dictionary<InventoryItemData, int>();
    private Dictionary<InventoryItemData, ShoppingCartItemUI> _shoppingCartUI = new Dictionary<InventoryItemData, ShoppingCartItemUI>();

    private int _priceTotal;
    private bool _isSelling;

    public void DisplayShopWindow(ShopSystem shopSystem, PlayerInventory playerInventory)
    {
        _shopSystem = shopSystem;
        _playerInventory = playerInventory;

        RefreshShopDisplay();
    }

    private void RefreshShopDisplay()
    {
        if (_buySellButton != null)
        {
            _buySellButtonText.text = _isSelling ? "Sell" : "Buy";
            _buySellButton.onClick?.RemoveAllListeners();
            
            if (_isSelling)
            {
                _buySellButton.onClick.AddListener(SellItems);
            }
            else
            {
                _buySellButton.onClick.AddListener(BuyItems);
            }
        }

        ClearShopInventorySlots();
        ClearItemPreview();

        _priceTotalText.enabled = false;
        _buySellButton.gameObject.SetActive(false);
        _priceTotal = 0;
        _playerGoldText.text = $"Player Gold: {_playerInventory.PrimaryInventorySystem.Gold}G";
        _shopGoldText.text = $"Shop Gold: {_shopSystem.AvailableGold}G";

        if (_isSelling) DisplayPlayerInventory();
        else DisplayShopInventory();

    }

    private void BuyItems()
    {
        if (_playerInventory.PrimaryInventorySystem.Gold < _priceTotal) return;
        if (!_playerInventory.PrimaryInventorySystem.HasAvailableInventorySlots(_shoppingCart)) return;

        foreach (var kvp in _shoppingCart)
        {
            _shopSystem.BuyItem(kvp.Key, kvp.Value);

            for (int i = 0; i < kvp.Value; i++)
            {
                _playerInventory.PrimaryInventorySystem.AddItemToInventory(kvp.Key, 1);
            }
        }

        _shopSystem.GetGold(_priceTotal);
        _playerInventory.PrimaryInventorySystem.UseGold(_priceTotal);

        RefreshShopDisplay();
    }

    private void SellItems()
    {
        if (_shopSystem.AvailableGold < _priceTotal) return;

        foreach (var kvp in _shoppingCart)
        {
            int finalPrice = GetModifiedPrice(kvp.Key, kvp.Value, _shopSystem.SellMarkUp);

            _shopSystem.SellItem(kvp.Key, kvp.Value, finalPrice);

            _playerInventory.PrimaryInventorySystem.GetGold(finalPrice);
            _playerInventory.PrimaryInventorySystem.RemoveItemsFromInventory(kvp.Key, kvp.Value);
        }

        RefreshShopDisplay();
    }
    

    private void ClearShopInventorySlots()
    {
        _shoppingCart = new Dictionary<InventoryItemData, int>();
        _shoppingCartUI = new Dictionary<InventoryItemData, ShoppingCartItemUI>();

        foreach (var item in _itemListContentPanel.transform.Cast<Transform>())
        {
            Destroy(item.gameObject);
        }

        foreach (var item in _shoppingCartContentPanel.transform.Cast<Transform>())
        {
            Destroy(item.gameObject);
        }
    }

    private void DisplayShopInventory()
    {
        foreach (var slot in _shopSystem.ShopInventory)
        {
            if (slot.InventoryItemData == null)
            {
                continue;
            }

            ShopInventorySlotUI shopInventorySlotUI = Instantiate(_shopSlotInventoryPrefab, _itemListContentPanel.transform);

            shopInventorySlotUI.Initialize(slot, _shopSystem.BuyMarkUp);
        }
    }

    private void DisplayPlayerInventory()
    {
        foreach (var item in _playerInventory.PrimaryInventorySystem.GetAllInventoryItems())
        {
            ShopInventorySlot tempShopInventorySlot = new ShopInventorySlot();

            tempShopInventorySlot.AssignItemData(item.Key, item.Value);

            ShopInventorySlotUI shopInventorySlotUI = Instantiate(_shopSlotInventoryPrefab, _itemListContentPanel.transform);
            shopInventorySlotUI.Initialize(tempShopInventorySlot, _shopSystem.SellMarkUp);
        }
    }

    public void AddShopItemToCart(ShopInventorySlotUI shopInventorySlotUI)
    {
        InventoryItemData inventoryItemData = shopInventorySlotUI.AssignedShopInventorySlot.InventoryItemData;

        UpdateItemPreview(shopInventorySlotUI);

        int finalPrice = GetModifiedPrice(inventoryItemData, 1, shopInventorySlotUI.Markup);

        // item already in cart - increase amount
        if (_shoppingCart.ContainsKey(inventoryItemData))
        {
            _shoppingCart[inventoryItemData]++;
            string previewItemData = $"{inventoryItemData.Name} - {finalPrice}G - x{_shoppingCart[inventoryItemData]}";
            _shoppingCartUI[inventoryItemData].SetItemPreviewText(previewItemData);
        }
        // item not in cart - add item to cart
        else
        {
            _shoppingCart.Add(inventoryItemData, 1);

            ShoppingCartItemUI shoppingCartItemUI = Instantiate(_shoppingCartItemPrefab, _shoppingCartContentPanel.transform);
            string previewItemData = $"{inventoryItemData.Name} - {finalPrice}G - x1";

            shoppingCartItemUI.SetItemPreviewText(previewItemData);
            _shoppingCartUI.Add(inventoryItemData, shoppingCartItemUI);        
        }

        // set total price of all items
        _priceTotal += finalPrice;
        _priceTotalText.text = $"Total: {_priceTotal}G";

        if (_priceTotal > 0 && !_priceTotalText.IsActive())
        {
            _priceTotalText.enabled = true;
            _buySellButton.gameObject.SetActive(true);
        }

        HasAvailableGold();
    }

    public void RemoveShopItemFromCart(ShopInventorySlotUI shopInventorySlotUI)
    {
        InventoryItemData inventoryItemData = shopInventorySlotUI.AssignedShopInventorySlot.InventoryItemData;

        int finalPrice = GetModifiedPrice(inventoryItemData, 1, shopInventorySlotUI.Markup);

        // item already in cart - decrease amount
        if (_shoppingCart.ContainsKey(inventoryItemData))
        {
            _shoppingCart[inventoryItemData]--;
            string previewItemData = $"{inventoryItemData.Name} - {finalPrice}G - x{_shoppingCart[inventoryItemData]}";
            _shoppingCartUI[inventoryItemData].SetItemPreviewText(previewItemData);

            if (_shoppingCart[inventoryItemData] <= 0)
            {
                _shoppingCart.Remove(inventoryItemData);
                GameObject shoppingCartItemUI = _shoppingCartUI[inventoryItemData].gameObject;
                _shoppingCartUI.Remove(inventoryItemData);
                Destroy(shoppingCartItemUI);
            }
        }

        _priceTotal -= finalPrice;
        _priceTotalText.text = $"Total: {_priceTotal}G";

        if (_priceTotal <= 0 &&  _priceTotalText.IsActive())
        {
            _priceTotalText.enabled = false;
            _buySellButton.gameObject.SetActive(false);
            ClearItemPreview();
            return;
        }

        HasAvailableGold();
    }


    private void UpdateItemPreview(ShopInventorySlotUI shopInventorySlotUI)
    {
        InventoryItemData inventoryItemData = shopInventorySlotUI.AssignedShopInventorySlot.InventoryItemData;

        _itemPreviewSprite.sprite = inventoryItemData.Sprite;
        _itemPreviewSprite.color = Color.white;
        _itemPreviewName.text = inventoryItemData.Name;
        _itemPreviewDescription.text = inventoryItemData.Description;
    }

    private void ClearItemPreview()
    {
        _itemPreviewSprite.sprite = null;
        _itemPreviewSprite.color = Color.clear;
        _itemPreviewName.text = "";
        _itemPreviewDescription.text = "";
    }

    public static int GetModifiedPrice(InventoryItemData inventoryItemData, int amount, float markup)
    {
        int basePrice = inventoryItemData.GoldValue * amount;

        return Mathf.FloorToInt(basePrice + basePrice * markup);
    }
    private void HasAvailableGold()
    {
        int availableGold = _isSelling ? _shopSystem.AvailableGold : _playerInventory.PrimaryInventorySystem.Gold;

        _priceTotalText.color = _priceTotal > availableGold ? Color.red : Color.white;

        if (_isSelling || _playerInventory.PrimaryInventorySystem.HasAvailableInventorySlots(_shoppingCart)) return;

        _priceTotalText.text = "Not enough space in Inventory!";
        _priceTotalText.color = Color.red;

    }


    public void OnBuyTabPressed()
    {
        _isSelling = false;
        RefreshShopDisplay();
    }

    public void OnSellTabPressed()
    {
        _isSelling = true;
        RefreshShopDisplay();
    }

}

                    
                

Save System

The SaveLoad class manages saving, loading and deleting game data.

It saves the game data in JSON format to a specified file, loads the data from the file, and triggeres a callback when loading. If a save file exists, it can also be deleted.