프로젝트/프로젝트 B

# 007 Development Log

효따 2025. 9. 14. 23:30

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

 


 

안녕하세요!

 

바~로!

 

먼저 작업 중인 영상부터 보고 이야기를 이어가겠습니다!

 


 

작한 내용은 크게 4가지입니다.

 

1. 사운드 재생 작업 BGM & SFX

2. 게임 오버 UI

3. 드론 및 카드 덱 추가

4. Enemy Ground 추가

 


 

먼저 사운드 재생 작업입니다.

 

지난번 작성한 ScriptableObject로 만들어 둔 SoundDatabase에 

 

Lobby / Play / UI 클립들을 추가시켜 주었습니다.

using UnityEngine;

[CreateAssetMenu(fileName = "SoundDatabase", menuName = "Sound/SoundDatabase")]
public class SoundDatabase : ScriptableObject
{
    [Header("Lobby Scene")]
    public AudioClip _lobbyBgm;

    [Header("Play Scene")]
    public AudioClip _playBgm;
    public AudioClip _playGunFire;
    public AudioClip _playJump;
    public AudioClip _playHit;

    [Header("UI")]
    public AudioClip _settingButtonClick;
    public AudioClip _gameStartButtonClick;

}

 


 

사운드 재생도 지난번 작성한 SoundManager의

 

PlayBGM() / PlaySFX() 함수를 사용했습니다.

using UnityEngine;

/// <summary>
/// 사운드 매니저
/// </summary>
public class SoundManager : MonoBehaviour
{
    public static SoundManager Instance;

    [Header("Audio Sources")]
    public AudioSource _bgmSource;
    public AudioSource _sfxSource;

    [Header("Database")]
    public SoundDatabase _soundDatabase;

    [Header("Setting value")]
    public bool _bgmCtrl = true;
    private bool _currentBgmCtrl = true;

    public bool _sfxCtrl = true;
    private bool _currentSfxCtrl = true;

    public bool _vibCtrl = true;
    private bool _currentVibCtrl = true;

    [Header("UI")]
    public bool _currentBgmBtnActive = true;
    public bool _currentSfxBtnActive = true;
    public bool _currentVibBtnActive = true;

    private void Awake()
    {
        if (Instance == null) Instance = this;
        else Destroy(gameObject);

        DontDestroyOnLoad(gameObject);
    }

    /// <summary>
    /// BGM 플레이
    /// </summary>  
    public void PlayBGM(AudioClip clip, float volume = 1f)
    {
        if (clip == null) return;

        // 같은 곡 방지;
        if (_bgmSource.clip == clip && _bgmSource.isPlaying) return;

        _bgmSource.clip = clip;
        _bgmSource.volume = volume;
        // PlayOneShot의 중복 사운드 재생 방지 : Play
        _bgmSource.Play();
    }

    /// <summary>
    /// SFX 플레이
    /// </summary>    
    public void PlaySFX(AudioClip clip, float volume = 1f)
    {
        if (clip == null) return;

        if (_currentSfxCtrl)
            _sfxSource.PlayOneShot(clip, volume);
    }
    /// <summary>
    /// Vib 플레이
    /// </summary>
    public void PlayVib()
    {
        if (_currentVibCtrl)
        {
            Handheld.Vibrate();
        }
    }

    /// <summary>
    /// BGM 버튼 클릭
    /// </summary>
    public void BgmToggle() => _bgmCtrl = _bgmCtrl ? false : true;

    /// <summary>
    /// SFX 버튼 클릭
    /// </summary>
    public void SfxToggle() => _sfxCtrl = _sfxCtrl ? false : true;

    /// <summary>
    /// Vib 버튼 클릭
    /// </summary>
    public void VibToggle() => _vibCtrl = _vibCtrl ? false : true;

    /// <summary>
    /// 세이브 버튼 클릭
    /// </summary>
    public void OnClickSavebutton()
    {
        _currentBgmCtrl = _bgmCtrl;
        _currentSfxCtrl = _sfxCtrl;
        _currentVibCtrl = _vibCtrl;

        _bgmSource.mute = _currentBgmCtrl ? false : true;
        _sfxSource.mute = _currentSfxCtrl ? false : true;
    }

