오늘자로 개인 프로젝트 제출까지 마무리 지었습니다. 이제는 마지막으로 남은 팀 프로젝트를 준비해야 합니다.
스파르타 코딩클럽 10기, 유니티 심화 개인 프로젝트 인벤토리 과제작업물입니다.
기존 Step에 부가적인 기능들을 몇가지 추가하고
별도로 절차적 맵 생성과 FSM 으로 3D 방치형 게임 링크도 첨부했습니다.
https://drive.google.com/file/d/1Vtvr9xWM_tKw7vaETlN4cJh5IEMwNfQM/view?usp=sharing
https://github.com/coolblue1853/Unity_Master_Solo
using TMPro;
using UnityEngine;
using UnityEngine.UI;
public class UIMainMenu : UIBase
{
private AudioManager _audioManager;
private GameObject _statusObj;
private GameObject _InventoryObj;
private Button _statusBtn;
private Button _InventoryBtn;
private Button _mainBtn;
private TextMeshProUGUI _jobTxt;
private TextMeshProUGUI _nameTxt;
private TextMeshProUGUI _levelTxt;
private TextMeshProUGUI _expTxt;
private TextMeshProUGUI _descriptionTxt;
private TextMeshProUGUI _coinTxt;
private Image _expImg;
bool isInit = false;
enum Btn
{
StatusBtn,
InventoryBtn,
MainBtn,
}
enum Txts
{
JobTxt,
NameTxt,
LevelTxt,
ExpTxt,
DescriptionTxt,
CoinTxt,
}
enum Images
{
ExpBar,
}
protected override void Start()
{
base.Start();
_audioManager = GameManager.Instance.AudioManager;
// 자동화된 UI 연결
Bind<Button>(typeof(Btn));
Bind<TextMeshProUGUI>(typeof(Txts));
Bind<Image>(typeof(Images));
_statusBtn = Get<Button>((int)Btn.StatusBtn);
_InventoryBtn = Get<Button>((int)Btn.InventoryBtn);
_mainBtn = Get<Button>((int)Btn.MainBtn);
_jobTxt = Get<TextMeshProUGUI>((int)Txts.JobTxt);
_nameTxt = Get<TextMeshProUGUI>((int)Txts.NameTxt);
_levelTxt = Get<TextMeshProUGUI>((int)Txts.LevelTxt);
_expTxt = Get<TextMeshProUGUI>((int)Txts.ExpTxt);
_descriptionTxt = Get<TextMeshProUGUI>((int)Txts.DescriptionTxt);
_coinTxt = Get<TextMeshProUGUI>((int)Txts.CoinTxt);
_expImg = Get<Image>((int)Images.ExpBar);
_statusObj = _uIManager.UIStatus.gameObject;
_InventoryObj = _uIManager.UIInventory.gameObject;
// 버튼 함수 지정
_mainBtn.onClick.AddListener(OpenMainMenu);
_statusBtn.onClick.AddListener(OpenStatus);
_InventoryBtn.onClick.AddListener(OpenInventory);
OpenMainMenu();
isInit = true;
// 최초 데이터 갱신 요청
GameManager.Instance.SetData();
}
// 메인 UI
void OpenMainMenu()
{
if (isInit)
{
_audioManager.PlaySFX("CancelBtn");
}
_uIManager.ResetUI();
ResetBtn(Define.Button.Main);
}
// 능력치 UI
void OpenStatus()
{
_audioManager.PlaySFX("SelectBtn");
_uIManager.ResetUI();
ResetBtn(Define.Button.Status);
_statusObj.SetActive(true);
}
// 인벤토리 UI
void OpenInventory()
{
_audioManager.PlaySFX("SelectBtn");
_uIManager.ResetUI();
ResetBtn(Define.Button.Inventory);
_InventoryObj.SetActive(true);
}
// 메인 화면의 능력치 표기
public void SetDetail(Character character)
{
_jobTxt.text = character.Stats.Job.Name;
_nameTxt.text = character.Name;
_levelTxt.text = $"LV {character.Stats.Level}";
_expTxt.text = $"{character.Stats.NowExp} / {character.Stats.MaxExp}";
_expImg.fillAmount = (float)character.Stats.NowExp / character.Stats.MaxExp;
_coinTxt.text = Utils.FormatBigInteger(character.Stats.Coin);
_descriptionTxt.text = character.Stats.Job.Description;
}
// 버튼 리셋
private void ResetBtn(Define.Button type)
{
_mainBtn.gameObject.SetActive(false);
_statusBtn.gameObject.SetActive(false);
_InventoryBtn.gameObject.SetActive(false);
switch (type)
{
case Define.Button.Main:
_statusBtn.gameObject.SetActive(true);
_InventoryBtn.gameObject.SetActive(true);
break;
default:
_mainBtn.gameObject.SetActive(true);
break;
}
}
}
using System;
using UnityEngine;
using System.Numerics;
// 스탯 종류 구조체
[System.Serializable]
public struct CharacterStats
{
public JobInfo Job;
public int Level;
public int MaxExp;
public int NowExp;
public int Attack;
public int AttackSpeed;
public int Defence;
public int Health;
public int Critical;
public BigInteger Coin;
public CharacterStats(JobInfo job,int level, int maxExp, int nowExp, int attack, int attackSpeed, int defence, int health, int critical, BigInteger coin)
{
Job = job;
Level = level;
MaxExp = maxExp;
NowExp = nowExp;
Attack = attack;
AttackSpeed = attackSpeed;
Defence = defence;
Health = health;
Critical = critical;
Coin = coin;
}
}
public class Character : MonoBehaviour
{
private PlayerController _playerController;
[SerializeField] private string _name;
// 옵저버 패턴
public event Action<CharacterStats> OnStatsChanged;
public event Action<BigInteger> OnCoinChanged;
private void Awake()
{
_playerController = GetComponent<PlayerController>();
}
public string Name
{
get => _name;
set => _name = value;
}
[SerializeField]
private CharacterStats _stats;
public CharacterStats Stats
{
get => _stats;
set
{
CharacterStats oldStats = _stats;
_stats = value;
if (oldStats.Coin != _stats.Coin)
{
OnCoinChanged?.Invoke(_stats.Coin);
}
OnStatsChanged?.Invoke(_stats);
}
}
public Inventory Inventory { get; private set; }
public void Init(string name, CharacterStats stats, Inventory inventory)
{
_name = name;
_stats = stats;
Inventory = inventory;
Inventory.Character = this;
_playerController.InitPlayer(this);
}
}
private void Init()
{
for (int i = 0; i < Constant.InventoryCount; i++)
{
var go = Instantiate(_slot);
go.transform.SetParent(_slotGroup.transform);
go.transform.localScale = Vector3.one;
var slot = go.GetComponent<UISlot>();
slot.ResetSlot(i);
_slots.Add(slot);
}
SetInvenCount();
_inventory.AddObserver(this);
}
// 인벤토리 아이템수 / 슬롯수 갱신
private void SetInvenCount()
{
int itemCount = 0;
for (int i = 0; i < Constant.InventoryCount; i++)
{
if (_inventory.Items[i] != null)
itemCount++;
}
_slotTxt.text = $"Inventory {itemCount} / {Constant.InventoryCount}";
}
using UnityEngine;
using UnityEngine.UI;
public class UISlot : MonoBehaviour
{
public int Idx = -1;
private Inventory _inventory;
private Character _character;
public ItemData Item = null;
private bool _isEquiped = false;
[SerializeField] private Image _icon;
[SerializeField] private Image _base;
[SerializeField] private GameObject _equiped;
private void Start()
{
_inventory = GameManager.Instance.Character.Inventory;
_character = GameManager.Instance.Character;
}
// 아이콘 업데이트
public void UpdateIcon(ItemData item)
{
_icon.sprite = item.Icon;
_icon.color = Constant.Alpha255;
switch (item.Rarity)
{
case ItemRarity.Common:
_base.color = Constant.White;
break;
case ItemRarity.Uncommon:
_base.color = Constant.Green;
break;
case ItemRarity.Rare:
_base.color = Constant.Blue;
break;
case ItemRarity.Unique:
_base.color = Constant.Pink;
break;
case ItemRarity.Legendary:
_base.color = Constant.Orange;
break;
}
}
public void SetItem(ItemData item)
{
Item = item;
}
// 장착여부 업데이트
// UI 초기화 함수
public void ResetSlot(int idx = -1)
{
if(idx != -1)
Idx = idx;
Item = null;
_icon.sprite = null;
_icon.color = Constant.Alpha0;
}
public void OnClickItemSlot()
{
if (_inventory.IsSellMode)
SellItem();
else
ToggleEquip();
}
// 아이템 판매
public void SellItem()
{
if (Item == null)
return;
//코인 추가
var stats = _character.Stats;
stats.Coin += Constant.sellCost / 2;
_character.Stats = stats;
// 인벤토리 갱신
_base.color = Constant.White;
if (_isEquiped)
{
_isEquiped = false;
_equiped.SetActive(false);
_inventory.RemoveItemStat(Item);
}
_inventory.DeleteItem(Idx);
ResetSlot();
}
// UI 등장 합수
public void ToggleEquip()
{
if (Item == null)
return;
if (!_isEquiped)
{
_isEquiped = true;
_equiped.SetActive(true);
_inventory.ApplyItemStat(Item);
}
else
{
_isEquiped = false;
_equiped.SetActive(false);
_inventory.RemoveItemStat(Item);
}
}
}
using UnityEngine;
public enum ItemType
{
Resource,
Equipable,
Consumable
}
public enum ConsumableType
{
Health,
}
public enum BuffType
{
Attack,
AttackSpeed,
Defence,
Health,
Critical,
}
public enum ItemRarity
{
Common,
Uncommon,
Rare,
Unique,
Legendary
}
[System.Serializable]
public class ItemDataConsumable
{
public ConsumableType Type;
public float Value;
}
[System.Serializable]
public class ItemDataBuff
{
public BuffType Type;
public float Time;
public float Value;
}
[System.Serializable]
public class ItemDataEquip
{
public BuffType Type;
public float Value;
public void Apply(ref CharacterStats stats)
{
switch (Type)
{
case BuffType.Attack:
stats.Attack += (int)Value;
break;
case BuffType.AttackSpeed:
stats.AttackSpeed += (int)Value;
break;
case BuffType.Defence:
stats.Defence += (int)Value;
break;
case BuffType.Health:
stats.Health += (int)Value;
break;
case BuffType.Critical:
stats.Critical += (int)Value;
break;
}
}
public void Remove(ref CharacterStats stats)
{
switch (Type)
{
case BuffType.Attack:
stats.Attack -= (int)Value;
break;
case BuffType.AttackSpeed:
stats.AttackSpeed -= (int)Value;
break;
case BuffType.Defence:
stats.Defence -= (int)Value;
break;
case BuffType.Health:
stats.Health -= (int)Value;
break;
case BuffType.Critical:
stats.Critical -= (int)Value;
break;
}
}
}
[CreateAssetMenu(fileName = "Item", menuName = "New Item")]
public class ItemData : ScriptableObject
{
[Header("Info")]
public string DisplayName;
public string Descrition;
public ItemType Type;
public ItemRarity Rarity;
public Sprite Icon;
public GameObject DropPrefab;
[Header("Stacking")]
public int MaxStackAmount;
[Header("Consumable")]
public ItemDataConsumable[] consumables;
[Header("Buff")]
public ItemDataBuff[] buffs;
[Header("Equip")]
public ItemDataEquip[] equips;
}
public void ToggleEquip()
{
if (Item == null)
return;
if (!_isEquiped)
{
_isEquiped = true;
_equiped.SetActive(true);
_inventory.ApplyItemStat(Item);
}
else
{
_isEquiped = false;
_equiped.SetActive(false);
_inventory.RemoveItemStat(Item);
}
}
// 스탯치 반영
public void ApplyItemStat(ItemData item)
{
if (item == null || item.equips == null) return;
var stats = Character.Stats;
foreach (var equip in item.equips)
equip.Apply(ref stats);
Character.Stats = stats; // 옵저버 트리거
}
public void RemoveItemStat(ItemData item)
{
if (item == null || item.equips == null) return;
var stats = Character.Stats;
foreach (var equip in item.equips)
equip.Remove(ref stats);
Character.Stats = stats; // 옵저버 트리거
}
public void SetData()
{
CharacterStats stats = _defaultStatData.ToCharacterStats();
Inventory inventory = Instantiate(_inventoryPrefab).GetComponent<Inventory>();
inventory.UiInventory = _uiManager.UIInventory;
inventory.Init();
_character = _characterPrefab.GetComponent<Character>();
_character.Init("Chad", stats, inventory);
// 옵저버 구독
_character.OnStatsChanged += (newStats) =>
{
_uiManager.UIStatus.SetStatus(_character);
};
_character.OnCoinChanged += (coin) =>
{
_uiManager.UIMainMenu.SetDetail(_character);
};
_uiManager.UIMainMenu.SetDetail(_character);
}
[SerializeField]
private CharacterStats _stats;
public CharacterStats Stats
{
get => _stats;
set
{
CharacterStats oldStats = _stats;
_stats = value;
if (oldStats.Coin != _stats.Coin)
{
OnCoinChanged?.Invoke(_stats.Coin);
}
OnStatsChanged?.Invoke(_stats);
}
}
using UnityEngine;
using System.Numerics;
[CreateAssetMenu(fileName = "CharacterStatData", menuName = "Game Data/CharacterStatData")]
public class CharacterStatData : ScriptableObject
{
public JobInfo Job;
public int Level;
public int MaxExp;
public int NowExp;
public int Attack;
public int AttackSpeed;
public int Defence;
public int Health;
public int Critical;
[SerializeField]
private string _coinString = "0";
public BigInteger Coin
{
get => BigInteger.TryParse(_coinString, out var result) ? result : BigInteger.Zero;
set => _coinString = value.ToString();
}
// 스크립터블 오브젝트에서 CharacterStats로 추출
public CharacterStats ToCharacterStats()
{
return new CharacterStats(Job, Level, MaxExp, NowExp, Attack, AttackSpeed, Defence, Health, Critical, Coin);
}
}
// 아이템 뽑기 함수
public void SpawnRandItem()
{
if (_character == null)
_character = GameManager.Instance.Character;
if (_inventory == null)
_inventory = _character.Inventory;
if (_character.Stats.Coin < Constant.ItemCost)
return;
// 코인 차감
var stats = _character.Stats;
stats.Coin -= Constant.ItemCost;
_character.Stats = stats;
// 확률에 따라 등급 선택
ItemRarity selectedRarity = RollRarity();
// 해당 등급의 아이템 필터링
List<ItemData> candidates = _dataList.FindAll(item => item.Rarity == selectedRarity);
if (candidates.Count > 0)
{
int rand = Random.Range(0, candidates.Count);
_inventory.AddItem(candidates[rand]);
}
}
public void UpdateIcon(ItemData item)
{
_icon.sprite = item.Icon;
_icon.color = Constant.Alpha255;
switch (item.Rarity)
{
case ItemRarity.Common:
_base.color = Constant.White;
break;
case ItemRarity.Uncommon:
_base.color = Constant.Green;
break;
case ItemRarity.Rare:
_base.color = Constant.Blue;
break;
case ItemRarity.Unique:
_base.color = Constant.Pink;
break;
case ItemRarity.Legendary:
_base.color = Constant.Orange;
break;
}
}
private readonly Dictionary<ItemRarity, float> rarityChances = new()
{
{ ItemRarity.Common, 0.5f },
{ ItemRarity.Uncommon, 0.25f },
{ ItemRarity.Rare, 0.15f },
{ ItemRarity.Unique, 0.07f },
{ ItemRarity.Legendary, 0.03f }
};
// 등급 지정
private ItemRarity RollRarity()
{
float roll = Random.value; // 0.0 ~ 1.0
float cumulative = 0f;
foreach (var pair in rarityChances)
{
cumulative += pair.Value;
if (roll <= cumulative)
return pair.Key;
}
return ItemRarity.Common;
}
using UnityEngine;
public class Weapon : MonoBehaviour
{
private AudioManager _audioManager;
private Character _character;
[SerializeField] private Enemy _enemy; // 원래라면은 충돌이나 감지를 시켜줘야함
public void Init(Character character)
{
_character = character;
_audioManager = GameManager.Instance.AudioManager;
}
public void Attack()
{
int critNum = Random.Range(1, 101);
// 크리티컬 성공
if(_character.Stats.Critical >= critNum)
{
var stats = _character.Stats;
var attackPower = (int)(stats.Attack * Constant.CritCoinRate);
stats.Coin += attackPower;
_character.Stats = stats;
_enemy.Hit(attackPower,true);
}
else // 크리티컬 실패
{
var stats = _character.Stats;
stats.Coin += stats.Attack;
_character.Stats = stats;
_enemy.Hit(stats.Attack);
}
_audioManager.PlaySFX("Attack");
}
}
using UnityEngine;
using UnityEngine.Audio;
using System.Collections.Generic;
public class AudioManager : MonoBehaviour
{
[Header("Mixer Settings")]
public AudioMixer AudioMixer; // 오디오 믹서 (BGM, SFX 볼륨 제어)
[Header("Background Music")]
public AudioSource BgmSource; // BGM 재생용 AudioSource
public AudioClip[] BgmClips; // 재생 가능한 BGM 클립 배열
[Header("Sound Effects (Auto Register)")]
public AudioSource SfxSource; // SFX 재생용 AudioSource
private Dictionary<string, AudioClip> _sfxDict = new Dictionary<string, AudioClip>(); // SFX 이름-클립 매핑 딕셔너리
private AudioSource _loopSource; // 루프용 AudioSource
private void Awake()
{
LoadAllSFX();
}
// Resources/Audio/SFX 폴더 내 모든 오디오 클립을 자동 등록
void LoadAllSFX()
{
AudioClip[] clips = Resources.LoadAll<AudioClip>("Audio/SFX");
foreach (AudioClip clip in clips)
{
if (!_sfxDict.ContainsKey(clip.name))
{
_sfxDict.Add(clip.name, clip);
Debug.Log($"SFX 로드 완료: {clip.name}");
}
}
}
// 인덱스로 BGM 재생
public void PlayBGM(int index)
{
if (index >= 0 && index < BgmClips.Length)
{
BgmSource.clip = BgmClips[index];
BgmSource.loop = true;
BgmSource.Play();
}
}
// 이름으로 SFX 재생
public void PlaySFX(string name)
{
if (_sfxDict.TryGetValue(name, out AudioClip clip))
{
SfxSource.PlayOneShot(clip);
}
else
{
Debug.LogWarning($"SFX '{name}' 을(를) 찾을 수 없습니다.");
}
}
// 피치와 볼륨 설정하여 SFX 재생
public void PlaySFX(string name, float volume, float pitch)
{
if (_sfxDict.TryGetValue(name, out AudioClip clip))
{
SfxSource.pitch = pitch;
SfxSource.PlayOneShot(clip, volume);
SfxSource.pitch = 1f; // 재생 후 피치 초기화
}
}
// 위치 기반으로 SFX 재생 (3D 공간)
public void PlaySFXAtPosition(string name, Vector3 position)
{
if (_sfxDict.TryGetValue(name, out AudioClip clip))
{
AudioSource.PlayClipAtPoint(clip, position);
}
}
// 루프 사운드 시작 (지속 재생 효과음)
public void PlaySFXLoop(string name)
{
if (_loopSource == null)
{
_loopSource = gameObject.AddComponent<AudioSource>();
_loopSource.loop = true;
_loopSource.playOnAwake = false;
}
if (_sfxDict.TryGetValue(name, out AudioClip clip))
{
_loopSource.clip = clip;
_loopSource.Play();
}
}
// 루프 사운드 정지
public void StopSFXLoop()
{
if (_loopSource != null && _loopSource.isPlaying)
{
_loopSource.Stop();
}
}
// BGM 볼륨 설정 (0.0 ~ 1.0 범위, dB 변환)
public void SetBGMVolume(float value)
{
AudioMixer.SetFloat("BGM", Mathf.Log10(Mathf.Clamp(value, 0.0001f, 1f)) * 20);
}
// SFX 볼륨 설정 (0.0 ~ 1.0 범위, dB 변환)
public void SetSFXVolume(float value)
{
AudioMixer.SetFloat("SFX", Mathf.Log10(Mathf.Clamp(value, 0.0001f, 1f)) * 20);
}
}
using System.Collections;
using System.Collections.Generic;
using System.Numerics;
using UnityEngine;
public class Utils
{
// Big 인티저를 이용해서 가져오는 포맷값
public static string FormatBigInteger(BigInteger number)
{
if (number < 1000) return number.ToString();
string[] suffixes = { "", "K", "M", "B", "T", "Q" };
int i = 0;
while (number >= 1000 && i < suffixes.Length - 1)
{
number /= 1000;
i++;
}
return number.ToString() + suffixes[i];
}
}
// 아이템 판매
public void SellItem()
{
if (Item == null)
return;
//코인 추가
var stats = _character.Stats;
stats.Coin += Constant.sellCost / 2;
_character.Stats = stats;
// 인벤토리 갱신
_base.color = Constant.White;
if (_isEquiped)
{
_isEquiped = false;
_equiped.SetActive(false);
_inventory.RemoveItemStat(Item);
}
_inventory.DeleteItem(Idx);
ResetSlot();
}
├── Audio
│ └── AudioManager.cs
│
├── Enemy
│ └── Enemy.cs
│
├── Inventory
│ └── Inventory.cs
│
├── Item
│ ├── ItemData.cs
│ └── ItemSpawner.cs
│
├── Manager
│ └── GameManager.cs
│
├── Player
│ ├── Character.cs
│ ├── CharacterStatData.cs
│ ├── PlayerController.cs
│ └── Weapon.cs
│
├── UI
│ ├── UIBase.cs
│ ├── UIInventory.cs
│ ├── UIMainMenu.cs
│ ├── UIManager.cs
│ ├── UISlot.cs
│ └── UIStatus.cs
│
├── Utils
│ ├── Constant.cs
│ ├── DamageText.cs
│ ├── Define.cs
│ └── Utils.cs