본문 바로가기

포트폴리오

게임 개발 포트폴리오 <ArcaneOdyssey>

3D 탑뷰 뱀서라이크 모바일 게임

랜덤하게 등장하는 스킬과 패시브를 기반으로 끊임없이 몰려오는 적을 처치하며 성장하는 탑뷰 뱀서라이크 모바일 게임

 

기간 2024.07 ~ 2024.12
팀원 1명
역할 기획, 인게임, UI.UX, 데이터 관리, 사운드
시연 영상 https://www.youtube.com/watch?v=KUfLb7VX3j8
GitHub https://github.com/0chotona/ArcaneOdyssey

 

주요 기능

- 다양한 공격 로직

다양한 액티브 / 패시브 공격 로직 구현

 

— 추상 클래스 기반 확장형 구조

 

📄 Attack.cs

public abstract class Attack : MonoBehaviour
{
    ...

    public abstract void StartAttack();
    public abstract void AttackInteract();
    public abstract IEnumerator CRT_Attack();
    public abstract void UpdateStat(CStat stat);
    public abstract void SetSkill(CSkill skill);
    public abstract void UpdateBuffStat(CBuffStat buffStat);
    public abstract void SetPlayerTrs(Transform playerTrs);
}

 

📄 Attack_Boomerang.cs

public class Attack_Boomerang : Attack
{
    private void OnEnable()
    {
        _name = eSKILL.Boomerang;
    }

    public override void UpdateStat(CStat stat)
    {
        ...
    }
    public override IEnumerator CRT_Attack()
    {
        ...
    }
    public override void AttackInteract()
    {
        ...
    }
    public override void SetSkill(CSkill skill)
    {
        ...
    }
    public override void StartAttack()
    {
        ...
    }
    public override void UpdateBuffStat(CBuffStat buffStat)
    {
        ...
    }
    public override void SetPlayerTrs(Transform playerTrs)
    { 
        ...
    }
}

이 구조는 공통적인 공격 동작의 틀을 상위 클래스인 Attack에서 정의하고, 구체적인 실행 로직은 각각의 하위 클래스에서 구현하도록 설계되어 있습니다.

예를 들어, Attack_Boomerang 클래스는 부메랑 스킬의 고유한 공격 방식과 스탯 처리, 상호작용 로직 등을 구체적으로 구현합니다.

이처럼 스킬별 특성을 하위 클래스에서 자유롭게 정의할 수 있어, 새로운 스킬이 추가되더라도 상위 로직을 건드리지 않고도 유지보수와 확장이 용이합니다.

또한, 이 추상 클래스 기반 구조는 다른 개발자나 팀원이 공통 인터페이스를 기반으로 작업할 수 있도록 해, 협업과 코드 일관성을 높여줍니다.

 

— 전력 패턴, 의존성 주입 이용 액티브 스킬 할당

 

📄 IActiveAttackable.cs

public interface IActiveAttackable
{
    void ActiveInteract();
    void SetBuffStat(CBuffStat buffStat);
    void SetPlayerTrs(Transform playerTrs);
}

 

📄 ActiveController.cs

public class ActiveController
{
    IActiveAttackable _skill1Method;
    IActiveAttackable _skill2Method;

    string _iconEName;
    string _iconRName;

    public void SetAttack1Method(IActiveAttackable attackMethod)
    {
        _skill1Method = attackMethod;
    }

    public void SetAttack2Method(IActiveAttackable attackMethod)
    {
        _skill2Method = attackMethod;
    }

    public IActiveAttackable GetAttackMethod()
    {
        return _skill1Method;
    }

    public void SetBuffStat(CBuffStat buffStat)
    {
        _skill1Method?.SetBuffStat(buffStat);
        _skill2Method?.SetBuffStat(buffStat);
    }

    public void PerformAttack1()
    {
        if (_skill1Method != null)
            _skill1Method.ActiveInteract();
        else
            Debug.Log("어택 메소드 없음");
    }

    public void PerformAttack2()
    {
        if (_skill2Method != null)
            _skill2Method.ActiveInteract();
        else
            Debug.Log("어택 메소드 없음");
    }

