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도 어느덧 끝이 보이는 것 같습니다.
제가 감기와 배탈이 동시에 찾아와 요즘 너무 정신이 없는데요😥
백 마디 말보다 한 번의 행동!
결과로 보여드리겠습니다.ㅎㅎ
그럼 모두 건강 잘 챙기시길 바라며,
행복한 하루 보내세요!!!
감사합니다.