    /// <summary>
    /// 캔슬 버튼 클릭
    /// </summary>
    public void OnClickCanclebutton()
    {
        _bgmCtrl = _currentBgmCtrl;
        _sfxCtrl = _currentSfxCtrl;
        _vibCtrl = _currentVibCtrl;
    }
}

 

해당 함수를 각 사용할 곳을 찾아 다음과 같이 호출을 해줬습니다.

SoundManager.Instance.PlaySFX(SoundManager.Instance._soundDatabase._playGunFire, 0.1f);

 

다음은 게임 오버 관련 작업입니다.

 

게임 오버 UI는 생각해 보니 게임이 끝날 때 팝업되는 Stage Clear! UI와 크게 다를 것이 없다고 생각되어서

 

StageClear! 의 텍스트만 변경하고 똑같이 팝업 되게 재활용했습니다.

using TMPro;
using UnityEngine;

public class GameOverController : MonoBehaviour
{
    [SerializeField] private TextMeshProUGUI _finishText;
    [SerializeField] private GameObject _finishObj;
    public void GameOver()
    {
        GameManager.Instance._gameState = GameManager.GameState.Stop;
        _finishText.text = "Game Over";
        _finishObj.SetActive(true);
    }
}

 

다음은 드론 및 카드 덱 추가 관련 작업입니다.

 

Fixelart를 활용해요 우주선 느낌을 살려보려고 했는데요🤣

 

이게 생각보다 쉽지 않더라고요ㅎㅎ

 

정말.. 믿기 힘드시겠지만(?) 계속 그렸다 지웠다 하면서 상당한 시간을 쏟았습니다.😁

 


 

드론(우주선)의 레이저는 라인랜더러를 사용했습니다.

using System.Collections;
using UnityEngine;

/// <summary>
/// 드론 레이저 컨트롤러
/// </summary>
public class DroneController : MonoBehaviour
{
    [Header("Attack Settings")]
    [SerializeField] private Transform _firePoint;              // 레이저 발사 기준 위치 (드론 자식 위치 등)
    [SerializeField] private float _maxLaserDistance = 20f;     // 레이저 최대 길이
    [SerializeField] private LayerMask _groundMask;             // 레이저 충돌 검사 대상 레이어
    [SerializeField] private string _targetTag = "BreakGround"; // 레이저 충돌 시 텍스트 호출할 태그
    [SerializeField] private int _laserDamage;                  // 레이저 데미지
    private int _randmDamage = 200;                  // 레이저 데미지


    [Header("Laser Visuals")]
    [SerializeField] private LineRenderer _lineRenderer;        // 레이저 시각화를 위한 LineRenderer

    [Header("Timing Settings")]
    [SerializeField] private float _waitDuration = 2f;  // 레이저 발사 전 대기 시간
    [SerializeField] private float _fireDuration = 2f;  // 레이저 발사 시간
    [SerializeField] private float _hitInterval = 0.2f; // 레이저 충돌 시 텍스트 호출 주기(초 단위)
    private float _lastHitTime = 0f;                    // 마지막 호출 시간 기록

    void Awake()
    {
        // LineRenderer 초기 설정
        _lineRenderer.positionCount = 2;    // 시작/끝점 2개
        _lineRenderer.startWidth = 0.1f;    // 시작 지점 두께
        _lineRenderer.endWidth = 0.1f;      // 끝 지점 두께
    }

    void Start()
    {
        // 드론 생성 시 자동 발사 사이클 시작
        StartCoroutine(FireCycleRoutine());
    }

    /// <summary>
    /// 레이저 발사 사이클 코루틴
    /// - 무한 루프: 대기 → 발사 → 대기 → 발사 ...
    /// </summary>
    private IEnumerator FireCycleRoutine()
    {
        while (true)
        {
            while (GameManager.Instance._gameState == GameManager.GameState.Stop)
            {
                yield return null;
            }
            // 대기 구간 : 레이저 꺼둠
            _lineRenderer.enabled = false;
            yield return new WaitForSeconds(_waitDuration);

            // 발사 구간 : 레이저 켜고 UpdateLaser()를 매 프레임 호출
            float elapsed = 0f;
            _lineRenderer.enabled = true;

            while (elapsed < _fireDuration)
            {
                UpdateLaser();              // 레이저 위치/충돌 업데이트
                elapsed += Time.deltaTime;  // 누적 시간
                yield return null;          // 다음 프레임까지 대기
            }
        }
    }