    public void SetIconName(string skillE, string skillR)
    {
        _iconEName = skillE;
        _iconRName = skillR;
    }
}

 

📄 ActiveAttack.cs

public class ActiveAttack : MonoBehaviour
{
    ActiveController _controller = new ActiveController();

    [Header("스킬 1 버튼"), SerializeField] Button _skill1Button;
    [Header("스킬 2 버튼"), SerializeField] Button _skill2Button;

    CBuffStat _buffStat = new CBuffStat();

    private void Awake()
    {
        _skill1Button.onClick.AddListener(() => Click_Skill1());
        _skill2Button.onClick.AddListener(() => Click_Skill2());
    }

    public void SetSkillMethod(IActiveAttackable method1, IActiveAttackable method2)
    {
        if (method1 != null)
            _controller.SetAttack1Method(method1);

        if (method2 != null)
            _controller.SetAttack2Method(method2);
    }

    public void SetBuffStat(CBuffStat buffStat)
    {
        _buffStat = buffStat;
        _controller.SetBuffStat(buffStat);
    }

    void Click_Skill1()
    {
        _controller.PerformAttack1();
    }

    void Click_Skill2()
    {
        _controller.PerformAttack2();
    }
}

전략 패턴의존성 주입을 기반으로 구성된 구조입니다.

ActiveAttack버튼 이벤트와 UI만 담당하고, 실제 스킬 동작은 IActiveAttackable 인터페이스를 통해 주입받아 실행합니다.

이 방식은 런타임 중에도 유연하게 스킬 교체가 가능하며, 각 스킬은 자신의 로직에만 집중할 수 있어 응집도는 높이고 결합도는 낮춘 구조를 실현합니다.

또한, 스킬 UI 아이콘 변경, 버프 효과 전달 등도 중간 레이어인 ActiveController에서 일괄 처리하여 UI와 로직을 명확히 분리했습니다.

추후 AI나 다른 입력 시스템과도 쉽게 연결 가능한 구조이므로, 스킬 실행의 확장성과 재사용성을 동시에 확보할 수 있습니다.

 

— 오브젝트 풀링

 

📄 Attack_FootprintTrap.cs

public override void StartAttack()
{
    _damageBoxPool = new ObjectPool(CreatePooledDamageBox, OnTakeFromPool, OnReturnedToPool, OnDestroyPoolObject, true, _defaultCapacity, _maxPoolSize);
    
    for (int i = 0; i < _defaultCapacity; i++)
    {
        DamageBox_FootprintTrap damageBox = CreatePooledDamageBox().GetComponent();
        damageBox._pool.Release(damageBox.gameObject);
    }

    StartCoroutine(CRT_Attack());
    StartCoroutine(CRT_SpawnDamage());
}

IEnumerator CRT_SpawnDamage()
{
    while (true)
    {
        yield return new WaitForSeconds(_spawnGap);
        GameObject bullet = _damageBoxPool.Get();
        bullet.transform.position = _shootTrs.position;
    }
}

GameObject CreatePooledDamageBox()
{
    GameObject poolObj = Instantiate(_damageBoxObj);
    DamageBox_FootprintTrap damageBox = poolObj.GetComponent();
    damageBox._pool = _damageBoxPool;
    return poolObj;
}

private void OnTakeFromPool(GameObject poolObj) 
{
    poolObj.SetActive(true);
    DamageBox_FootprintTrap damageBox = poolObj.GetComponent();
    damageBox.SetDamageBox(_durTime + _durTime * _buffStat._Dur);
    damageBox.UpdateScale(_baseScale * _attRange);        
    damageBox.SetAttack(this);
}

private void OnReturnedToPool(GameObject poolGo) 
{
    poolGo.SetActive(false);
}

private void OnDestroyPoolObject(GameObject poolGo) 
{
    Destroy(poolGo);
}

오브젝트 풀링(Object Pooling)을 활용하여, 지속적으로 생성되는 독성 발자국 효과를 효율적으로 구현했습니다.

발자국 독은 플레이어가 이동할 때 주기적으로 생성되며, 일정 시간이 지나면 자동으로 비활성화되어 풀로 반환됩니다. 이후 필요 시 재사용되어 불필요한 Instantiate/Destroy 호출을 방지합니다.

