[멋쟁이사자처럼 부트캠프 TIL 회고]
오늘의 목표
01. 타이머 만들기
02. 하트 만들기
01. 타이머 만들기
내가 한 방식 : 게임매니저에 우겨넣기
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GamePanelController : MonoBehaviour
{
public GameObject _firstQuizCardObject;
private GameObject _secondQuizCardObject;
private List<QuizData> _quizDataList;
private int _lastGeneratedQuizIndex;
private int _lastStageIndex;
private GoyaTimer _currentTimer;
private GoyaTimer _nextTimer;
private void Start()
{
_lastStageIndex = UserInformations.LastStageIndex;
InitQuizCards(_lastStageIndex);
}
private void InitQuizCards(int stageIndex)
{
_quizDataList = QuizDataController.LoadQuizData(stageIndex);
_firstQuizCardObject = ObjectPool.Instance.GetObject();
_firstQuizCardObject.GetComponent<QuizCardController>()
.SetQuiz(_quizDataList[0], 0, OnCompletedQuiz);
_secondQuizCardObject = ObjectPool.Instance.GetObject();
_secondQuizCardObject.GetComponent<QuizCardController>()
.SetQuiz(_quizDataList[1], 1, OnCompletedQuiz);
SetQuizCardPosition(_firstQuizCardObject, 0);
SetQuizCardPosition(_secondQuizCardObject, 1);
_currentTimer = _firstQuizCardObject.GetComponentInChildren<GoyaTimer>();
_currentTimer.onTimerEnd += ()=>GameOver();
_currentTimer.StartTimer();
_nextTimer = _secondQuizCardObject.GetComponentInChildren<GoyaTimer>();
_nextTimer.ResetTimer();
// 마지막으로 생성된 퀴즈 인덱스
_lastGeneratedQuizIndex = 1;
}
private void OnCompletedQuiz(int cardIndex)
{
if (cardIndex >= Constants.MAX_QUIZ_COUNT - 1)
{
if (_lastStageIndex >= Constants.MAX_STAGE_COUNT - 1)
{
// TODO: 올 클리어 연출
GameManager.Instance.QuitGame();
_currentTimer.PauseTimer();
}
else
{
// TODO: 스테이지 클리어 연출
_lastStageIndex += 1;
InitQuizCards(_lastStageIndex);
return;
}
}
ChangeQuizCard();
}
private void SetQuizCardPosition(GameObject quizCardObject, int index)
{
var quizCardTransform = quizCardObject.GetComponent<RectTransform>();
if (index == 0)
{
quizCardTransform.anchoredPosition = new Vector2(0, 0);
quizCardTransform.localScale = Vector3.one;
quizCardTransform.SetAsLastSibling();
}
else if (index == 1)
{
quizCardTransform.anchoredPosition = new Vector2(0, 160);
quizCardTransform.localScale = Vector3.one * 0.9f;
quizCardTransform.SetAsFirstSibling();
}
}
private void ChangeQuizCard()
{
if (_lastGeneratedQuizIndex >= Constants.MAX_QUIZ_COUNT) return;
_currentTimer.PauseTimer();
_currentTimer.ResetTimer();
_currentTimer.onTimerEnd -= ()=>GameOver();
var temp = _firstQuizCardObject;
_firstQuizCardObject = _secondQuizCardObject;
_secondQuizCardObject = ObjectPool.Instance.GetObject();
if (_lastGeneratedQuizIndex < _quizDataList.Count - 1)
{
_lastGeneratedQuizIndex++;
_secondQuizCardObject.GetComponent<QuizCardController>()
.SetQuiz(_quizDataList[_lastGeneratedQuizIndex], _lastGeneratedQuizIndex, OnCompletedQuiz);
}
SetQuizCardPosition(_firstQuizCardObject, 0);
_currentTimer = _firstQuizCardObject.GetComponentInChildren<GoyaTimer>();
_currentTimer.ResetTimer();
_currentTimer.StartTimer();
_currentTimer.onTimerEnd += ()=>GameOver();
_nextTimer = _secondQuizCardObject.GetComponentInChildren<GoyaTimer>();
if (_nextTimer != null)
{
_nextTimer.ResetTimer();
}
SetQuizCardPosition(_secondQuizCardObject, 1);
ObjectPool.Instance.ReturnObject(temp);
}
public void GameOver()
{
Debug.Log("Game Over");
_currentTimer.PauseTimer();
_currentTimer.onTimerEnd -= ()=>GameOver();
}
}
게임 매니저야 '해줘' 라고 말하는 듯한...
중간에 어려웠던 문제가 있었다.
InitQuizCard하면서 두 번째 카드가 항상 있을줄 알았더니,
if (_lastGeneratedQuizIndex < _quizDataList.Count - 1)
{
_lastGeneratedQuizIndex++;
_secondQuizCardObject.GetComponent<QuizCardController>()
.SetQuiz(_quizDataList[_lastGeneratedQuizIndex], _lastGeneratedQuizIndex, OnCompletedQuiz);
}
요 부분을 보면 마지막에서 한 번 뒤로 갔을 때, 그러니까 각 스테이지의 마지막 문제에서는 다음 퀴즈카드 오브젝트가
설정되지 않는다. 따라서 그냥 다음 타이머를 설정하려고 하면 오류가 발생한다.
_nextTimer = _secondQuizCardObject.GetComponentInChildren<GoyaTimer>();
if (_nextTimer != null)
{
_nextTimer.ResetTimer();
}
이렇게 예외 처리를 해 주었다.
여전히 문제가 있다.
스테이지 2로 갔을 때, 뒤쪽으로는 타이머가 나타나지 않는다.
스테이지 1도 타이머가 나타나지 않던가, 스테이지 2도 뒤쪽에 타이머가 나타나던가 둘 중 하나로 통일을 해야 한다.
문제가 발생하는 이유는 오브젝트 풀에서 계속해서 새로운 카드들이 생성되기 떄문.
이는 스테이지 넘어갈 때에 ReturnCard를 해주지 않았기 때문이다.
else
{
// TODO: 스테이지 클리어 연출
_lastStageIndex += 1;
ObjectPool.Instance.ReturnObject(_firstQuizCardObject);
ObjectPool.Instance.ReturnObject(_secondQuizCardObject);
InitQuizCards(_lastStageIndex);
return;
}
요렇게 스테이지 넘어갈 때 카드를 오브젝트 풀에 다시 넣어주면 된다.
강사님이 하신 방식, 확장성이 높게 QuizCard에 넣어 놓은 방식
우선 타이머 자체에 타이머 시간이 다 된다면 실행되는 Delegate를 만들어 둔다.
public delegate void GoyaTimerDelegate();
public GoyaTimerDelegate OnTimeout;
if (!_isPaused)
{
CurrentTime += Time.deltaTime;
if (CurrentTime >= totalTime)
{
headCapImage.gameObject.SetActive(false);
tailCapImage.gameObject.SetActive(false);
_isPaused = true;
OnTimeout?.Invoke();
}
그리고 카드 프리팹에 각각의 타이머를 등록해주고
[SerializeField] private Timer timer;
각각의 TimeOut함수에 패널을 끄고 키는 함수를 등록한다.
private void Start()
{
timer.OnTimeout = () =>
{
// TODO: 오답 연출
SetQuizCardPanelActive(QuizCardPanelType.InCorrectBackPanel);
};
}
이것까지는 원래 있었던, 오답 패널을 껐다 키는는 부분이기 때문에 타이머가 돌아가는 것과는 관련이 없다.
타이머를 멈추고, 다시 리셋하는 기능을 추가해 주어야 한다.
public void SetVisible(bool isVisible)
{
if (isVisible)
{
timer.InitTimer();
timer.StartTimer();
}
else
{
timer.InitTimer();
}
}
이렇게 타이머를 초기화하고, 시작할 수 있는 함수를 만들어준다.
private void SetQuizCardPosition(GameObject quizCardObject, int index)
{
var quizCardTransform = quizCardObject.GetComponent<RectTransform>();
if (index == 0)
{
quizCardTransform.anchoredPosition = new Vector2(0, 0);
quizCardTransform.localScale = Vector3.one;
quizCardTransform.SetAsLastSibling();
quizCardObject.GetComponent<QuizCardController>()
.SetVisible(true);
}
else if (index == 1)
{
quizCardTransform.anchoredPosition = new Vector2(0, 160);
quizCardTransform.localScale = Vector3.one * 0.9f;
quizCardTransform.SetAsFirstSibling();
quizCardObject.GetComponent<QuizCardController>()
.SetVisible(false);
}
}
다른 함수에서 불러주면 된다. ( 이 함수는 카드가 넘겨질 때마다 불림)
첫 번째 카드는 SetVisible(true), 두 번째 카드는 SetVisible(false)로 해 주면 된다.
02. 하트 만들기
ContentSizeFilter - 역할 알아보기
텍스트가 늘어나면 같이 늘어남
하트 컨트롤러를 만들 것이다.
하트가 늘어나는 것이나 줄어들 때 애니메이션 관련한 것.
DoTween을 사용해서 할 것이다.
[SerializeField] private GameObject _heartRemoveImageObject;
[SerializeField] private TMP_Text _heartCountText;
RemoveImageObject는 하트가 커지면서 연해져 사라지는 것처럼 보이게 할 이미지이고
heartCountText는 표시될 하트의 숫자이다.
public void InitHeartCount(int heartCount)
{
_heartCount = heartCount;
_heartCountText.text = _heartCount.ToString();
}
우선, 처음 하트의 개수를 설정할 수 있는 함수이다.
public void RemoveHeart()
{
// 하트 사라지는 연출
_heartRemoveImageObject.SetActive(true);
_heartRemoveImageObject.transform.localScale = Vector3.zero;
_heartRemoveImageObject.GetComponent<Image>().color = Color.white;
_heartRemoveImageObject.transform.DOScale(3f, 1f);
_heartRemoveImageObject.GetComponent<Image>().DOFade(0f, 1f);
// 하트 수 텍스트 떨어지는 연출
DOVirtual.DelayedCall(1.5f, () =>
{
_heartCountText.rectTransform.DOAnchorPosY(-40f, 1f);
_heartCountText.DOFade(0f, 1f).OnComplete(() =>
{
// 하트 개수 감소
_heartCount--;
_heartCountText.text = _heartCount.ToString();
var textLength = _heartCountText.text.Length;
GetComponent<RectTransform>().sizeDelta = new Vector2(100 + textLength * 30f, 100f);
_heartCountText.rectTransform.DOAnchorPosY(40f, 0f);
_heartCountText.rectTransform.DOAnchorPosY(0, 1f);
_heartCountText.DOFade(1f, 1f);
});
});
}
이런 식으로 사라지는 연출은 RemvoeImage를 커지게 하면서 알파값을 조정시켜 해결하고
그 후에 텍스트가 떨어지며 사라지게 한뒤 완료되면 다시 위에서 나타나게 한다. (줄어든 숫자로)
* DoTween에는 Sequence라는 것이 있음
Sequence는 트윈들의 집합이며 실행 순서 제어, 간격 조절, 반복 및 루핑, 그룹화 등 다양한 기능을 사용할 수 있다.
- DOTween.Squence() : 시퀸스 생성
- Append(), Join(), Insert() : 시퀸스에 트윈을 추가
Append() : 시퀸스에 트윈을 순차적으로 추가
Join() : 시퀸스에 트윈을 동시에 추가
Insert(): 특정 시간 후에 트윈이 실행되도록 시퀸스에 추가 - SetLoops(), SetDelay() : 반복 횟수, 지연 시간 등 설정
- AppendCallback : 시퀸스에 콜백 함수를 추가 시퀀스가 해당 지점에 도달했을 때 실행
나중에 이 모든 애니메이션이 끝나고 다음 패널을 넘기든, 무언가 다른 행위를 하든 했으면 좋겠어서 델리게이트를
등록해 주었다.
public void RemoveHeart(Action onComplete)
{
// 효과음 재생
if (UserInformations.IsPlaySFX)
_audioSource.PlayOneShot(_heartRemoveAudioClip);
// 하트 사라지는 연출
_heartRemoveImageObject.SetActive(true);
_heartRemoveImageObject.transform.localScale = Vector3.zero;
_heartRemoveImageObject.GetComponent<Image>().color = Color.white;
_heartRemoveImageObject.transform.DOScale(3f, 1f);
_heartRemoveImageObject.GetComponent<Image>().DOFade(0f, 1f);
// 하트 수 텍스트 떨어지는 연출
DOVirtual.DelayedCall(1f, () => { ChangeTextAnimation(false, onComplete); });
}
public void ChangeTextAnimation(bool isAdd, Action onComplete = null)
{
float duration = 0.2f;
float yPos = 40f;
heartCountText.rectTransform.DOAnchorPosY(-yPos, duration);
heartCountText.DOFade(0, duration).OnComplete(() =>
{
if (isAdd)
{
var currentHeartCount = heartCountText.text;
heartCountText.text = (int.Parse(currentHeartCount) + 1).ToString();
}
else
{
var currentHeartCount = heartCountText.text;
heartCountText.text = (int.Parse(currentHeartCount) - 1).ToString();
}
// Heart Panel의 Width를 글자 수에 따라 변경
var textLength = heartCountText.text.Length;
GetComponent<RectTransform>().sizeDelta = new Vector2(100 + textLength * 30f, 100f);
// 새로운 하트 수 추가 애니메이션
heartCountText.rectTransform.DOAnchorPosY(yPos, 0);
heartCountText.rectTransform.DOAnchorPosY(0, duration);
heartCountText.DOFade(1, duration).OnComplete(() => { onComplete?.Invoke(); });
});
}
기본 값은 null로 잡아주어 없어도 실행되게 했다.
public void OnClickRetryQuizButton()
{
if (GameManager.Instance.heartCount > 0)
{
heartPanel.heartCountText.text = GameManager.Instance.heartCount.ToString();
heartPanel.RemoveHeart(() =>
{
GameManager.Instance.heartCount--;
Debug.Log($"현재 GameManager의 heartCount 수{GameManager.Instance.heartCount}");
SetQuizCardPanelActive(QuizCardPanelType.Front);
// 타이머 초기화 및 시작
timer.InitTimer();
timer.StartTimer();
});
}
else
{
// 하트가 부족해서 다시도전w 불가
heartPanel.EmptyHeart();
// TODO: 하트 부족 알림
}
}
실행은 다시하기 버튼으로 실행한다.
하트의 개수 관련해서는, 지금 총 3가지의 저장 혹은 표현하는 방식이 있다.
1. UserInformation의 HeartCount
2. GameManager의 heartCount
3. Text로 표현될 heartCountText
이 세 가지를 헷갈려서 좀 고생했다.
우선 중심이 될 것은 GameManager의 heartCount이다.
계속해서 UserInformation을 고치는 것은 비효율적이니 GameManager인 것을 계속해서 변경하다
종료 시에만 바꿔줄 것이다.
위에서 리트라이 버튼을 누르면 하트가 하나 주는 것까지는 봤다.
private void OnApplicationQuit()
{
Debug.Log("OnApplicationQuit!!");
UserInformations.HeartCount = heartCount;
}
OnApplicationQuit() 에서 현재 GameManager의 heartCoiunt를 저장해준다.
그리고
public void OnClickRetryQuizButton()
{
if (GameManager.Instance.heartCount > 0)
{
heartPanel.heartCountText.text = GameManager.Instance.heartCount.ToString();
heartPanel.RemoveHeart(() =>
{
GameManager.Instance.heartCount--;
Debug.Log($"현재 GameManager의 heartCount 수{GameManager.Instance.heartCount}");
SetQuizCardPanelActive(QuizCardPanelType.Front);
// 타이머 초기화 및 시작
timer.InitTimer();
timer.StartTimer();
});
누를 때마다 text를 업데이트 해 주고 줄여준다. ( 어쩌피 줄어든 이후 바로 사라질 패널)