프로젝트/프로젝트 B

# 003 Development Log

효따 2025. 9. 6. 23:52

https://github.com/Kimhyogyeom/ProjectA

 

GitHub - Kimhyogyeom/ProjectA: Unity-MOBA-Prototype

Unity-MOBA-Prototype. Contribute to Kimhyogyeom/ProjectA development by creating an account on GitHub.

github.com


https://github.com/Kimhyogyeom/ProjectB

 

GitHub - Kimhyogyeom/ProjectB: Drill-inspired Project

Drill-inspired Project. Contribute to Kimhyogyeom/ProjectB development by creating an account on GitHub.

github.com


 

ProjectB 작업 진행중 영상

 


 

안녕하세요.

 

ProjectB 작업을 조금씩 진행 중에 있는데요.

 

중간 개발일지를 작성하여 정리를 해놓는것이 좋을 것 같아서,

 

commit Message에 맞게 순서대로

 

작업한 내용을 작성하도록 하겠습니다.

 


 

파괴될 그라운드가 파괴될 때 폭파 Effect 효과를 주었습니다.

 

기존에 작성된 BreakGroundController 스크립트입니다.

using TMPro;
using UnityEngine;
using System.Collections;
using System.Collections.Generic;

/// <summary>
/// 파괴 될 그라운드 컨트롤러
/// </summary>
public class BreakGroundController : MonoBehaviour
{
    [SerializeField] private float _breakGroundHp = 1350;           // 바닥 HP  
    [SerializeField] private TextMeshProUGUI _breakGroundHpText;    // HP 텍스트 UI
    [SerializeField] private GameObject _boomObject;                // 파괴 시 나타나는 효과
    [SerializeField] private BoxCollider2D _breakGroundCollider;    // 바닥 Collider
    [SerializeField] private GameObject _breakGroundText;           // 바닥 텍스트
    [SerializeField] private SpriteRenderer _breakGroundSr;         // 바닥 스프라이트 렌더러
    private Vector3 _originalScale;                                 // 원래 크기 저장

    void Awake()
    {
        _originalScale = transform.localScale;  // 초기 스케일 저장
    }

    /// <summary>
    /// 데미지를 받는 함수
    /// </summary>
    public void TakeDamage(float damage)
    {
        _breakGroundHp -= damage;   // HP 감소
        _breakGroundHpText.text = _breakGroundHp.ToString();    // UI 갱신

        StopAllCoroutines();
        StartCoroutine(HitEffect());    // 히트 이펙트 실행

        if (_breakGroundHp <= 0)
        {
            GetBoomObject();    // HP 0이면 파괴 처리
        }
    }

    /// <summary>
    /// 바닥 히트 시 스케일 확대/축소 효과
    /// </summary>
    private IEnumerator HitEffect()
    {
        Vector3 targetScale = _originalScale * 1.1f;    // 살짝 확대
        float t = 0f;

        // 확대
        while (t < 0.1f)
        {
            transform.localScale = Vector3.Lerp(_originalScale, targetScale, t / 0.1f);
            t += Time.deltaTime;
            yield return null;
        }
        transform.localScale = targetScale;

        // 원래 크기로 복귀
        t = 0f;
        while (t < 0.1f)
        {
            transform.localScale = Vector3.Lerp(targetScale, _originalScale, t / 0.1f);
            t += Time.deltaTime;
            yield return null;
        }
        transform.localScale = _originalScale;
    }

    /// <summary>
    /// 바닥 파괴 시 처리
    /// </summary>
    private void GetBoomObject()
    {
        _boomObject.SetActive(true);            // 파괴 이펙트 활성화
        _breakGroundCollider.enabled = false;   // Collider 해제
        _breakGroundText.SetActive(false);      // 텍스트 비활성화
        _breakGroundSr.enabled = false;         // 스프라이트 비활성화
        StartCoroutine(BreakGroundDisable());   // 일정 시간 후 다시 활성화
    }

    /// <summary>
    /// 파괴 후 일정 시간 지나면 바닥 재활성화
    /// </summary>
    private IEnumerator BreakGroundDisable()
    {
        yield return new WaitForSeconds(1f);
        _boomObject.SetActive(false);           // 파괴 이펙트 비활성화
        _breakGroundCollider.enabled = true;    // Collider 활성화
        _breakGroundText.SetActive(true);       // 텍스트 활성화
        _breakGroundSr.enabled = true;          // 스프라이트 활성화
        gameObject.SetActive(false);            // 바닥 오브젝트 비활성화
    }
}

 