Unity의 ObjectPool<T>을 사용해 풀을 생성하고, OnTakeFromPool, OnReturnedToPool 등의 콜백으로 생성/반환 시의 초기화, 상태 설정을 분리하여 처리했습니다.

특히 CRT_SpawnDamage() 코루틴을 통해 일정 간격마다 자동 생성되며, damageBox.SetAttack(this)를 통해 공격 정보도 할당됩니다.

이러한 구조는 성능 최적화에 매우 효과적이며, 다양한 지속성 공격 효과를 구현할 때 재사용 가능합니다.

 

- 터치 상호작용

— 조이스틱 이동

 

📄 JoyStick.cs

public class JoyStick : InputController, IBeginDragHandler, IDragHandler, IEndDragHandler
{
    [SerializeField] RectTransform _lever;
    [SerializeField] Transform _arrowTrs; 
    [SerializeField] float _leverRange = 50f;
    RectTransform _rectTransform;            

    void Awake() { ... }

    void ControlLever(PointerEventData eventData)
    {
        RectTransformUtility.ScreenPointToLocalPointInRectangle(
            _rectTransform,
            eventData.position,
            eventData.pressEventCamera,
            out Vector2 localPoint );
        Vector2 direction = localPoint;
        Vector2 clampedDir = Vector2.ClampMagnitude(direction, _leverRange);

        _lever.anchoredPosition = clampedDir;
        _inputDir = clampedDir.sqrMagnitude > 0 ? clampedDir.normalized : Vector2.zero;
    }

    public void OnBeginDrag(PointerEventData eventData)
    {
        ControlLever(eventData);
    }

    public void OnDrag(PointerEventData eventData)
    {
        ControlLever(eventData);

        float angle = Mathf.Atan2(_inputDir.y, _inputDir.x) * Mathf.Rad2Deg;
        Quaternion rotation = Quaternion.Euler(0, 0, angle);
        _arrowTrs.rotation = rotation;
    }

    public void OnEndDrag(PointerEventData eventData)
    {
        _lever.anchoredPosition = Vector2.zero;
        _inputDir = Vector2.zero;
    }
}

조이스틱을 통해 플레이어 이동 방향을 입력받습니다. 드래그 방향에 따라 _inputDir이 계산되고, 이동 방향에 따라 화살표 UI도 회전하여 직관적인 피드백을 제공합니다.

 

— 액티브 스킬 조준

 

📄 Momoi_GetDrag.cs

public class Momoi_GetDrag : MonoBehaviour
{
    [Header("범위 오브젝트"), SerializeField] GameObject _cursorObj;
    [Header("액티브 R"), SerializeField] Momoi_Active_R _activeR;

    private void Awake()
    {
        _cursorObj.transform.SetParent(null);
        _cursorObj.SetActive(false);
    }

    private void OnMouseDown()
    {
        _cursorObj.SetActive(true);
    }

    void OnMouseDrag()
    {
        Vector3 worldPosition = Vector3.zero;
        RaycastHit hit;
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        if (Physics.Raycast(ray, out hit, Mathf.Infinity, 1 << 7))
        {
            worldPosition = hit.point;
        }

        worldPosition.y = 1f;
        _cursorObj.transform.position = worldPosition;
    }

    private void OnMouseUp()
    {
        if (_cursorObj.activeSelf)
        {
            Time.timeScale = 1;
            Debug.Log(_cursorObj.transform.position);

            _activeR.ShootRocket(_cursorObj.transform.position);
            _cursorObj.SetActive(false);
            gameObject.SetActive(false);
        }
    }
}

터치 기반 드래그를 활용해 액티브 스킬의 조준 위치를 지정하는 기능입니다.

터치 시작 시 커서 오브젝트가 활성화되고, 드래그 중 마우스 위치를 Raycast로 월드 좌표로 변환하여 조준 위치에 따라 커서가 이동합니다.

터치 종료 시 해당 위치에 ShootRocket()이 호출되어 지정된 위치로 스킬이 발사됩니다.

UI 기반 조이스틱 이동과 월드 기반 조준을 나눠 구현함으로써, 직관적인 조작정확한 타겟팅이 가능해졌습니다.

- 몬스터 소환

