Inventory System은 게임의 핵심적인 시스템 중 하나로, 플레이어가 아이템을 저장, 정렬, 관리할 수 있도록 도와줍니다. 이를 통해 플레이어는 게임 플레이 중 필요한 아이템을 효율적으로 사용할 수 있으며, 게임 내 자원 관리 및 전략적 선택에 중요한 역할을 합니다. 이번 글에서는 Unity를 활용하여 Inventory System의 기초적인 설계와 구현 방법에 대해 알아보겠습니다.
총 3편으로 제작되며 1편에서는 Stat 과 Item 구현에 대한 글입니다.
Inventory System (2)
Inventory System (3)

- 스텟 구현
- 아이템 구현
- 인벤토리 구현
- 캐릭터 장비창 구현
- 구현에 대해 필요한 정보를 먼저 설명하겠습니다.
- Stat은 Character과 Item이 공통적으로 사용합니다.
- Character가 Item을 장착하였을 때 해당 Item의 Stat이 더해지는 방식입니다.
- Stat은 ScriptableObject로 관리합니다.
- Stat은 Enum(능력치 이름) 값과 Float(능력치 수치) 값을 가지고 있습니다.
/// <summary>
/// 장비 아이템 장착 부위
/// </summary>
public enum EquipType
{
Helmet, // 헬멧
Armor, // 갑옷
Weapon_1, // 무기 1
Weapon_2, // 무기 2
Weapon_3, // 무기 3
Spacial, // 특수 아이템
Ring, // 반지
Necklace, // 목걸이
}
/// <summary>
/// 아이템 타입
/// </summary>
public enum ItemType
{
Equipment, // 장비
Spacial, // 아직 고민 중
Food, // 음식
Etc, // 기타
}
/// <summary>
/// 스텟 타입
/// </summary>
public enum StatType
{
Attack, // 공격력
Defence, // 방어력
WorkSpeed, // 작업 속도
Weight, // 소지 중량
HP, // 체력
Stamina // 기력
}
- Enums 클래스에 필요한 Enum 값들을 정리합니다.
[CreateAssetMenu(fileName = "Stat", menuName = "Public/Stat")]
public class Stat : ScriptableObject
{
public StatType StatType;
public float DefaultValue;
}
- ScritableObject를 상속 받고 StatType(Enum)과 float 형식의 변수를 사용합니다.
[System.Serializable]
public class StatModifier
{
public Stat TargetStat; // 영향을 받을 Stat
public float ModifierValue; // 증가 또는 감소 값
}
- Stat을 참조하여 해당 스텟에 대한 변동 값을 정의합니다. ItemData에 사용 될 클래스입니다.
[CreateAssetMenu(fileName = "ItemData", menuName = "Inventory/Item")]
public class ItemData : ScriptableObject
{
public ItemType ItemType; // 아이템 타입
public string ItemName; // 아이템 이름
[TextArea(3, 5)] public string Description; // 아이템 설명
public Sprite Icon; // 아이템 아이콘
public bool IsStackable; // 중첩 가능 여부
public int MaxStackSize; // 최대 중첩 개수
public List<StatModifier> StatModifiers = new List<StatModifier>(); // 스탯 수정 리스트
}
- ScriptableObject로 구현하였습니다.
- 각 필요한 변수와 중첩 가능 여부와 최대 중첩 개수를 가지고 있으며 StatModifier를 사용합니다.
public class CharacterStats
{
public Dictionary<Stat, float> _stats = new Dictionary<Stat, float>();
private List<ItemData> _equippedItems = new List<ItemData>();
public int StatusPoints { get; private set; } = 10; // 초기 스테이터스 포인트
public event Action<Stat, float> OnStatChanged; // 스탯 변경 이벤트
public event Action<int> OnStatusPointsChanged; // 스테이터스 포인트 변경 이벤트
public CharacterStats(List<Stat> baseStats)
{
foreach (var stat in baseStats)
{
_stats[stat] = stat.DefaultValue; // 기본값 설정
}
}
// 현재 스탯 가져오기
public float GetStatValue(Stat stat)
{
if (_stats.ContainsKey(stat))
return _stats[stat];
return 0; // 해당 스탯이 없으면 0 반환
}
// 아이템 장착
public void EquipItem(ItemData item)
{
_equippedItems.Add(item);
ApplyModifiers(item.StatModifiers);
}
// 아이템 해제
public void UnequipItem(ItemData item)
{
_equippedItems.Remove(item);
RemoveModifiers(item.StatModifiers);
}
// 스탯 수정 적용
private void ApplyModifiers(List<StatModifier> modifiers)
{
foreach (var modifier in modifiers)
{
if (_stats.ContainsKey(modifier.TargetStat))
{
_stats[modifier.TargetStat] += modifier.ModifierValue;
}
else
{
_stats[modifier.TargetStat] = modifier.ModifierValue;
}
}
}
// 스탯 수정 제거
private void RemoveModifiers(List<StatModifier> modifiers)
{
foreach (var modifier in modifiers)
{
if (_stats.ContainsKey(modifier.TargetStat))
{
_stats[modifier.TargetStat] -= modifier.ModifierValue;
// 기본값 이하로 내려가면 기본 값 표시
if (_stats[modifier.TargetStat] < modifier.TargetStat.DefaultValue)
{
_stats[modifier.TargetStat] = modifier.TargetStat.DefaultValue;
}
}
}
}
// 능력치 증가
public void IncreaseStatWithPoints(StatModifier stat)
{
if (StatusPoints > 0)
{
_stats[stat.TargetStat] += stat.ModifierValue;
StatusPoints -= 1;
OnStatChanged?.Invoke(stat.TargetStat, _stats[stat.TargetStat]);
OnStatusPointsChanged?.Invoke(StatusPoints);
}
}
// 능력치 감소
public void DecreaseStatWithPoints(StatModifier stat)
{
float minValue = GetMinimumStatValue(stat.TargetStat);
if (_stats[stat.TargetStat] > minValue)
{
_stats[stat.TargetStat] -= stat.ModifierValue;
StatusPoints += 1;
OnStatChanged?.Invoke(stat.TargetStat, _stats[stat.TargetStat]);
OnStatusPointsChanged?.Invoke(StatusPoints);
}
}
// 최소값 계산
private float GetMinimumStatValue(Stat stat)
{
float baseValue = stat.DefaultValue;
float itemBonus = GetItemBonus(stat);
return baseValue + itemBonus;
}
// 아이템으로 인한 스탯 보너스 계산
private float GetItemBonus(Stat stat)
{
float totalBonus = 0;
foreach (var item in _equippedItems)
{
foreach (var modifier in item.StatModifiers)
{
if (modifier.TargetStat == stat)
{
totalBonus += modifier.ModifierValue;
}
}
}
return totalBonus;
}
}
- Character의 Stat을 관리하는 클래스입니다.
- Item을 장착하였을 때 StatModifier에 있는 Stat이 존재하는지 검사한 뒤 값을 더해주거나 빼줍니다.
- User가 Stat을 찍는 경우를 위한 함수와 아이템 장착 시 최소값을 계산하기 위한 함수를 추가해줍니다.
public class CharacterTest : MonoBehaviour
{
public List<Stat> BaseStats;
public ItemData Sword;
public ItemData Shield;
[HideInInspector] public CharacterStats _characterStats;
private void Awake()
{
_characterStats = new CharacterStats(BaseStats);
PrintStats("초기 스텟 상태");
_characterStats.EquipItem(Sword);
PrintStats("무기 장착");
_characterStats.EquipItem(Shield);
PrintStats("갑옷 장착");
_characterStats.UnequipItem(Shield);
PrintStats("갑옷 해제");
}
private void PrintStats(string context)
{
Debug.Log($"{context} Stats:");
foreach (var stat in BaseStats)
{
Debug.Log($"{stat.StatType}: {_characterStats.GetStatValue(stat)}");
}
}
}
- 스텟의 변동을 테스트 하기 위한 클래스입니다.
- 아이템 장착,해제시 로그로 현재 스텟을 보여줍니다.
public class CharacterStatsTest : MonoBehaviour
{
public CharacterTest characterTest;
public Text text_attack;
public Text text_defance;
public Text text_workspeed;
public Text text_hp;
public Text text_stamina;
public Text text_weight;
public Text text_StatusPoint;
public Button button_atk_Increase;
public Button button_def_Increase;
public Button button_workspeed_Increase;
public Button button_hp_Increase;
public Button button_stamina_Increase;
public Button button_weight_Increase;
public Button button_atk_decrease;
public Button button_def_decrease;
public Button button_workspeed_decrease;
public Button button_hp_decrease;
public Button button_stamina_decrease;
public Button button_weight_decrease;
public StatModifier statModifier_atk;
public StatModifier statModifier_def;
public StatModifier statModifier_workspeed;
public StatModifier statModifier_hp;
public StatModifier statModifier_stamina;
public StatModifier statModifier_weight;
private void Start()
{
characterTest._characterStats.OnStatChanged += UpdateText;
characterTest._characterStats.OnStatusPointsChanged += UpdateStatusPoint;
button_atk_Increase.onClick.AddListener(() => OnClickIncreaseStat(statModifier_atk));
button_def_Increase.onClick.AddListener(() => OnClickIncreaseStat(statModifier_def));
button_workspeed_Increase.onClick.AddListener(() => OnClickIncreaseStat(statModifier_workspeed));
button_hp_Increase.onClick.AddListener(() => OnClickIncreaseStat(statModifier_hp));
button_stamina_Increase.onClick.AddListener(() => OnClickIncreaseStat(statModifier_stamina));
button_weight_Increase.onClick.AddListener(() => OnClickIncreaseStat(statModifier_weight));
button_atk_decrease.onClick.AddListener(() => OnClickDecreaseStat(statModifier_atk));
button_def_decrease.onClick.AddListener(() => OnClickDecreaseStat(statModifier_def));
button_workspeed_decrease.onClick.AddListener(() => OnClickDecreaseStat(statModifier_workspeed));
button_hp_decrease.onClick.AddListener(() => OnClickDecreaseStat(statModifier_hp));
button_stamina_decrease.onClick.AddListener(() => OnClickDecreaseStat(statModifier_stamina));
button_weight_decrease.onClick.AddListener(() => OnClickDecreaseStat(statModifier_weight));
InitUI();
}
private void InitUI()
{
foreach(var pair in characterTest._characterStats._stats)
{
UpdateText(pair.Key, pair.Value);
}
UpdateStatusPoint(characterTest._characterStats.StatusPoints);
}
private void OnClickIncreaseStat(StatModifier stat)
{
characterTest._characterStats.IncreaseStatWithPoints(stat);
}
private void OnClickDecreaseStat(StatModifier stat)
{
characterTest._characterStats.DecreaseStatWithPoints(stat);
}
private void UpdateText(Stat stat, float point)
{
switch(stat.StatType)
{
case StatType.Attack: text_attack.text = $"데미지 : {point.ToString()}"; break;
case StatType.Defence: text_defance.text = $"방어력 {point.ToString()}"; break;
case StatType.WorkSpeed: text_workspeed.text = $"작업 속도 : {point.ToString()}"; break;
case StatType.HP: text_hp.text = $"체력 : {point.ToString()}"; break;
case StatType.Stamina: text_stamina.text = $"기력 : {point.ToString()}"; break;
case StatType.Weight: text_weight.text = $"최대 중량 {point.ToString()}"; break;
}
}
private void UpdateStatusPoint(int value)
{
text_StatusPoint.text = $"StatusPoint : {value.ToString()}";
}
}
- CharacterStat의 Event에 UIUpdate 함수들을 연결해줍니다.
- StatModifier를 변수로 갖고 있어 수치와 스텟을 미리 정의해놓았습니다.
- 테스트를 위한 코드라 지저분합니다.. 양해 부탁드립니다