이 중 하단에 GetBoomObject() 함수와, BreakGroundDisable() 코루틴 함수로

 

파괴될 그라운드가 파괴가 될 때 즉시 비활성화를 하지 않고,

 

파괴될 그라운드의 Sprite의 enable = false로 안 보이게 해 준 뒤

 

하위에 붙어있는 Effect 오브젝트를 활성화 한 뒤

 

일정 시간 지연후 파괴될 그라운드를 비활성화되게 해 주었습니다.

 

 

또한 Effect 오브젝트는 애니메이션 실행으로 연출을 주었습니다.

 


 

다음은 MinerController 스크립트의 Fix 사항입니다.

using System.Collections;
using UnityEngine;

/// <summary>
/// 광부 이동 및 채굴 동작을 관리하는 컨트롤러
/// </summary>
public class MinerController : MonoBehaviour
{
    [SerializeField] private byte _minerDirection;          // 0: 왼쪽 시작, 1: 오른쪽 시작
    [SerializeField] private Transform _startPosLeft;       // 왼쪽 시작 위치
    [SerializeField] private Transform _startPosRight;      // 오른쪽 시작 위치
    [SerializeField] private Transform _endPos;             // 이동 후 도착 위치
    public float _minerMoveSpeed = 2f;      // 이동 속도
    public float _minerProduction = 40f;    // 생산량
    [SerializeField] private SpriteRenderer _spriteRenderer;    // 광부 스프라이트 렌더러
    [SerializeField] private Animator _bodyAnim;                // 몸통 애니메이터
    [SerializeField] private Animator _minerAnim;               // 광부 애니메이터
    [SerializeField] private GameObject _minerAnimObj;          // 광부 애니메이션 오브젝트
    [SerializeField] private GameObject _starObj;               // 채굴 후 스타 오브젝트

    /// <summary>
    /// 코루틴 시작
    /// </summary>
    private void Start()
    {
        StartCoroutine(MiningLoop()); // 채굴 루프 시작
    }

    /// <summary>
    /// 광부 무한 채굴 루프
    /// </summary>
    private IEnumerator MiningLoop()
    {
        while (true)
        {
            // 시작 위치로 이동
            if (_minerDirection == 0)
            {
                _spriteRenderer.flipX = false;
                yield return StartCoroutine(MoveToLocalPosition(_startPosLeft.localPosition));
            }
            else if (_minerDirection == 1)
            {
                _spriteRenderer.flipX = true;
                yield return StartCoroutine(MoveToLocalPosition(_startPosRight.localPosition));
            }

            // 채굴 애니메이션 4회 재생
            for (int i = 0; i < 4; i++)
            {
                _bodyAnim.SetTrigger("Mine");
                _minerAnimObj.SetActive(true);
                _minerAnim.SetTrigger("Mine");

                yield return new WaitForSeconds(1f);
            }
            _minerAnimObj.SetActive(false);
            _starObj.SetActive(true);   // 채굴 완료 후 스타 표시

            // 도착 위치 이동 전 스프라이트 방향 변경
            if (_minerDirection == 0) _spriteRenderer.flipX = true;
            else if (_minerDirection == 1) _spriteRenderer.flipX = false;

            // 종료 위치로 이동
            yield return StartCoroutine(MoveToLocalPosition(_endPos.localPosition));

            // UI 게이지 증가
            GameManager.Instance._uiManager.AddGauge(_minerProduction);

            // 이동 후 스프라이트 방향 원복
            if (_minerDirection == 0) _spriteRenderer.flipX = false;
            else if (_minerDirection == 1) _spriteRenderer.flipX = true;
        }
    }

    /// <summary>
    /// 지정된 로컬 위치까지 부드럽게 이동 > endPos : 스타 비활성화
    /// </summary>
    private IEnumerator MoveToLocalPosition(Vector3 targetLocalPos)
    {
        while (Vector3.Distance(transform.localPosition, targetLocalPos) > 0.01f)
        {
            Vector3 dir = (targetLocalPos - transform.localPosition).normalized;
            transform.localPosition += dir * _minerMoveSpeed * Time.deltaTime;
            yield return null;
        }

        transform.localPosition = targetLocalPos;

        if (_starObj.activeSelf)
            _starObj.SetActive(false);
    }
}

 

수정 사항의 핵심 로직은

 

다음과 같습니다.