— 팩토리 패턴 사용

 

📄 MobFactory.cs

public abstract class MobFactory : MonoBehaviour
{
    public Transform _playerTrs;
    public abstract void Spawn(eNORMALMOB_TYPE type, Vector3 pos);
    public abstract void SetData(Dictionary&lt;eNORMALMOB_TYPE, CEnemy&gt; enemyDatas);
    public abstract void SetTarget(Transform playerTrs);
    public abstract void SetSpawner(GameObject mobObj);
}

 

📄 Factory_NormalMob.cs

public class Factory_NormalMob : MobFactory
{
    ...
    [Header("몹 프리펩"), SerializeField] List&lt;GameObject&gt; _normalPrefs;
    Dictionary&lt;eNORMALMOB_TYPE, CEnemy&gt; _enemyDatas = new Dictionary&lt;eNORMALMOB_TYPE, CEnemy&gt;();
    public override void Spawn(eNORMALMOB_TYPE type, Vector3 pos)
    {
        GameObject mob = GetMobByPrefName(_enemyDatas[type]._PrefName);
        GameObject spawnedMob = Instantiate(mob, pos, Quaternion.identity);
        SetSpawner(spawnedMob);
        EnemyInfo info = spawnedMob.GetComponent&lt;EnemyInfo&gt;();
        info.SetStat(_enemyDatas[type]);
        EnemyMove move = spawnedMob.GetComponent&lt;EnemyMove&gt;();
        move.SetPlayerTrs(_playerTrs);
    }
    public GameObject GetMobByPrefName(string prefName)
    {
        GameObject mob = null;
        foreach(GameObject obj in _normalPrefs)
        {
            if(obj.name == prefName)
            {
                mob = obj;
            }
        }
        return mob;
    }
    ...
}

 

📄 Factory_BossMob.cs

public class Factory_BossMob : MobFactory
{
    ...
    public override void Spawn(eNORMALMOB_TYPE type, Vector3 pos)
    {
        ...
    }
    public GameObject GetMobByPrefName(string prefName)
    {
        ...
    }
    ...
}

공통 인터페이스인 MobFactory 추상 클래스를 통해 몬스터 소환의 기본 동작을 정의하고, 각 몹 종류에 맞는 구체 클래스를 분리해 구현했습니다.

Factory_NormalMobFactory_BossMob 클래스는 각각 다른 프리팹 리스트를 기반으로 몬스터를 인스턴스화하고, 스탯 세팅 및 플레이어 타겟 등록을 포함한 초기 설정을 자동화했습니다.

이 구조는 새로운 몹 타입이 추가될 경우, 별도의 팩토리 서브클래스를 만들어 적용할 수 있어 확장성에 유리합니다.

- IK

 

— IK(역운동학)

 

📄 PlayerShooter.cs

public class PlayerShooter : MonoBehaviour
{
    [Header("오른쪽 팔꿈치"), SerializeField] Transform _elbowRTrs;
    [Header("왼쪽 팔꿈치"), SerializeField] Transform _elbowLTrs;
    [Header("오른쪽 손"), SerializeField] Transform _handRTrs;
    [Header("왼쪽 손"), SerializeField] Transform _handLTrs;

    Animator _anim;
    private void Awake()
    {
        _anim = GetComponent&lt;Animator&gt;();
    }
    private void OnAnimatorIK(int layerIndex)
    {
        _elbowRTrs.position = _anim.GetIKHintPosition(AvatarIKHint.RightElbow);

        _anim.SetIKPositionWeight(AvatarIKGoal.RightHand, 1f);
        _anim.SetIKRotationWeight(AvatarIKGoal.RightHand, 1f);

        _anim.SetIKPosition(AvatarIKGoal.RightHand, _handRTrs.position);
        _anim.SetIKRotation(AvatarIKGoal.RightHand, _handRTrs.rotation);
        

        _elbowLTrs.position = _anim.GetIKHintPosition(AvatarIKHint.LeftElbow);

        _anim.SetIKPositionWeight(AvatarIKGoal.LeftHand, 1f);
        _anim.SetIKRotationWeight(AvatarIKGoal.LeftHand, 1f);

        _anim.SetIKPosition(AvatarIKGoal.LeftHand, _handLTrs.position);
        _anim.SetIKRotation(AvatarIKGoal.LeftHand, _handLTrs.rotation);
    }
}

 

