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. 공격이 가능한 위치일 때만 점프 게이지 차오르게끔 추가
2. 아래로 내려가는 Ground 위치에 따라서 게이지 차게끔 추가
3. Finish 선에 위치했을 때 게임 성공 추가
4. Reward 시스템 추가
먼저 게임 상태를 관리하는 GameManager에 각 State들을 추가시켜 주었습니다.
using UnityEngine;
/// <summary>
/// 게임 전반을 관리하는 싱글톤 GameManager
/// </summary>
public class GameManager : MonoBehaviour
{
// 싱글톤 인스턴스
public static GameManager Instance = null;
[Header("Managers")]
public SkillManager _skillManager; // 스킬 관련 관리
public CardManager _cardManager; // 카드 레벨 관리
public UIManager _uiManager; // UI 관리
public LevelManager _levelManager; // 레벨 관리
[Header("In Game")]
public CoinManager _coinManager; // 코인 관련
public PossessionManager _possessionManager; // 소지 목록 관련
public enum GameState
{
None, // Null
Ready, // 대기
Attack, // 공격
Stop, // 끝
}
public GameState _gameState = GameState.None;
/// <summary>
/// 싱글톤 초기화
/// </summary>
private void Awake()
{
if (Instance == null)
{
Instance = this;
}
else
{
// 이미 존재하면 중복 제거
Destroy(this.gameObject);
}
}
}
다음은 총알 발사 부분을 담당하는 GunFire 스크립트에
Raycast를 발사하여 일정 거리 내에 BreakGround가 있다면, 총알을 발사 / 게임 상태는 Attack
BreakGround가 없다면 게임 상태를 Ready로 해줬습니다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 총 발사 컨트롤러
/// </summary>
public class GunFire : MonoBehaviour
{
[SerializeField] private GameObject _bulletPrefab; // 생성할 총알 프리팹
[SerializeField] private float _fireRate = 0.5f; // 발사 간격 (초)
[SerializeField] private Transform _fireStartPoint; // 총알 발사 시작 위치
[Header("Gun Sprites")]
[SerializeField] private Sprite _spriteA; // 기본 총 이미지
[SerializeField] private Sprite _spriteB; // 발사 애니메이션 이미지
[SerializeField] private SpriteRenderer _gunRenderer; // 총 스프라이트 렌더러
[Header("Raycast Settings")]
[SerializeField] private float _rayDistance = 10f; // 레이캐스트 거리
[SerializeField] private LayerMask _groundLayer; // BreakGround 전용 레이어
private float _fireTimer = 0f; // 발사 타이머
public List<GameObject> _bulletPool = new List<GameObject>(); // 총알 풀
/// <summary>
/// 오브젝트 활성화 시
/// </summary>
void OnEnable()
{
_fireTimer = 0; // 활성화될 때 타이머 초기화
}
/// <summary>
/// 타이머 체크 & 총알 발사(생성)
/// </summary>
void Update()
{
if (GameManager.Instance._gameState == GameManager.GameState.Stop) return;
_fireTimer += Time.deltaTime; // 시간 누적
if (_fireTimer >= _fireRate)
{
// Raycast
RaycastHit2D hit = Physics2D.Raycast(_fireStartPoint.position, Vector2.down, _rayDistance, _groundLayer);
if (hit.collider != null && hit.collider.CompareTag("BreakGround"))
{
GameManager.Instance._gameState = GameManager.GameState.Attack;
// 총알 생성
GenerateBullet();
}
else
{
GameManager.Instance._gameState = GameManager.GameState.Ready;
}
// hit == null & hit collider tag == BreakGround : Timer reset;
_fireTimer = 0f;
}
}
/// <summary>
/// 총 발사 애니메이션, 총알(Pooling)
/// </summary>
void GenerateBullet()
{
// 총 발사 Sprite
StartCoroutine(SwitchSprite());
// 총알 풀에서 비활성화된 총알을 찾아 재사용
foreach (GameObject bullet in _bulletPool)
{
if (!bullet.activeSelf)
{
bullet.transform.position = _fireStartPoint.position;
bullet.transform.rotation = Quaternion.identity;
bullet.SetActive(true);
return;
}
}
// 사용 가능한 총알이 없으면 새로 생성 후 풀에 추가
GameObject newBullet = Instantiate(_bulletPrefab, _fireStartPoint.position, Quaternion.identity);
_bulletPool.Add(newBullet);
}
/// <summary>
/// 총알 애니메이션(Sprite Chage)
/// </summary>
private IEnumerator SwitchSprite()
{
_gunRenderer.sprite = _spriteB; // 발사 이미지로 변경
yield return new WaitForSeconds(0.1f);
_gunRenderer.sprite = _spriteA; // 다시 기본 이미지로 복원
}
/// <summary>
/// Scene 뷰에서 레이 확인 (에디터 전용)
/// </summary>
private void OnDrawGizmosSelected()
{
if (_fireStartPoint == null) return;
Gizmos.color = Color.red;
Gizmos.DrawLine(_fireStartPoint.position, _fireStartPoint.position + Vector3.down * _rayDistance);
}
}
다음은 GroundGauge 스크립트를 추가시켜 주었습니다.
해당 공격 상태가 Attack일 때만 게이지가 차게 해 주었습니다.
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
public class GroundGauge : MonoBehaviour
{
[SerializeField] private Slider _slider; // 점프 버튼 슬라이더
[SerializeField] private float _recoverSpeed = 5f; // 초당 채워지는 양
/// <summary>
/// 코루틴 실행
/// </summary>
private void Start()
{
StartCoroutine(FillRoutine());
}
private IEnumerator FillRoutine()
{
while (true)
{
// 게임 상태가 공격 중 이라면
if (GameManager.Instance._gameState == GameManager.GameState.Attack)
{
// value가 maxValue(150) 보다 작다면
if (_slider.value < _slider.maxValue)
{
// 초 당 상승
_slider.value += _recoverSpeed * Time.deltaTime;
// value가 maxValue(150) 보다 크다면 value = maxValue
if (_slider.value > _slider.maxValue)
_slider.value = _slider.maxValue;
}
}
// 게임 상태 Ready
yield return null;
}
}
}
다음은 왼쪽의 Slide 관리 부분입니다.
DistanceCalculate 함수가 실행되면 그라운드 오브젝트와, 마지막 Finish 오브젝트를 가져와 거리를 계산해 주었습니다.
또한 Mathf.InverseLerp을 사용해서 해당 거리가 작아질수록 변숫값 이 1에 가까워지게 해 주었고,
이 해당 변숫값을 Slider.value에 대입해 주었습니다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// 그라운드와 Finish 그라운드 거리 비율 to 슬라이더
/// </summary>
public class DistanceSlider : MonoBehaviour
{
[SerializeField] private FinishController _finishController; // 끝났을 때 컴포넌트
[SerializeField] private Transform _groundObj; // 그라운드
[SerializeField] private Transform _finishObj; // 피니쉬 그라운드
[SerializeField] private Slider _gaugeSlider; // 게이지 슬라이드
private float startDistance; // 초기 거리 (_groundObj, _finishObj)
private bool _isReached = false; // 1회 실행 위한 플래그
/// <summary>
/// _finishObj 오브젝트 활성화가 될 때 호출되는 거리 계산 함수
/// </summary>
public void DistanceCalculate()
{
// 시작할 때 두 오브젝트 사이의 거리 저장
startDistance = Vector3.Distance(_groundObj.position, _finishObj.position);
}
/// <summary>
/// InverseLerp : A와 B 사이에서 C의 선형 비율 계산 0~1
/// </summary>
void Update()
{
if (GameManager.Instance._gameState == GameManager.GameState.Stop) return;
float currentDistance = Vector3.Distance(_groundObj.position, _finishObj.position);
// 거리가 줄어들수록 1에 가까워짐
float value = Mathf.InverseLerp(startDistance, 0f, currentDistance);
_gaugeSlider.value = value;
// 1회 실행
if (value >= 0.99f && !_isReached)
{
_isReached = true;
_gaugeSlider.value = 1f; // 확실히 1로 고정
GameManager.Instance._gameState = GameManager.GameState.Stop;
// print("Finish");
_finishController.GameFinish();
}
}
}
위에서 사용한 FinishGround는 초기 BreakGround들을 생성하는
BreakGroundGenerate 스크립트에서 추가시켜 주었습니다.
거꾸로 맨 아래에서부터 BreakGround가 생성되는 for문에서
i가 0일 때 _ySpacing 만큼 아래로 띄워서
Finish Ground가 활성화되게 해 주었습니다.
using UnityEngine;
/// <summary>
/// 파괴 될 그라운드 초기 생성
/// </summary>
public class BreakGroundGenerate : MonoBehaviour
{
[Header("Component")]
[SerializeField] private DistanceSlider _distanceSlider;
[SerializeField] private Collider2D _deathZone; // 데스존
[SerializeField] private GameObject _breakGroundPrefab; // 생성할 바닥 프리팹
[SerializeField] private int _generateCount = 5; // 생성할 바닥 개수
[SerializeField] private float _ySpacing = 0.5f; // 바닥 간 Y 간격
[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++)
{
// 각 바닥의 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);
}
}
}
이후 DistanceCalculate 함수를 호출하여
초기 Ground와 Finish Ground의 거리를 계산해 줬습니다.
또한 위에서 본 DistanceSlider 스크립트에서
Value의 값이 0.99f 이상이면 게임의 상태를 Stop이 되게 해 주었는데요
여기서 1이 아닌 0.99f로 잡은 이유는 float 값 이 정확히 1이 안될 수 있기 때문입니다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// 그라운드와 Finish 그라운드 거리 비율 to 슬라이더
/// </summary>
public class DistanceSlider : MonoBehaviour
{
[SerializeField] private FinishController _finishController; // 끝났을 때 컴포넌트
[SerializeField] private Transform _groundObj; // 그라운드
[SerializeField] private Transform _finishObj; // 피니쉬 그라운드
[SerializeField] private Slider _gaugeSlider; // 게이지 슬라이드
private float startDistance; // 초기 거리 (_groundObj, _finishObj)
private bool _isReached = false; // 1회 실행 위한 플래그
/// <summary>
/// _finishObj 오브젝트 활성화가 될 때 호출되는 거리 계산 함수
/// </summary>
public void DistanceCalculate()
{
// 시작할 때 두 오브젝트 사이의 거리 저장
startDistance = Vector3.Distance(_groundObj.position, _finishObj.position);
}
/// <summary>
/// InverseLerp : A와 B 사이에서 C의 선형 비율 계산 0~1
/// </summary>
void Update()
{
if (GameManager.Instance._gameState == GameManager.GameState.Stop) return;
float currentDistance = Vector3.Distance(_groundObj.position, _finishObj.position);
// 거리가 줄어들수록 1에 가까워짐
float value = Mathf.InverseLerp(startDistance, 0f, currentDistance);
_gaugeSlider.value = value;
// 1회 실행
if (value >= 0.99f && !_isReached)
{
_isReached = true;
_gaugeSlider.value = 1f; // 확실히 1로 고정
GameManager.Instance._gameState = GameManager.GameState.Stop;
// print("Finish");
_finishController.GameFinish();
}
}
}
마지막에 GameFinish() 함수를 호출하는데
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// 끝났을 때 컨트롤러
/// </summary>
public class FinishController : MonoBehaviour
{
[SerializeField] private Button _enterBtn; // 확인 버튼
[SerializeField] private GameObject _sliceObj; // 슬라이드 UI
[SerializeField] private GameObject _finishObj; // reward UI
/// <summary>
/// AddListener Setting
/// </summary>
void Awake()
{
_enterBtn.onClick.AddListener(OnClickEnterButton);
}
/// <summary>
/// 게임 끝났을 때
/// </summary>
public void GameFinish()
{
_finishObj.SetActive(true);
}
/// <summary>
/// 확인 버튼 눌렀을 때
/// </summary>
public void OnClickEnterButton()
{
_sliceObj.SetActive(true);
}
}
FinishController 스크립트를 만들어 게임이 끝났을 때 실행될 때 함수들을 관리해 주었습니다.
GameFinish() 함수는 StageClear 표시가 된 UI 오브젝트를 활성화해줍니다.
마지막으로 Stage clear는 애니메이션을 적용해 주었는데요
마지막 2초 구간에 Event를 추가해 주었으며
FinishGetCoin 스크립트를 만들어서 GetCoinRewardText()를 연결시켜 주었습니다.
using System.Collections;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// 코인 가져오기
/// </summary>
public class FinishGetCoin : MonoBehaviour
{
[SerializeField] private TextMeshProUGUI coinText; // 코인 표시할 UI Text
[SerializeField] private float duration = 1.0f; // 애니메이션 시간
public void GetCoinRewardText()
{
float coin = GameManager.Instance._possessionManager._inGameCoin;
StartCoroutine(GetCoinCorutine(coin));
}
/// <summary>
/// 0 ~ targetCoin까지 duration시간 내 순차적 상승 연출
/// </summary>
/// <param name="targetCoin"></param>
/// <returns></returns>
IEnumerator GetCoinCorutine(float targetCoin)
{
float current = 0f;
float elapsed = 0f;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
// 0 ~ targetCoin까지 자연스럽게 증가
current = Mathf.Lerp(0, targetCoin, elapsed / duration);
coinText.text = Mathf.FloorToInt(current).ToString();
yield return null;
}
// 마지막 보정
coinText.text = targetCoin.ToString();
}
}
기존에 코인이 획득되면 possessionManager._inGameCoin도 함께 추가가 됐었는데요
여기서 해당 값을 받아와 사용을 해줬으며,
코루틴과 Mathf.Lerp()를 활용해
0부터 현재 코인까지 올라가는 연출을 해줬습니다.
일단 현재 작업 계획중인것은
피버타임, 로비 두가지가 있습니다.
이 2가지를 빠르게 완료하고 ProjcetB는 마무리를 할 계획입니다.
감사합니다.