while (Vector3.Distance(transform.localPosition, targetLocalPos) > 0.01f)
{
    Vector3 dir = (target - transform.position).normalized;
    transform.Translate(dir * _minerMoveSpeed * Time.deltaTime, Space.World);
    Vector3 dir = (targetLocalPos - transform.localPosition).normalized;
    transform.localPosition += dir * _minerMoveSpeed * Time.deltaTime;
    yield return null;
}

 

기존 Space.World로 하면 월드 좌표계 기준 방향(dir)으로 이동하여

 

부모 오브젝트의 위치, 회전, 스케일 등의 위치에 영향을 받지 않고 절대좌표 기준으로 이동하여

 

매 프레임마다 내려가는 Ground의 속도가 빨라지면, Miner들이 공중에 뜨게 되는 현상이 있었습니다.

 

 

그리하여 localPosition로 부모 객체 기준 위치를 받아와 주었고,

 

부모 좌표계 기준으로 움직이는 효과가 나기에

 

Ground의 속도가 조정이 되어도 항상 똑같이 Miner는 움직이게 해 주었습니다.

 


 

다음은 카드 관련 부분입니다.

 

가장 먼저! 카드가 생될 때는

 

애니메이션이 진행되게 작업을 해주었습니다.

 

이번 작품으로 말할 것 같으면.. Scal, Rotation, Color 등.. 역동적인 느낌을 주려고 했습니다.

 


 

먼저 ScriptableObject로 만들 CardData 스크립트를 먼저 보겠습니다.

using UnityEngine;

public enum CardEffectType
{
    None,               // None;
    GunPowerUp,         // 건 파워
    GunEaPlus,          // 건 개수
    MinerSpeed,         // 광부 스피드
    MinerProduction,    // 광부 생산량
}
/// <summary>
/// 카드 ScriptableObject
/// </summary>
[CreateAssetMenu(fileName = "NewCardData", menuName = "Card/CardData")]
public class CardData : ScriptableObject
{
    public int _number;             // 카드 번호
    public string _title;           // 카드 제목
    public string _description;     // 카드 설명
    public string _description2;    // 추가 설명
    public Sprite _image;           // 카드 이미지

    public int _level = 0;          // 카드 레벨
    public Sprite _starBasic;       // 기본 별 이미지
    public Sprite _starLevelUp;     // 레벨 업 시 별 이미지

    public CardEffectType _effectType = CardEffectType.None;  // 카드 효과 타입
}

 

각 카드에 필요한 항목들을 받아오게 해 주었으며

 

enum으로 선언한 CardEffectType으로 어떤 스킬 인지 판단하여 레벨업을 해주었습니다.

 

 

현재는 4가지의 CardData들이 있습니다.

 

건 파워/개수 추가, 광부 생산량/속도 추가

 


 

다음은 CardUI 스크립트입니다.

using TMPro;
using UnityEngine;
using UnityEngine.UI;

/// <summary>
/// 카드 UI 컨트롤러
/// </summary>
public class CardUI : MonoBehaviour
{
    [Header("UI Elements")]
    [SerializeField] private Button _cardButton;                // 카드 클릭 버튼
    [SerializeField] private TextMeshProUGUI _titleText;        // 카드 제목 텍스트
    [SerializeField] private TextMeshProUGUI _descriptionText;  // 카드 설명 텍스트
    [SerializeField] private TextMeshProUGUI _descriptionText2; // 카드 추가 설명 텍스트
    [SerializeField] private Image _iconImage;                  // 카드 아이콘
    [SerializeField] private Image star1;                       // 별 UI 1
    [SerializeField] private Image star2;                       // 별 UI 2
    [SerializeField] private Image star3;                       // 별 UI 3

    private int _level = 0; // 카드 레벨
    private CardData _data; // 카드 데이터 참조

    /// <summary>
    /// AddListener Setting
    /// </summary>
    void Awake()
    {
        // 버튼 클릭 시 레벨 업 이벤트 등록
        _cardButton.onClick.AddListener(OnClickLevelUp);
    }

    /// <summary>
    /// 카드 데이터와 레벨 설정
    /// </summary>
    public void SetCardData(CardData data, int level)
    {
        _data = data;
        _level = level;

        if (_titleText != null) _titleText.text = _data._title;
        if (_descriptionText != null) _descriptionText.text = _data._description;
        if (_descriptionText2 != null) _descriptionText2.text = _data._description2;
        if (_iconImage != null) _iconImage.sprite = _data._image;

        UpdateStarUI();
    }