Unity의 IK(Inverse Kinematics) 시스템을 활용하여 플레이어 캐릭터의 양손과 팔꿈치 위치를 정밀하게 제어했습니다.

OnAnimatorIK 메서드 안에서 SetIKPositionSetIKRotation 함수를 통해 손의 위치와 회전을 지정하고, IKHint를 활용해 자연스러운 팔꿈치 움직임을 구현했습니다.

 

- 셰이더 그래프

— 툰 셰이더 (Toon Shader)

Unity의 Shader Graph를 활용하여 카툰 렌더링 스타일의 셰이더(툰 셰이더)를 제작했습니다.

이 셰이더는 단순하고 선명한 색 대비로 게임 내 캐릭터와 몬스터에 만화 같은 시각 효과를 적용하는 데 사용됩니다.

 

- 사운드

— 사운드 관리 시스템

사운드는 팩토리 패턴제네릭을 조합하여 효율적으로 관리했습니다. 사운드 종류별로 SoundFactory<T>를 상속한 클래스를 만들고, SoundManager에서 하나의 진입점으로 통합 제어합니다.

 

📄 SoundFactory.cs

public abstract class SoundFactory<T> : MonoBehaviour where T : Enum
{
    public abstract void PlaySound(T soundType);
}

 

📄 SoundUI.cs

public class SoundUI : SoundFactory<eUISOUNDTYPE>
{
    [Header("버튼 클릭 사운드"), SerializeField] AudioSource _buttonSound;
    [Header("게임 시작 사운드"), SerializeField] AudioSource _gameStartSound;
    [Header("아이템 선택 사운드"), SerializeField] AudioSource _getItemSound;

    public override void PlaySound(eUISOUNDTYPE soundType)
    {
        switch (soundType)
        {
            case eUISOUNDTYPE.Button:
                _buttonSound.Play();
                break;
            case eUISOUNDTYPE.GameStart:
                _gameStartSound.Play();
                break;
            case eUISOUNDTYPE.GetItem:
                _getItemSound.Play();
                break;
        }
    }
}

📄 SoundSkill.cs

public class SoundSkill : SoundFactory<eSKILLSOUNDTYPE> 
{ 
	...
    public override void PlaySound(eSKILLSOUNDTYPE soundType) 
    { 
    	switch (soundType) 
        { ... }
	}
}

 

각 사운드 타입별로 클래스를 분리하여 책임을 위임하고, 재생할 사운드만 직관적으로 관리할 수 있도록 구성했습니다.

 

📄 SoundManager.cs

public class SoundManager : MonoBehaviour
{
    static SoundManager instance;
    public static SoundManager Instance
    {
        get
        {
            if (instance == null)
                instance = FindObjectOfType();
            return instance;
        }
    }
    [Header("BGM 사운드"), SerializeField] SoundFactory _bgmSound;
    [Header("UI 사운드"), SerializeField] SoundFactory _uiSound;
    [Header("스킬 사운드"), SerializeField] SoundFactory _skillSound;
    [Header("캐릭터 사운드"), SerializeField] SoundFactory _charSound;
    private void Awake()
    {
        var obj = FindObjectsOfType();
        if (obj.Length == 1)
            DontDestroyOnLoad(gameObject);
        else
            Destroy(gameObject);
    }
    public void PlaySound(T soundType) where T : Enum
    {
        if (typeof(T) == typeof(eUISOUNDTYPE))
        {
            _uiSound.PlaySound((eUISOUNDTYPE)(object)soundType);
        }
        else if (typeof(T) == typeof(eSKILLSOUNDTYPE))
        {
            _skillSound.PlaySound((eSKILLSOUNDTYPE)(object)soundType);
        }
        else if (typeof(T) == typeof(eCHARSOUNDTYPE))
        {
            _charSound.PlaySound((eCHARSOUNDTYPE)(object)soundType);
        }
        else
        {
            Debug.LogWarning($"Unsupported sound type: {typeof(T)}");
        }
    }
    public void PlayBGM(eBGMTYPE type)
    {
        _bgmSound.PlaySound(type);
    }
    public void PlayClickButton()
    {
        _uiSound.PlaySound(eUISOUNDTYPE.Button);
    }
}

 

