← Back to Work
Gameplay System · Released
Dynamic Inventory System
A Gameplay System
Unity 6WindowsRPG2025-04 — 2025-05
Credits
Team
Gunaraj Poojary · Gameplay Programmer

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
Gallery
Screenshots & Media
Videos
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;
}
}Resources