    /// <summary>
    /// 카드 클릭 시 레벨 업 처리
    /// </summary>
    public void OnClickLevelUp()
    {
        LevelUp();
        ApplyEffect();

        // 카드 UI 정리 및 레벨업 UI 숨김
        transform.parent.GetComponent<SpawnRandomPrefabs>().ClearCards();
        GameManager.Instance._levelManager.HideLevelUpUI();
    }

    /// <summary>
    /// 카드 레벨 증가
    /// </summary>
    private void LevelUp()
    {
        _level++;
        if (_level > 2) _level = 2; // 최대 레벨 제한

        GameManager.Instance._cardManager.SetLevel(_data._number, _level);

        UpdateStarUI();
    }

    /// <summary>
    /// Ster Sprite UI Update
    /// </summary>
    private void UpdateStarUI()
    {
        if (_data == null) return;

        if (_level == 0)
        {
            star1.sprite = _data._starLevelUp;
            star2.sprite = _data._starBasic;
            star3.sprite = _data._starBasic;
        }
        else if (_level == 1)
        {
            star1.sprite = _data._starLevelUp;
            star2.sprite = _data._starLevelUp;
            star3.sprite = _data._starBasic;
        }
        else if (_level == 2)
        {
            star1.sprite = _data._starLevelUp;
            star2.sprite = _data._starLevelUp;
            star3.sprite = _data._starLevelUp;
        }
    }

    /// <summary>
    /// 카드 효과 적용
    /// </summary>
    private void ApplyEffect()
    {
        if (_data == null) return;

        switch (_data._effectType)
        {
            case CardEffectType.GunPowerUp:
                GameManager.Instance._skillManager.GunPowerUp(_level);
                break;

            case CardEffectType.GunEaPlus:
                GameManager.Instance._skillManager.GunEaPlus(_level);
                break;

            case CardEffectType.MinerSpeed:
                GameManager.Instance._skillManager.MinerSpeed(_level);
                break;

            case CardEffectType.MinerProduction:
                GameManager.Instance._skillManager.MinerProduction(_level);
                break;

            default:
                break;
        }
    }

    /// <summary>
    /// 메모리 누수 방지: 파괴될 때 버튼 이벤트 해제
    /// </summary>
    private void OnDestroy()
    {
        if (_cardButton != null)
            _cardButton.onClick.RemoveListener(OnClickLevelUp);
    }
}

 

로직을 간단히 살펴보자면 먼저 Awake에서 AddListener를 등록해 주었습니다.

 

이는 카드를 클릭하면 해당 함수가 실행되게 하기 위함입니다.

    void Awake()
    {
        // 버튼 클릭 시 레벨 업 이벤트 등록
        _cardButton.onClick.AddListener(OnClickLevelUp);
    }

 

 

다음 SetCardDate() 함수입니다.

 

이는 카드가 생성될 때 해당 카드의 데이터를 정해주는 함수이며

 

ScriptableObject에 받아온 해당 항목들을 대입해 주는 로직입니다.

    public void SetCardData(CardData data, int level)
    {
        _data = data;
        _level = level;

        if (_titleText != null) _titleText.text = _data._title;
        if (_descriptionText != null) _descriptionText.text = _data._description;
        if (_descriptionText2 != null) _descriptionText2.text = _data._description2;
        if (_iconImage != null) _iconImage.sprite = _data._image;

        UpdateStarUI();
    }

 

 

UpdateStartUI는 현재 레벨에 따라 다르게 관리하기 위해 함수로 따로 작성을 했습니다.

starLevelUP : 노란색 별

starBasic : 비어있는 별

    private void UpdateStarUI()
    {
        if (_data == null) return;

        if (_level == 0)
        {
            star1.sprite = _data._starLevelUp;
            star2.sprite = _data._starBasic;
            star3.sprite = _data._starBasic;
        }
        else if (_level == 1)
        {
            star1.sprite = _data._starLevelUp;
            star2.sprite = _data._starLevelUp;
            star3.sprite = _data._starBasic;
        }
        else if (_level == 2)
        {
            star1.sprite = _data._starLevelUp;
            star2.sprite = _data._starLevelUp;
            star3.sprite = _data._starLevelUp;
        }
    }

 

 

다음은 카드를 클릭했을 때 실행되는 OnClickLevelUp() 함수입니다.

 

이 함수에서는 사용자가 카드를 선택해 해당 카드를 LevelUp 하겠다는 의미로

 

LevelUp 함수와 ApplyEffect 함수를 호출합니다.

    public void OnClickLevelUp()
    {
        LevelUp();
        ApplyEffect();

        // 카드 UI 정리 및 레벨업 UI 숨김
        transform.parent.GetComponent<SpawnRandomPrefabs>().ClearCards();
        GameManager.Instance._levelManager.HideLevelUpUI();
    }

 

 