- Create -> Public -> Stat으로 ScriptableObject를 생성해줍니다.

- Character의 기본 Stat을 만든 뒤 초기치를 생성해주고 Type을 정해줍니다.

- Craete -> Inventory -> Item 으로 ScriptableObject를 생성해줍니다.
- 아이템의 정보와 장착하였을 때 변동이 있어야 하는 스텟의 값들을 넣어줍니다.

- GameObject를 하나 생성한 뒤 Character Test 컴포넌트를 넣어줍니다.
- 캐릭터의 기본 스텟들을 넣어주고 테스트를 위한 ItemData를 넣어줍시다.





- 아이템을 장착,해제 시 스텟이 변동되면서 정상적으로 동작합니다.
- 스텟을 업하고 다운하는 과정에서 StatusPoint가 제대로 반환되며 값도 제대로 반환됩니다.
- UI 갱신도 이루어지며 아이템 장착한 값 이하로는 내려가지 않습니다.
예전에 Stat과 Item을 구현한 것 보다는 구조가 더 짜임새가 있는 것 같습니다.
UI 테스트 부분의 코드가 만족스럽진 않지만 Inventory 작업을 하면서 최종본을 낼 때 같이 수정해야 겠습니다.그거 말고는 개인적으로 만족하는 코드입니다.