    /// <summary>
    /// 레이저 갱신 함수
    /// </summary>
    private void UpdateLaser()
    {
        Vector2 origin = _firePoint.position;
        Vector2 dir = Vector2.down;

        RaycastHit2D hit = Physics2D.Raycast(origin, dir, _maxLaserDistance, _groundMask);
        Vector3 endPos;

        if (hit.collider != null)
        {
            endPos = hit.point;  // 충돌 위치

            // 태그 체크 후 일정 간격으로 데미지 적용 및 텍스트 표시
            if (hit.collider.CompareTag(_targetTag))
            {
                if (Time.time >= _lastHitTime + _hitInterval)
                {
                    _laserDamage = Random.Range(_randmDamage - 20, _randmDamage + 20);
                    // 데미지 적용
                    hit.collider.GetComponent<BreakGroundController>()?.TakeDamage(_laserDamage);

                    // 데미지 텍스트 표시
                    DamageTextSpawner spawner = FindObjectOfType<DamageTextSpawner>();
                    spawner?.ShowDamage(_laserDamage, hit.point);

                    // 호출 시간 갱신
                    _lastHitTime = Time.time;
                }
            }
        }
        else
        {
            endPos = origin + dir * _maxLaserDistance;
        }

        // LineRenderer 갱신
        _lineRenderer.SetPosition(0, _firePoint.position);
        _lineRenderer.SetPosition(1, endPos);
    }

    /// <summary>
    /// 시각화
    /// </summary>
    void OnDrawGizmosSelected()
    {
        if (_firePoint)
        {
            Gizmos.color = Color.red;
            Gizmos.DrawLine(_firePoint.position, _firePoint.position + Vector3.down * _maxLaserDistance);
        }
    }
}

 

 

타이머를 정해주어 while문으로 일정 시간만큼 매 프레임마다 레이저를 발사하게 해 주었으며

            while (elapsed < _fireDuration)
            {
                UpdateLaser();              // 레이저 위치/충돌 업데이트
                elapsed += Time.deltaTime;  // 누적 시간
                yield return null;          // 다음 프레임까지 대기
            }

 

 

대미지 텍스트 팝업 및 대미지가 입는 로직은 매

 

프레임 그대로 호출하게 되면 60 FPS 기준 초당 60번이 호출되기에 

 

타이머를 정해주어 일정 간격으로 호출되게 구현했습니다.

            // 태그 체크 후 일정 간격으로 데미지 적용 및 텍스트 표시
            if (hit.collider.CompareTag(_targetTag))
            {
                if (Time.time >= _lastHitTime + _hitInterval)
                {
                    _laserDamage = Random.Range(_randmDamage - 20, _randmDamage + 20);
                    // 데미지 적용
                    hit.collider.GetComponent<BreakGroundController>()?.TakeDamage(_laserDamage);

                    // 데미지 텍스트 표시
                    DamageTextSpawner spawner = FindObjectOfType<DamageTextSpawner>();
                    spawner?.ShowDamage(_laserDamage, hit.point);

                    // 호출 시간 갱신
                    _lastHitTime = Time.time;
                }
            }

 

또한 에디터에서 확인을 하기 위하여

 

OnDrawGizmosSelected()를 사용하여, 레이저의 최대 거리를 확인해 주었습니다.🙂

    void OnDrawGizmosSelected()
    {
        if (_firePoint)
        {
            Gizmos.color = Color.red;
            Gizmos.DrawLine(_firePoint.position, _firePoint.position + Vector3.down * _maxLaserDistance);
        }
    }


다음 카드 덱 추가는

 

ScriptableObject로 에디터에서 생성한 NewCardData를 생성해 DronePlus 카드에 사용될

 

기본 설정값들을 맞춰 주었습니다.


 

다음으로 카드가 생성되는 부분에 함수를 추가해 주었는데요!

 