먼저 LevelUp 함수입니다.

 

레벨업의 최대는 2로 초기레벨은 0입니다.

 

즉 0, 1, 2로 별 세 개를 나타내 주었으며 레벨에 맞게 UpdateStarUI 함수를 실행시켜 주었습니다.

    private void LevelUp()
    {
        _level++;
        if (_level > 2) _level = 2; // 최대 레벨 제한

        GameManager.Instance._cardManager.SetLevel(_data._number, _level);

        UpdateStarUI();
    }

 

 

다음은 ApplyEffect 함수입니다.

 

싱글톤으로 만든 GameManager에 접근하여 해당 카드를 선택했을 때 실행되는 로직이 담긴 함수를

 

호출합니다.

    private void ApplyEffect()
    {
        if (_data == null) return;

        switch (_data._effectType)
        {
            case CardEffectType.GunPowerUp:
                GameManager.Instance._skillManager.GunPowerUp(_level);
                break;

            case CardEffectType.GunEaPlus:
                GameManager.Instance._skillManager.GunEaPlus(_level);
                break;

            case CardEffectType.MinerSpeed:
                GameManager.Instance._skillManager.MinerSpeed(_level);
                break;

            case CardEffectType.MinerProduction:
                GameManager.Instance._skillManager.MinerProduction(_level);
                break;

            default:
                break;
        }
    }

 


 

다음은 카드가 생성될 때 실행되는 함수가 담긴 SpawnRandomPrefabs 스크립트입니다.

using System.Collections;
using UnityEngine;

/// <summary>
/// 카드 프리팹을 랜덤하게 생성하고 관리하는 클래스
/// </summary>
public class SpawnRandomPrefabs : MonoBehaviour
{
    [Header("Prefab & Data")]
    [SerializeField] private GameObject _cardPrefab;    // 카드 프리팹
    [SerializeField] private CardData[] _cardDatas;     // 카드 데이터 배열
    [SerializeField] private int _spawnCount = 3;       // 한 번에 생성할 카드 개수
    [SerializeField] private float _spawnDelay = 0.5f;  // 카드 생성 간격(초)

    /// <summary>
    /// 카드 생성 시작
    /// </summary>
    public void StartCardSpawn()
    {
        StartCoroutine(CardSpawnCoroutine());
    }

    /// <summary>
    /// 카드 생성 코루틴
    /// </summary>
    private IEnumerator CardSpawnCoroutine()
    {
        // 프리팹이나 카드 데이터가 없으면 종료
        if (_cardPrefab == null || _cardDatas == null || _cardDatas.Length == 0)
            yield break;

        // 첫 카드 생성 전 대기
        yield return new WaitForSecondsRealtime(_spawnDelay);

        for (int i = 0; i < _spawnCount; i++)
        {
            // 카드 프리팹 생성
            GameObject cardObj = Instantiate(_cardPrefab, transform);

            // 애니메이터가 있다면 시간 스케일에 무관하게 업데이트
            Animator anim = cardObj.GetComponent<Animator>();
            if (anim != null)
                anim.updateMode = AnimatorUpdateMode.UnscaledTime;

            // 카드 데이터 랜덤 선택
            int dataIndex = Random.Range(0, _cardDatas.Length);
            CardData cardData = _cardDatas[dataIndex];

            // 카드 현재 레벨 가져오기
            int currentLevel = GameManager.Instance._cardManager.GetLevel(cardData._number);

            // CardUI 세팅
            CardUI cardUI = cardObj.GetComponent<CardUI>();
            if (cardUI != null)
                cardUI.SetCardData(cardData, currentLevel);

            // 다음 카드 생성 전 대기
            yield return new WaitForSecondsRealtime(_spawnDelay);
        }
    }

    /// <summary>
    /// 생성된 모든 카드 제거
    /// </summary>
    public void ClearCards()
    {
        for (int i = transform.childCount - 1; i >= 0; i--)
        {
            Destroy(transform.GetChild(i).gameObject);
        }
    }
}

 

StartCardSoawn 함수가 호출되면 StarCorutine()를 사용해 코루틴을 실행시켜 주었으며,

 

특이사항으로 

 

Animator의 updateMode를 AnimatorUpdateMode.UnscaledTime으로 지정해 주어

 

Time.Scale에 영향을 받지 않게 해 주었습니다.


 