SoundManager는 Singleton 패턴으로 구현되어 언제든지 전역 접근이 가능하며, PlaySound<T> 제네릭 메서드를 통해 다양한 Enum 타입의 사운드를 재생할 수 있게 만들었습니다. 이를 통해 유지보수성 높은 구조로 UI, 스킬, 캐릭터 등 다양한 사운드 분기를 하나의 메서드로 처리할 수 있습니다.

 

- 데이터 관리

— CSV 형식 데이터 저장

게임에 필요한 다양한 데이터를 CSV(Comma-Separated Values) 형식으로 관리하고, 이를 코드에서 읽어와 객체로 가공해 사용하는 방식을 채택했습니다.

 

데이터는 컬럼(헤더)을 포함한 구조로 작성되며, 행마다 하나의 스킬 정보나 몬스터 정보 등으로 구성됩니다.

Excel에서 작성한 CSV 파일을 저장한 뒤, 이를 Unity에서 `TextAsset`으로 불러와 파싱합니다.

 

📄 CSVReader.cs

CSVReader : MonoBehaviour
{
    
    [Header("일반 스킬"), SerializeField] TextAsset _skillDataFile;
    [Header("전용 스킬"), SerializeField] TextAsset _charSkillDataFile;
    [Header("캐릭터"), SerializeField] TextAsset _charDataFile;
    [Header("몬스터"), SerializeField] TextAsset _mobDataFile;
    ...
    private void Awake()
    {
        ReadCSV(_skillDataFile, _skillData);
        ReadCSV(_charSkillDataFile, _charSkillData);
        ReadCSV(_charDataFile, _charData);
        ReadCSV(_mobDataFile, _mobData);

        CharacterSelector.Instance.SetData(_skillData as SkillData);
        CharacterSelector.Instance.SetData(_charSkillData as CharSkillData);
        CharacterSelector.Instance.SetData(_charData as CharacterData);
        DataHandler.Instance.SetData(_mobData as EnemyData);
    }

    void ReadCSV(TextAsset dataFile, ICSVDataConverter converter)
    {
        string[] lines = System.Text.Encoding.UTF8.GetString(dataFile.bytes).Split('\n');
        List&lt;string&gt; rows = new List&lt;string&gt;(lines);
        converter.ConvertToDictionary(rows);
    }
}

 

CSVReader 클래스는 실제 CSV 파일을 읽고, ICSVDataConverter를 통해 파싱 작업을 위임합니다. 전략 패턴을 적용함으로써 데이터 타입에 따른 분기 없이도 일관된 처리 흐름을 유지할 수 있습니다.

 

— 전략 패턴 기반 데이터 파싱

여러 종류의 데이터를 효율적으로 처리하기 위해 전략 패턴을 도입했습니다. 각 데이터 타입은 ICSVDataConverter 인터페이스를 구현해, 고유한 파싱 로직을 가지도록 설계했습니다.

 

📄 ICSVDataConverter.cs

interface ICSVDataConverter
{
    void ConvertToDictionary(List&lt;string&gt; rows);
}

 

📄 SkillData.cs

