Gunaraj Poojary
Resume
← Back to Work
Gameplay System · Released

Dynamic Inventory System

A Gameplay System

Unity 6WindowsRPG2025-04 — 2025-05
Credits

Team

Gunaraj Poojary · Gameplay Programmer
Dynamic Inventory System cover
Overview

What This Project Is

A modular inventory model, a presenter that mediates between model & view, a fully featured UI (tabs, grid, overview, quantity selector), a shop generated from ScriptableObjects, and DOTween-powered UI polish.

Highlights

Key Features

  • Three item categories: Weapons (non-stackable), Armor (non-stackable), Resources (stackable)
  • Stack handling for resources with ItemStack helper
  • Automatic compaction of inventory after removals (shifts items left)
  • Tab-based UI with capacity counters (e.g. 12/40)
  • Auto-selection of first occupied slot when opening/switching tabs
  • Shop generated from ScriptableObject database (SOItemDatabase)
  • DOTween-based UI polish: button press/hover, slot select scale, popup animations
  • ScriptableObject-driven animation configuration (SOTweenButtonConfig)
  • Event-driven communication via GameEvents (decoupled systems)
  • MVP (Model–View–Presenter) structure for clean separation and testability
Details

Technology

Technology

Unity 6C#ScriptableObjectsUnity UI (UGUI)DOTween

Architecture

SOLIDOOPObserverObject PoolingMVP
My Role

Contributions

Gameplay

  • Implemented core mechanics
Portfolio

Code Samples

Gameplay System

Inventory System

using System;
using System.Collections.Generic;

/// <summary>
/// Inventory Model (Data class)
/// Responsibilities:
/// - Store items in 3 categories (weapons, armors, resources)
/// - Apply stacking rules
/// - Add / remove items
/// - Compact inventory after removal
/// - Notify presenter/UI of changes
/// </summary>
public class Inventory
{
    // Storage for each category
    private readonly List<InventoryItem> _weapons;
    private readonly List<InventoryItem> _armors;
    private readonly List<InventoryItem> _resources;

    // Tracks how many slots are in use per category
    private int _weaponsCount;
    private int _armorsCount;
    private int _resourceCount;

    /// <summary>
    /// Fired whenever an inventory slot changes.
    /// Presenter listens to this event and updates UI.
    /// </summary>
    public event ItemUpdated OnItemUpdated;
    public delegate void ItemUpdated(int slotIndex, InventoryItem item, ItemType itemType, int usedSlotsCount);

    public Inventory(int weaponCapacity, int armorCapacity, int resourceCapacity)
    {
        _weapons = InitItems(weaponCapacity);
        _armors = InitItems(armorCapacity);
        _resources = InitItems(resourceCapacity);

        _weaponsCount = 0;
        _armorsCount = 0;
        _resourceCount = 0;
    }

    /// <summary>
    /// Creates a list pre-filled with empty InventoryItem objects.
    /// </summary>
    private List<InventoryItem> InitItems(int capacity)
    {
        List<InventoryItem> list = new List<InventoryItem>(capacity);

        for (int i = 0; i < capacity; i++)
            list.Add(new InventoryItem());

        return list;
    }

    /// <summary>
    /// High-level add method used by presenter/shop.
    /// Automatically routes to correct category.
    /// </summary>
    public bool TryAddItem(SOItemConfig config, out int leftover, int amount = 1)
    {
        leftover = amount;

        return config.Type switch
        {
            ItemType.Weapon => TryAddWeapons(config, ref leftover),
            ItemType.Armor => TryAddArmors(config, ref leftover),
            ItemType.Resource => TryAddResources(config, ref leftover),
            _ => false
        };
    }

    /// <summary>
    /// Weapons are non-stackable. Each must occupy an empty slot.
    /// </summary>
    private bool TryAddWeapons(SOItemConfig config, ref int leftover)
    {
        bool addedAny = false;

        while (leftover > 0)
        {
            int slotIndex = GetFirstEmptySlot(_weapons);
            if (slotIndex == -1)
            {
                GameEvents.RaisePopupEvent("Weapon inventory is full!");
                return addedAny;
            }

            _weapons[slotIndex].Init(config);
            leftover--;
            addedAny = true;
            _weaponsCount++;

            OnItemUpdated?.Invoke(slotIndex, _weapons[slotIndex], ItemType.Weapon, _weaponsCount);
        }

        return addedAny;
    }