위의 SpawnRandomPrefabs 스크립트의 StartCardSoawn 함수가 호출될 때는

 

LevelUP = Guage가 가득 찼을 때입니다.

 

게이지가 가득 차 레벨이 업을 하게 되면 ShowLevelUpUI가 호출되게 해 주었는데요

 

이때 UI 오브젝트 활성화와 SpawnRandomPrefabs. StartCardSoawn  가 호출됩니다.

using UnityEngine;
using UnityEngine.UI;

/// <summary>
/// 레벨 업 관련 UI 및 카드 스폰을 관리하는 매니저
/// </summary>
public class LevelManager : MonoBehaviour
{
    [SerializeField] private SpawnRandomPrefabs spawnRandomPrefabs; // 카드 스폰 매니저
    [SerializeField] private Image _levelUpPopup;       // 레벨 업 팝업 UI
    [SerializeField] private Animator _animatorTitle;   // 레벨 업 타이틀 애니메이터

    /// <summary>
    /// 레벨 업 UI를 화면에 보여주는 함수
    /// </summary>
    public void ShowLevelUpUI()
    {
        if (_levelUpPopup != null)
            _levelUpPopup.gameObject.SetActive(true);   // 팝업 활성화

        if (_animatorTitle != null)
            _animatorTitle.updateMode = AnimatorUpdateMode.UnscaledTime;    // 애니메이션을 일시정지에도 동작하도록 설정

        if (spawnRandomPrefabs != null)
            spawnRandomPrefabs.StartCardSpawn();    // 카드 스폰 시작

        Time.timeScale = 0f;    // 게임 일시정지
    }

    /// <summary>
    /// 레벨 업 UI를 숨기고 게임 진행을 재개하는 함수
    /// </summary>
    public void HideLevelUpUI()
    {
        if (_levelUpPopup != null)
            _levelUpPopup.gameObject.SetActive(false);  // 팝업 비활성화

        Time.timeScale = 1f;    // 게임 시간 재개
    }
}

 

아래 HideLevelUpUI 함수는 

 

레벨업을 했을 때 활성화 되는 UI를 비활성화해주기 위함입니다.

    public void HideLevelUpUI()
    {
        if (_levelUpPopup != null)
            _levelUpPopup.gameObject.SetActive(false);  // 팝업 비활성화

        Time.timeScale = 1f;    // 게임 시간 재개
    }

 


 

다음은 CardManager 스크립트입니다.

 

해당 카드의 레벨업을 해주기 위해 Dictionary로 (카드 번호, 레벨)을 관리해 주었으며

 

카드의 번호를 설정할 SetLevel 함수입니다.

카드의 가져올 GetLevel 함수입니다.

using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 카드별 레벨을 관리하는 클래스
/// </summary>
public class CardManager : MonoBehaviour
{
    /// <summary>
    /// Key : 카드 번호, Value : 레벨
    /// </summary>
    private Dictionary<int, int> _cardLevels = new Dictionary<int, int>();

    /// <summary>
    /// 특정 카드 번호의 레벨 가져오기
    /// </summary>
    /// <param name="cardNumber">카드 번호</param>
    /// <returns></returns>
    public int GetLevel(int cardNumber)
    {
        if (_cardLevels.TryGetValue(cardNumber, out int level))
            return level;
        return 0; // 등록된 레벨이 없으면 0 반환
    }

    /// <summary>
    /// 특정 카드 번호의 레벨 설정
    /// </summary>
    /// <param name="cardNumber">카드 번호</param>
    /// <param name="level">레벨</param>
    public void SetLevel(int cardNumber, int level)
    {
        _cardLevels[cardNumber] = level;
    }
}

 


 

다음은 SkillManager 스크립트입니다.

 

각 카드 종류별로 스크립트를 작성하여 한 곳에 모아 작업하지 않고 분할을 해주었습니다.

 

해당 카드의 스크립트에 접근하여 필요한 함수만 호출시켜 줍니다.

using UnityEngine;

/// <summary>
/// 스킬 관련 기능을 관리하는 매니저
/// - Gun(총) 관련: 개수 증가, 파워 증가
/// - Miner(광부) 관련: 생산량 증가
/// </summary>
public class SkillManager : MonoBehaviour
{
    [Header("Gun")]
    [SerializeField] private DeckGunEaPlus _deckGunEaPlus;          // 총 개수 증가 처리
    [SerializeField] private DeckGunPowerUp _deckGunPowerUp;        // 총 데미지 증가 처리