작업을 하면서 테스트를 진행하다 보니, 이전에 Level이 3 이상일 때는 카드가 안 나오게 임시방편으로 막아두려고 했는데

 

주석 처리만 해놓고 작업을 진행하지 않은 것이 있더라고요😅

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;

            // 랜덤 카드 선택, 레벨 3이면 재선택
            CardData cardData;
            int currentLevel;
            do
            {
                int dataIndex = Random.Range(0, _cardDatas.Length);
                cardData = _cardDatas[dataIndex];
                currentLevel = GameManager.Instance._cardManager.GetLevel(cardData._number);
            }
            while (currentLevel >= 3);

            // 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);
        }
    }
}

 

 

수정된 주요 로직은 다음과 같습니다.

            CardData cardData;
            int currentLevel;
            do
            {
                int dataIndex = Random.Range(0, _cardDatas.Length);
                cardData = _cardDatas[dataIndex];
                currentLevel = GameManager.Instance._cardManager.GetLevel(cardData._number);
            }
            while (currentLevel >= 3);

 

현재 카드를 사용자가 선택하면 CardData의 Level을 +1 해주는 로직이 있는데

 

이 CardData의 현재 Level을 판단하여 해당 Level이 3 이상이면 다시 진행되게 해 주었고

 

작업을 하다 보니 텍스트를 먼저 검사하고 뒤에 조건을 검사하는

 

do-while문법이 기억이 나서 사용해 보았습니다.😎


 

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

using System.Collections;
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
    [SerializeField] private Animator cardAnim;                 // 카드 애니메이터
    private int _level = 0; // 카드 레벨
    private CardData _data; // 카드 데이터 참조

    /// <summary>
    /// AddListener Setting
    /// </summary>
    void Awake()
    {
        // 버튼 클릭 시 레벨 업 이벤트 등록
        _cardButton.onClick.AddListener(OnClickLevelUp);
        // 카드 타임 스케일에 영향 받지 않게
        cardAnim.updateMode = AnimatorUpdateMode.UnscaledTime;
    }

    /// <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()
    {
        StartCoroutine(CardClickCorutine());
    }
    /// <summary>
    /// 카드 클릭하면 실행될 코루틴
    /// </summary>
    IEnumerator CardClickCorutine()
    {
        cardAnim.SetTrigger("Click");
        yield return new WaitForSecondsRealtime(1.0f);

        // 카드 레벨업
        LevelUp();
        // 카드 레벨업 효과 적용
        ApplyEffect();
        // 카드 UI 정리 및 레벨업 UI 숨김
        transform.parent.GetComponent<SpawnRandomPrefabs>().ClearCards();
        GameManager.Instance._levelManager.HideLevelUpUI();
    }
    /// <summary>
    /// 카드 레벨 증가
    /// </summary>
    private void LevelUp()
    {
        _level++;

        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;

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

            default:
                break;
        }
    }

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

 

 

카드가 랜덤 스폰이 완료되면 CardUI 스크립트의 SetCardData() 함수부터 시작이 되는데요

 

여기서 카드의 기본 설정값이 비어있는 카드 프리팹에 적용이 되며,

    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();
    }

 

별(Star) 이미지가 현재 Level에 따라 맞게 표시되게 설정을 하는 로직을 지난번에 작성했었습니다.

    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;
        }
    }

 

 

이후 사용자가 카드를 선택하면 실행되는 ApplyEffect() 함수에 DroneEaPlus를 추가시켜 주었고

 

이 ApplyEffect()는 사용자가 카드를 선택한 뒤 효과를 적용하기 위한 함수입니다.

    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;

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

            default:
                break;
        }
    }

 

이 후 카드 효과를 담당하는 SkillManager도 GunEaPlus 함수를 추가시켜 주었으며,

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;  // 광부 생산량 증가 처리

    [Header("Drone")]
    [SerializeField] private DeckDroneEaPlus _deckDroneEaPlus;  // 드론 개수 증가 처리

    /// <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);
    }

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

    /// <summary>
    /// 드론 개수 증가
    /// </summary>
    /// <param name="level">카드 레벨</param>
    public void DroneEaPlus(int level)
    {
        _deckDroneEaPlus.DroneEaPlus(level);
    }
}

 