public class SkillData : ICSVDataConverter
{
    List&lt;CSkill&gt; _skillDatas = new List&lt;CSkill&gt;();
    public List&lt;CSkill&gt; _SkillDatas =&gt; _skillDatas;
    public void ConvertToDictionary(List&lt;string&gt; rows)
    {
        rows.RemoveAt(0); // 첫 줄(컬럼 헤더) 제거
        rows.RemoveAt(rows.Count - 1);  // 마지막 공백 행 제거
        Dictionary&lt;int, CSkill&gt; skillDictionary = new Dictionary&lt;int, CSkill&gt;();
        foreach (string row in rows)
        {
            string[] values = row.Split(',');
            int id = int.Parse(values[(int)eSKILL_COLUMN.ID]);
            int level = int.Parse(values[(int)eSKILL_COLUMN.Level]);
            CStat stat = new CStat
            {
                _damage = float.Parse(values[(int)eSKILL_COLUMN.Damage]),
                _attRange = string.IsNullOrEmpty(values[(int)eSKILL_COLUMN.AttRange]) ? 0 : float.Parse(values[(int)eSKILL_COLUMN.AttRange]),
                _coolTime = string.IsNullOrEmpty(values[(int)eSKILL_COLUMN.CoolTime]) ? 0 : float.Parse(values[(int)eSKILL_COLUMN.CoolTime]),
                _durTime = string.IsNullOrEmpty(values[(int)eSKILL_COLUMN.DurTime]) ? 0 : float.Parse(values[(int)eSKILL_COLUMN.DurTime]),
                _criRate = string.IsNullOrEmpty(values[(int)eSKILL_COLUMN.CriRate]) ? 0 : float.Parse(values[(int)eSKILL_COLUMN.CriRate]),
                _projectileCount = string.IsNullOrEmpty(values[(int)eSKILL_COLUMN.ProjectileCount]) ? 0 : int.Parse(values[(int)eSKILL_COLUMN.ProjectileCount])
            };
            if (!skillDictionary.ContainsKey(id))
            {
                CSkill skill = new CSkill
                {
                    _id = id,
                    _level = 0,
                    _skillText = values[(int)eSKILL_COLUMN.Skill_Name],
                    _skillName = (eSKILL)System.Enum.Parse(typeof(eSKILL), values[(int)eSKILL_COLUMN.Enum_Name]),
                    _iconName = values[(int)eSKILL_COLUMN.Icon_Name],
                    _synergyType = (eBUFF_TYPE)System.Enum.Parse(typeof(eBUFF_TYPE), values[(int)eSKILL_COLUMN.SynergyType]),
                    _skillMap = new Dictionary&lt;int, CStat&gt;()
                };
                skill._skillMap.Add(level, stat);
                skillDictionary[id] = skill;
            }
            else
            {
                skillDictionary[id]._skillMap.Add(level, stat);
            }
        }
        _skillDatas = new List&lt;CSkill&gt;(skillDictionary.Values);
    }
}

 

스킬 정보는 SkillData 클래스에서 파싱되며, 하나의 스킬 ID 아래에 레벨별 상세 스탯이 Dictionary<int, CStat> 형태로 저장됩니다. 조건에 따라 파싱 여부를 판단하고 기본값 처리도 수행합니다.

 

📄 EnemyData.cs

public class EnemyData : MonoBehaviour, ICSVDataConverter
{
    ...
    public void ConvertToDictionary(List&lt;string&gt; rows)
    {
        ...
    }
}

 

적 데이터도 같은 인터페이스를 통해 별도 클래스에서 분리되어 관리되며, 이후 전투 씬 등에서 사용됩니다.

 

📄 CharacterSelector.cs

public class CharacterSelector : MonoBehaviour, ICSVDataConverter
{
    static CharacterSelector _instance;
    public static CharacterSelector Instance
    {
        get
        {
            if (_instance == null)
                _instance = FindObjectOfType&lt;CharacterSelector&gt;();
            return _instance;
        }
    }
    ...
    private void Awake()
    {
        var obj = FindObjectsOfType&lt;CharacterSelector&gt;();
        if (obj.Length == 1)
            DontDestroyOnLoad(gameObject);
        else
            Destroy(gameObject);
    }
    public void SetData(SkillData data)
    {
        _skillData = data;
    }
    public void SetData(CharSkillData data)
    {
        _charSkillData = data;
    }
    public void SetData(CharacterData data)
    {
        _charData = data;
        _charList = _charData._CharacterDatas;

        _selectedChar = _charList[0];
        LobbyUIManager.Instance.SpawnModelObj(_selectedChar._modelName);
        LobbyUIManager.Instance.SetProductInfos(_charList);
    }
    ...
}

 

CharacterSelector는 로비 씬에서 사용자의 캐릭터 선택 정보를 저장하고, DontDestroyOnLoad를 통해 게임 씬으로 전달되는 Singleton 객체입니다.
또한, SetData 메서드를 오버로딩하여 다양한 데이터 타입(SkillData, CharSkillData, CharacterData)을 동일한 구조로 주입할 수 있도록 구성했습니다.