    [Header("Miner")]
    [SerializeField] private DeckMinerSpeed _deckMinerSpeed;            // 광부 스피드
    [SerializeField] private DeckMinerProduction _deckMinerProduction;  // 광부 생산량 증가 처리

    /// <summary>
    /// 머신건 개수 증가
    /// </summary>
    /// <param name="level">카드 레벨</param>
    public void GunEaPlus(int level)
    {
        _deckGunEaPlus.GunEaPlus(level);
    }

    /// <summary>
    /// 머신건 데미지 증가
    /// </summary>
    /// <param name="level">카드 레벨</param>
    public void GunPowerUp(int level)
    {
        _deckGunPowerUp.GunPowerUp(level);
    }

    //──────────────────────────────────────────────────────────────────────

    /// <summary>
    /// 광부 스피드
    /// </summary>
    /// <param name="level">카드 레벨</param>
    public void MinerSpeed(int level)
    {
        _deckMinerSpeed.MinerSpeed(level);
    }

    /// <summary>
    /// 광부 생산량 증가
    /// </summary>
    /// <param name="level">카드 레벨</param>
    public void MinerProduction(int level)
    {
        _deckMinerProduction.MinerProduction(level);
    }

}

 


 

먼저 DeckGunEaPlus 스크립트입니다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 덱 건 개수 업
/// </summary>
public class DeckGunEaPlus : MonoBehaviour
{
    [Header("Gun Settings")]
    [SerializeField] private Transform _gunParentObject;    // 총들을 담는 부모 오브젝트
    [SerializeField] private Transform _gunGenPos;          // 총 생성 기준 위치
    [SerializeField] private float _spacing = 0.2f;         // 총 사이 간격
    [SerializeField] private float _damageReduction = 0.8f; // 총 데미지 감소 비율
    [SerializeField] private GameObject[] _guns;            // 미리 만들어 놓은 총들

    /// <summary>
    /// 총 개수와 데미지를 레벨에 맞춰 설정
    /// </summary>
    /// <param name="level">카드 레벨</param>
    public void GunEaPlus(int level)
    {
        // 모든 총을 비활성화
        foreach (var gun in _guns)
        {
            gun.SetActive(false);
        }

        // 레벨 + 1 만큼 총을 활성화, 총 배열 길이 초과 방지
        int count = Mathf.Min(level + 1, _guns.Length);

        // 중앙 기준으로 좌우 균등 배치
        float totalWidth = (count - 1) * _spacing;
        float startX = -totalWidth / 2f;

        for (int i = 0; i < _guns.Length; i++)
        {
            if (i < count)
            {
                // 위치 계산 및 적용
                Vector3 offset = new Vector3(startX + i * _spacing, 0f, 0f);
                _guns[i].transform.position = _gunGenPos.position + offset;
                _guns[i].SetActive(true);

                // 총알 데미지 감소 적용
                List<GameObject> bullets = _guns[i].GetComponent<GunFire>()._bulletPool;
                foreach (var item in bullets)
                {
                    item.GetComponent<GunBullet>()._bulletDamage *= _damageReduction;
                }
            }
            else
            {
                _guns[i].SetActive(false);
            }
        }
    }
}

 

모든 총을 초기에 비활성화시켜준 이유는, 총이 발사되는 로직을 모두 맞추기 위해서입니다.

 

또한 현재 레벨만큼 활성화가 될 Gun의 개수를 count로 지정하여

 

좌우 균등 배치를 하게 해 주었습니다.

        // 레벨 + 1 만큼 총을 활성화, 총 배열 길이 초과 방지
        int count = Mathf.Min(level + 1, _guns.Length);

        // 중앙 기준으로 좌우 균등 배치
        float totalWidth = (count - 1) * _spacing;
        float startX = -totalWidth / 2f;

 

 

또한 해당 카드는 Gun + 1 / Power - 20% 이기에

 

Bullet의 GunBullet 스크립트에 접근하여 대미지를 낮춰주었습니다.

 

        for (int i = 0; i < _guns.Length; i++)
        {
            if (i < count)
            {
                // 위치 계산 및 적용
                Vector3 offset = new Vector3(startX + i * _spacing, 0f, 0f);
                _guns[i].transform.position = _gunGenPos.position + offset;
                _guns[i].SetActive(true);

                // 총알 데미지 감소 적용
                List<GameObject> bullets = _guns[i].GetComponent<GunFire>()._bulletPool;
                foreach (var item in bullets)
                {
                    item.GetComponent<GunBullet>()._bulletDamage *= _damageReduction;
                }
            }
            else
            {
                _guns[i].SetActive(false);
            }
        }

 


 