실제 호출되는 함수는 다음과 같이 현재 Level이 1이라면 -1 = 0으로

 

0번 인덱스의 드론 배열을 활성화시켜주었습니다.

 

(새로 프리팹을 받아와 생성하지 않고, 미리 오브젝트를 생성해 준 뒤 인덱스에 따라 활성화되게끔 구현해 보았습니다.😎)

using UnityEngine;

/// <summary>
/// 드론 건 개수 업
/// </summary>
public class DeckDroneEaPlus : MonoBehaviour
{
    [SerializeField] private GameObject[] _droneObjs;

    public void DroneEaPlus(int level)
    {
        _droneObjs[level - 1].SetActive(true);
    }
}

 

다음은 EnemyGround인데요

 

이것도 마찬가지로 Fixelart를 활용하여 Sprite를 제작해 보았습니다.

 

살짝 슈퍼마리오에 기둥에서 나오는 꽃 같은 느낌을 주려고 했었는데

 

뭐. 시간만 많이 주어졌다면 훨씬 더 멋지게 나왔을 거라고 생각을..

 


 

EnemyGround는 약간 기획에 고민이 많았습니다.

 

초기 생성이 될 때부터 공격을 하게 되면 위에 파괴되지 않은 그라운드가 있음에도 공격 모션이 나와 어색한 상황이 연출될 것 같고

 

생성 후 일정 시간 지연 후 공격하게 하거나 혹은 EnemyGround 마다 매 프레임 레이를 쏘게 하거나 하는 것도 고민을 해봤는데 좀 비효율(?) 과하다(?)는 느낌이 들었습니다.

 

결국 총알(Bullet)에 닿으면 그때 Enemy가 활성화가 되게 하는 방법을 선택했고 코드는 다음과 같습니다.

using System.Collections;
using UnityEngine;
using UnityEngine.UI;

public class BreakGroundEnemy : MonoBehaviour
{
    [SerializeField] private Image _background; // 투명 빨강 (고정)
    [SerializeField] private Image _fill;       // 진한 빨강 (차오름)
    [SerializeField] private float _fillDuration = 5f; // 차오르는 시간
    [SerializeField] private float _resetDelay = 2f;   // 공격 후 대기 시간

    [SerializeField] private Sprite _idleSprite;
    [SerializeField] private Sprite _attackSprite;
    [SerializeField] private SpriteRenderer _enemySpriteRenderer;
    [SerializeField] private BoxCollider2D _enemyBoxCollider2D;
    [SerializeField] private GameObject[] _activeObjects;

    public bool _isStart = false;

    public void StartCoroutine()
    {
        StartCoroutine(FillRoutine());
    }
    public void ObjectActive()
    {
        for (int i = 0; i < _activeObjects.Length; i++)
        {
            _activeObjects[i].SetActive(true);
        }
    }
    public void ObjectInActive()
    {
        for (int i = 0; i < _activeObjects.Length; i++)
        {
            _activeObjects[i].SetActive(false);
        }
    }

    private IEnumerator FillRoutine()
    {
        while (true)
        {
            float elapsed = 0f;
            while (elapsed < _fillDuration)
            {
                elapsed += Time.deltaTime;
                float t = Mathf.Clamp01(elapsed / _fillDuration);
                _fill.fillAmount = t;
                yield return null;
            }

            _enemySpriteRenderer.sprite = _attackSprite;
            _enemyBoxCollider2D.enabled = true;
            // Debug.Log("공격!");

            // 대기
            yield return new WaitForSeconds(_resetDelay);

            // 초기화
            _fill.fillAmount = 0f;
            _enemySpriteRenderer.sprite = _idleSprite;
            _enemyBoxCollider2D.enabled = false;
        }
    }
}

 

코루틴을 활용하여 타이머를 지정하여 Image의 fillAmount를 활용해 공격 범위를 시각적으로 보여주는 연출 효과를 주었고

 

공격 후 일정시간 지연 후 초기화 되게끔 구현해 보았습니다.


 

EnemyGround가 생성되는 조건은 일반 BreakGround의 10의 배수 때 마다입니다.

using UnityEngine;