    private bool TryAddArmors(SOItemConfig config, ref int leftover)
    {
        bool addedAny = false;

        while (leftover > 0)
        {
            int slotIndex = GetFirstEmptySlot(_armors);
            if (slotIndex == -1)
            {
                GameEvents.RaisePopupEvent("Armor inventory is full!");
                return addedAny;
            }

            _armors[slotIndex].Init(config);
            leftover--;
            addedAny = true;
            _armorsCount++;

            OnItemUpdated?.Invoke(slotIndex, _armors[slotIndex], ItemType.Armor, _armorsCount);
        }

        return addedAny;
    }

    /// <summary>
    /// Resources can stack if same item type is found.
    /// If not found, they behave like non-stackables but with quantity.
    /// </summary>
    private bool TryAddResources(SOItemConfig config, ref int leftover)
    {
        bool addedAny = false;

        // Check if we can stack into an existing resource slot
        for (int i = 0; i < _resources.Count; i++)
        {
            InventoryItem item = _resources[i];

            if (!item.IsEmpty && item.ItemConfig == config)
            {
                int startLeftover = leftover;
                int before = item.Quantity;

                leftover = item.AddQuantity(leftover);
                int after = item.Quantity;

                // If we could not add everything then stack reached limit
                if (after - before < startLeftover)
                    GameEvents.RaisePopupEvent($"{config.ItemName} stack is full!");

                OnItemUpdated?.Invoke(i, item, ItemType.Resource, _resourceCount);
                return true;
            }
        }

        // Create a new stack if no existing stack found
        int slotIndexNew = GetFirstEmptySlot(_resources);
        if (slotIndexNew == -1)
        {
            GameEvents.RaisePopupEvent("Resource inventory is full!");
            return addedAny;
        }

        _resources[slotIndexNew].Init(config);
        leftover = _resources[slotIndexNew].AddQuantity(leftover);

        addedAny = true;
        _resourceCount++;

        OnItemUpdated?.Invoke(slotIndexNew, _resources[slotIndexNew], ItemType.Resource, _resourceCount);

        return addedAny;
    }

    public void RemoveItem(int slotIndex, ItemType type, int amount = 1)
    {
        switch (type)
        {
            case ItemType.Weapon: RemoveWeapons(slotIndex); break;
            case ItemType.Armor: RemoveArmors(slotIndex); break;
            case ItemType.Resource: RemoveResources(slotIndex, amount); break;
        }
    }

    private void RemoveWeapons(int slotIndex)
    {
        _weapons[slotIndex].Clear();
        _weaponsCount--;
        Compact(_weapons, ItemType.Weapon, ref _weaponsCount);
    }

    private void RemoveArmors(int slotIndex)
    {
        _armors[slotIndex].Clear();
        _armorsCount--;
        Compact(_armors, ItemType.Armor, ref _armorsCount);
    }

    /// <summary>
    /// If a resource stack hits 0, treat it like removing a full item.
    /// </summary>
    private void RemoveResources(int slotIndex, int amount)
    {
        if (_resources[slotIndex].RemoveQuantity(amount))
            _resourceCount--;

        Compact(_resources, ItemType.Resource, ref _resourceCount);
    }

    /// <summary>
    /// "Shifts left" all items after a removal.
    /// Preserves relative order and removes empty gaps.
    /// </summary>
    private void Compact(List<InventoryItem> items, ItemType type, ref int usedCount)
    {
        int write = 0;

        // Move filled slots to the left
        for (int read = 0; read < items.Count; read++)
        {
            if (!items[read].IsEmpty)
            {
                if (write != read)
                {
                    items[write].CopyFrom(items[read]);
                    items[read].Clear();
                }

                OnItemUpdated?.Invoke(write, items[write], type, usedCount);
                write++;
            }
        }

        // Clear the remaining trailing slots
        for (int i = write; i < items.Count; i++)
        {
            items[i].Clear();
            OnItemUpdated?.Invoke(i, items[i], type, usedCount);
        }
    }

    /// <summary>
    /// Returns index of first empty slot. Otherwise -1.
    /// </summary>
    private int GetFirstEmptySlot(List<InventoryItem> items)
    {
        for (int i = 0; i < items.Count; i++)
            if (items[i].IsEmpty)
                return i;

        return -1;
    }
}