다음은 DeckGunPowerUp 스크립트입니다.

 

해당 스크립트는 power + 40% 의 설명을 담고 있기에,

 

Bullet의 GunBullet 스크립트에 접근하여 대미지를 상승시켜 주었습니다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 덱 건 파워 업
/// </summary>
public class DeckGunPowerUp : MonoBehaviour
{
    [Header("Gun Settings")]
    [SerializeField] private GameObject[] _guns;            // 총 배열
    [SerializeField] private float _damageIncrease = 1.4f;  // 데미지 증가 배율

    /// <summary>
    /// 총 개수 / 레벨에 따라 데미지 증가
    /// </summary>
    /// <param name="level">카드 레벨</param>
    public void GunPowerUp(int level)
    {
        // 레벨 + 1 만큼 총 적용, 배열 길이 초과 방지
        int count = Mathf.Min(level + 1, _guns.Length);

        for (int i = 0; i < count; i++)
        {
            // 총의 총알 풀 가져오기
            List<GameObject> bullets = _guns[i].GetComponent<GunFire>()._bulletPool;
            foreach (var bullet in bullets)
            {
                // 총알 데미지 증가 적용
                bullet.GetComponent<GunBullet>()._bulletDamage *= _damageIncrease;
            }
        }
    }
}

 


 

다음은 광부의 생산량 상승 스크립트 DeckMinerProduction입니다.

 

해당 스크립트도 특이사항은 없으며, miner에 접근하여 해당 생산량(minerProduction)을 상향시켜주었습니다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 광부 생산량 업
/// </summary>
public class DeckMinerProduction : MonoBehaviour
{
    [SerializeField] private GameObject[] _miners;          // Miners
    [SerializeField] private float _productionValue = 0.1f; // 10%

    /// <summary>
    /// 생산량 업 / 레벨에 따라 생산량 증가
    /// </summary>
    /// <param name="level">카드 레벨</param>
    public void MinerProduction(int level)
    {
        foreach (var miner in _miners)
        {
            MinerController controller = miner.GetComponent<MinerController>();
            if (controller != null)
            {
                controller._minerProduction += controller._minerProduction * _productionValue * level;
            }
        }
    }
}

 


 

다음은 DeckMinerSpeed 스크립트입니다.

 

miner에 접근하여 해당 스피드(minerMoveSpeed)를 상향시켜주었습니다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 광부 스피드 업
/// </summary>
public class DeckMinerSpeed : MonoBehaviour
{
    [SerializeField] private GameObject[] _miners;  // Miners
    [SerializeField] private float _speedIncreasePercent = 0.3f; // 30%

    /// <summary>
    /// 스피드 업 / 레벨에 따라 스피드 증가
    /// </summary>
    /// <param name="level">카드 레벨</param>
    public void MinerSpeed(int level)
    {
        foreach (var minerObj in _miners)
        {
            MinerController controller = minerObj.GetComponent<MinerController>();
            if (controller != null)
            {
                controller._minerMoveSpeed += controller._minerMoveSpeed * _speedIncreasePercent * level;
            }
        }
    }
}

 


 

마지막으로 모든 스크립트에 주석 작업을 진행했습니다.

 

 

보안과제를 주로 작업하면서, 주석은 웬만하면 달지 않는 것이 좋다고 하여 습관을 들이진 못했는데,

 

여러 함수에 접근하려 할 때 접근할 함수의 명칭을 보고 유추는 가능하지만

 

확실하게 하기 위해 해당 함수를 한번 더 들여다보는 작업들이 조금은 비효율적이라고 판단되어

 

우선은 제가 보기 편한 방향으로 작업을 해봤습니다.

 

(주석 습관을 들이겠습니다.)

 


 

오늘 중점으로 작업한 내용은 다음과 같습니다.

 

1.ScriptableObject로 카드 덱을 편리하게 관리한 것

 

2. 오브젝트들을 생성/삭제보다는 활성화/비활성화하려고 한 것

 

3. 각 카드를 선택하여 레벨업시 실행되는 로직 작업

 

4. UI 애니메이션 작업

 

5. 주석작업

 


 

 

감사합니다.

 

 

'프로젝트 > 프로젝트 B' 카테고리의 다른 글

# 005 Development Log  (0) 2025.09.09
# 004 Development Log  (1) 2025.09.07
# 002 Development Log  (2) 2025.09.05
# 001 Development Log  (3) 2025.09.04
인사말  (0) 2025.09.04