/// <summary>
/// 파괴 될 그라운드 초기 생성
/// </summary>
public class BreakGroundGenerate : MonoBehaviour
{
    [Header("Component")]
    [SerializeField] private DistanceSlider _distanceSlider;

    [SerializeField] private Collider2D _deathZone;         // 데스존
    [SerializeField] private GameObject _breakGroundPrefab; // 생성할 바닥 프리팹
    [SerializeField] private GameObject _breakGroundEnemyPrefab; // 생성할 바닥 프리팹
    [SerializeField] private int _generateCount = 5;        // 생성할 바닥 개수
    [SerializeField] private float _ySpacing = 0.5f;        // 바닥 간 Y 간격
    [SerializeField] private BoxCollider2D _deathZoneCollider2D;

    [Header("Break Ground Color")]
    private Color _colorWhite = new Color(1f, 1f, 1f);          // 흰색
    private Color _colorOrange = new Color(1f, 0.733f, 0.506f); // 오렌지 계열

    [Header("Other Object To Place")]
    [SerializeField] private GameObject _targetObj;             // 맨 아래에 위치시킬 오브젝트

    /// <summary>
    /// Break Ground 생성
    /// </summary>
    void Start()
    {
        GameObject firstGround = null; // 맨 처음 만든 그라운드 저장용

        for (int i = 0; i < _generateCount; i++)
        {
            if (i > 0 && i % 10 == 0)
            {
                float yPosition = -(_generateCount - 1 - i) * _ySpacing;
                Vector3 position = transform.position + new Vector3(0, yPosition, 0);

                GameObject breakGroundEnemyPrefab = Instantiate(_breakGroundEnemyPrefab, position, Quaternion.identity, transform);
                Physics2D.IgnoreCollision(_deathZoneCollider2D, breakGroundEnemyPrefab.GetComponent<Collider2D>());
                continue; // 일반 breakGround 생성 건너뛰기
            }

            // 각 바닥의 Y 위치 계산
            float yPos = -(_generateCount - 1 - i) * _ySpacing;
            Vector3 pos = transform.position + new Vector3(0, yPos, 0);

            // 바닥 생성
            GameObject breakGround = Instantiate(_breakGroundPrefab, pos, Quaternion.identity, transform);

            // 색상 및 좌우 반전 적용
            SpriteRenderer sr = breakGround.GetComponent<SpriteRenderer>();
            if (sr != null)
            {
                sr.color = (i % 2 == 0) ? _colorWhite : _colorOrange;  // 짝수/홀수 색상 구분
                sr.flipX = (i % 2 != 0);    // 홀수는 좌우 반전
            }

            Physics2D.IgnoreCollision(_deathZone, breakGround.GetComponent<Collider2D>());

            // i == 0일 때(첫 번째 생성된 게 제일 아래)
            if (i == 0)
            {
                firstGround = breakGround;
            }
        }

        // 루프 끝난 뒤, 제일 아래(firstGround) 밑에 새 오브젝트 생성
        if (firstGround != null)
        {
            Vector3 belowPos = firstGround.transform.position + Vector3.down * _ySpacing;
            _targetObj.transform.position = belowPos;
            _targetObj.SetActive(true);

            // Ground, FinishGround : 초기 위치 세팅
            _distanceSlider.DistanceCalculate();
            // Instantiate(_targetObj, belowPos, Quaternion.identity, transform);
        }
    }

}

 


뭔가 오늘 작업 시간에 비해 글이 조금 짧은 느낌이지만…😂

 

ProjectB도 어느덧 끝이 보이는 것 같습니다.

 

제가 감기와 배탈이 동시에 찾아와 요즘 너무 정신이 없는데요😥

 

 

백 마디 말보다 한 번의 행동!

 

결과로 보여드리겠습니다.ㅎㅎ

 


 

그럼 모두 건강 잘 챙기시길 바라며,

 

행복한 하루 보내세요!!!

 

 

감사합니다.

 

 

 

 

 

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

# 008 Portfolio video (with. perf)  (2) 2025.09.22
# 006 Development Log  (0) 2025.09.12
# 005 Development Log  (0) 2025.09.09
# 004 Development Log  (1) 2025.09.07
# 003 Development Log  (1) 2025.09.06