목차
멋쟁이사자처럼 로켓단 인턴쉽
25.08.19
회의록
>> 안건
- 업무 업데이트
- 질문 혹은 피드백 요청
- 주요 안건
- 개발지속
- 해상도 변경이슈
>> 회의 내용
- 해상도 9:16 비율로 변경
- 현재 인게임 외적요소 완성 목표 - 수요일
게임 결과창 이펙트 완성
: 어제는 버그 때문에 테스트를 못해봤는데, 오늘 버그가 해결되어 테스트해보니 DOTween으로 만든 효과가 원하는 효과가 아니었다.
>> 테스트를 위한 설정
: ResultUIController에 'NewRecordText'를 만들어서 바인딩하였다.
--> 'Panel', 'ResultPanel', 'Score' 모두 Layout Group Component를 가지고 있기 때문에 UI의 원하는 위치에 편하게 넣을 수 있도록 Layout Group의 영향을 받지 않는 ResultUI의 자식으로 생성
>> 현재 적용되어 있는 효과
: 기본적으로 게임 결과가 순차적으로 나오고 그 결과값도 0에서부터 점수가 올라서 결과값이 나오는 것은 잘 적용되었다. 하지만 아래 이미지처럼 Day의 단위는 나오지 않았고, New Record일 때 "New Record" Text가 오른쪽에 치우쳐서 나오고, 글자가 나온 뒤에 다시 사라지고 더 이상 나타나지 않았다.
- 원하는 효과
- Day 수를 표시할 때는 뒤에 단위('일') 도 같이 표시되도록
- New Record일 때, Text가 해상도와 상관없이 점수의 위쪽 중앙에 나오도록
- New Record Text의 효과가 서서히 등장한 다음 계속 깜빡거리도록
>> 효과 적용하기
1. Day 수 뒤에 단위도 표시되도록
: DOTween 효과로 Day 수를 표시할 때 text 부분에 "일"을 추가
seq.Append(DOTween.To(() => 0, x => dayText.text = x.ToString() + "일", resultData.Day, 1f));
2. New Record일 때, Text가 해상도와 상관없이 점수의 위쪽 중앙에 나오도록
: 우선 'NewRecordText'가 해상도와 상관없이 점수 부분과 묶이려면 'Score'의 자식으로 있어야 하는데, 'Score'에 있는 'Horizontal Layout Group' Component 때문에 배치가 자유롭지 않다.
--> 'NewRecordText'를 Score의 자식으로 두되, Score에 있는 'Horizontal Layout Group'을 제거하고 새로 'Content'라는 빈 게임 오브젝트를 만들어서 Content에 'Horizontal Layout Group'을 추가한 다음 'Value Text'와 'Head Text'를 Content의 자식으로 넣었다.
3. New Record Text가 서서히 등장한 다음 계속 깜빡거리도록
: DOTween의 Sequence()를 활용하여 효과들의 순서를 정렬하고 DOFade와 DOScale을 활용하여 등장 및 깜빡임을 추가하였으며, Ease를 활용하여 좀 더 자연스러운 모션을 구현함.
--> 모든 효과를 Sequence()에 넣으면 Sequence.SetLoops()를 통해 반복할 때 Scale 애니메이션까지 전부 반복하기 때문에 팝업 느낌을 주는 Scale 애니메이션까지만 Sequence()에 넣고 반짝이는 애니메이션을 만들어주는 DOFade에만 SetLoops()를 사용하여 반짝이는 애니메이션을 무한 반복 재생
※ DOTween의 Ease 기능을 잘 설명한 블로그
[Asset] Unity3D 'DOTween' 13 : Ease에 대한 모든 것 / In, Out과 Flash / AnimationCurve를 활용한 Custom Ease
Ease는 트윈을 사용할 때 모션의 움직임을 결정해주는 아주 중요한 기능으로, 시간의 흐름에 따라 변화되...
blog.naver.com
▶ 테스트를 해보니 글자가 오른쪽 위에서 서서히 등장한 다음 깜빡거리기에 글자가 중앙을 기준으로 서서히 등장하도록 Pivot을 수정
: DOScale()은 Pivot을 기준으로 확대/축소가 일어나기 때문
- Anchor : 화면 해상도에 따라 UI의 위치를 어떻게 고정/비율 유지할지 결정
- Pivot : 오브젝트 자체의 스케일/회전 기준점
>> 최종적으로 효과가 적용된 모습
└ 최종 코드
- ResultUIController.cs
<hide/>
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using DG.Tweening;
public class ResultUIController : PopupController
{
[SerializeField] private RectTransform _rectTransform;
[SerializeField] private CanvasGroup _canvasGroup;
[SerializeField] private Button _quitButton;
[SerializeField] private TMP_Text dayText;
[SerializeField] private TMP_Text maxComboText;
[SerializeField] private TMP_Text scoreText;
[Header("New Record UI")]
[SerializeField] private TMP_Text newRecordText;
void Awake()
{
_quitButton.onClick.AddListener(OnClickQuitButton);
if (newRecordText != null)
newRecordText.gameObject.SetActive(false);
}
public void ShowPopup()
{
base.ShowPopup(gameObject);
}
public void ClosePopup()
{
base.ClosePopup(gameObject);
}
public void InitResultItem(GameResultData resultData)
{
// 처음에는 0으로 초기화
dayText.text = "0";
maxComboText.text = "0";
scoreText.text = "0";
// TODO: 유저의 최고기록 불러오기 (임시: PlayerPrefs)
int bestScore = PlayerPrefs.GetInt("BestScore", 0);
Sequence seq = DOTween.Sequence();
// Day Count Up
seq.Append(DOTween.To(() => 0, x => dayText.text = x.ToString() + "일", resultData.Day, 1f));
seq.AppendInterval(0.2f);
// MaxCombo Count Up
seq.Append(DOTween.To(() => 0, x => maxComboText.text = x.ToString(), resultData.MaxCombo, 1f));
seq.AppendInterval(0.2f);
// Score Count Up
seq.Append(DOTween.To(() => 0, x => scoreText.text = x.ToString(), resultData.Score, 1.5f)
.OnComplete(() =>
{
// New Record 체크
if (resultData.Score > bestScore)
{
PlayerPrefs.SetInt("BestScore", resultData.Score);
ShowNewRecordEffect();
}
}));
}
// New Record 시, 효과
public void ShowNewRecordEffect()
{
if (newRecordText != null)
{
newRecordText.gameObject.SetActive(true);
newRecordText.alpha = 0f;
newRecordText.transform.localScale = Vector3.zero * 0.8f;
Sequence seq = DOTween.Sequence();
// Fade In
seq.Append(newRecordText.DOFade(1f, 0.5f));
// 글자가 살짝 커졌다가 원래 크기로
seq.Join(newRecordText.transform.DOScale(1.2f, 0.3f).SetEase(Ease.OutBack));
seq.Append(newRecordText.transform.DOScale(1f, 0.2f).SetEase(Ease.InOutSine));
// 번쩍거리는 효과 무한 반복
seq.OnComplete(() =>
{
newRecordText.DOFade(0.3f, 0.2f)
.SetLoops(-1, LoopType.Yoyo)
.SetEase(Ease.InOutSine);
});
}
}
public void OnClickQuitButton()
{
ClosePopup();
GameManager.Instance.ResumeGame();
GameManager.Instance.inGameController.QuitGame();
}
}
Scene이 하나로 통합되면서 생긴 BGM 버그 해결
: 다른 팀원께서 기존에 Raise, Title, InGame로 나뉘어 있던 Scene을 하나로 통합하셨다. 기존에 BGM을 재생하는 방식이 Scene의 이름을 검사하여 그에 맞게 BGM을 틀었던 구조였기 때문에 Scene이 통합되면서 BGM이 재생되지 않는 문제가 생겼다.
--> BGM의 재생 방식을 Scene의 이름을 검사하는 것이 아니라 enum으로 존재하는 'GameState'에 따라 재생되도록 수정
- BGMController.cs에서 GameState를 매개변수로 받아서 Switch문으로 GameState에 따라 그에 해당하는 BGM이 실행되는 함수를 추가
- GameManager.cs에서 GameState를 어디서든 받아올 수 있는 함수를 추가
- BGM이 필요한 GameState가 Title과 InGame 이므로 각 State의 OnEnter()에서 BGM을 재생하도록 추가
- InGameController.cs에서 관리하는 게임시작/게임종료 Coroutine에서도 BGM을 재생 및 정지하도록 추가 --> 게임 중 일시정지 한 다음 Retry하거나 Quit 했을 때도 BGM이 정상작동 하도록 하기 위함
└ 최종 코드
1. BGMController.cs
<hide/>
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using static Constants;
public class BGMController : Singleton<BGMController>
{
// BGM을 추가하실 때, 여기에 추가해주세요.
[SerializeField] private AudioClip titleBGM;
[SerializeField] private AudioClip gameBGM;
// 여기까지
private AudioSource _bgmSource;
private AudioClip _currentBGM; // 현재 재생해야 할 BGM 저장
private bool _isBGMOn = true; // BGM이 켜져있는지 여부
public bool IsBGMOn() => _isBGMOn;
protected override void Initialize()
{
SceneManager.sceneLoaded += OnSceneLoaded;
_bgmSource = gameObject.AddComponent<AudioSource>();
}
private void OnSceneLoaded(Scene scene, LoadSceneMode loadSceneMode)
{
AudioManager.Instance.SetBGMController(this);
// TODO: 유저 정보에 소리 설정이 OFF라면 재생되지 않도록
}
// BGM를 추가하신 뒤, 아래 함수 모음에 재생 함수를 작성해주세요. 그리고 작성하신 함수를 통해 사용하시면 됩니다.
#region PlayBGM 함수 모음
public void PlayTitleBGM() => PlayBGM(titleBGM);
public void PlayGameBGM() => PlayBGM(gameBGM);
#endregion
// Scene에 따라 BGM 재생
public void PlayBGMByState(GameState currentState)
{
switch (currentState)
{
case GameState.Title:
PlayTitleBGM();
break;
case GameState.InGame:
PlayGameBGM();
break;
default:
StopBGM();
break;
}
}
// BGM 재생 (반복 O)
private void PlayBGM(AudioClip clip)
{
_currentBGM = clip; // 현재 BGM 저장
if (!_isBGMOn || clip == null) return;
_bgmSource.clip = clip;
_bgmSource.loop = true;
_bgmSource.Play();
}
// BGM 중지
public void StopBGM()
{
if (_bgmSource != null)
{
_bgmSource.Stop();
}
}
// _isBGMOn값을 조정하고 그에 따라 BGM 재생 및 중지
public void SetBGMOn(bool isBGMOn)
{
_isBGMOn = isBGMOn;
if (_isBGMOn && _currentBGM != null) // 재생
{
PlayBGM(_currentBGM);
}
else // 중지
{
StopBGM();
}
}
}
2. GameManager.cs
<hide/>
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using static Constants;
public class GameManager : Singleton<GameManager>
{
public InGameController inGameController;
/// <summary>
/// 게임은 상태패턴으로 관리됩니다. Title, InGame, Pause 세가지로 관리됩니다.
/// IGameState 인터페이스를 기반으로 만들어졌으며, 각 상태에 진입(OnEnter)할 때 씬을 로드합니다.
/// GameState는 ChangeGameState를 통해 변경합니다.
/// </summary>
private GameState _previousState;
private GameState _currentState;
//public GameState CurrentGameState => _currentState;
private Dictionary<GameState, IGameState> _states = new Dictionary<GameState, IGameState>();
public Action<GameState> GameStateChanged;
//일시정지 관리.
private bool _isPaused;
protected override void Initialize()
{
//Initialize
_states[GameState.Title] = new TitleState();
_states[GameState.InGame] = new InGameState();
_states[GameState.Pause] = new PauseState();
inGameController = new InGameController();
_isPaused = false;
SceneManager.sceneLoaded += OnSceneLoaded;
}
private void OnSceneLoaded(Scene scene, LoadSceneMode loadSceneMode)
{
SceneManager.sceneLoaded -= OnSceneLoaded;
StartCoroutine(LoadGameSystem());
}
//최초 실행시 초기화 진행
private IEnumerator LoadGameSystem()
{
yield return StartCoroutine(inGameController.Initialize());
}
private void Update()
{
if (_currentState != GameState.None)
{
_states[_currentState].OnUpdate();
}
// //테스트용 입력
// if (Input.GetKeyDown(KeyCode.Alpha1))
// {
// GoToInGame();
// }
// else if (Input.GetKeyDown(KeyCode.Alpha2))
// {
// //게임오버 시키기
// inGameController.QuitGame();
// //ReturnToTitle();
// }
// else if (Input.GetKeyDown(KeyCode.Alpha3))
// {
// PauseGame();
// }
// else if (Input.GetKeyDown(KeyCode.Alpha4))
// {
// ResumeGame();
// }
}
///게임을 시작합니다.
public void GoToInGame()
{
ChangeGameState(GameState.InGame);
}
///타이틀로 돌아갑니다.
public void ReturnToTitle()
{
ChangeGameState(GameState.Title);
}
///게임을 일시정지 합니다.
public void PauseGame()
{
if (_isPaused)
{
Debug.LogWarning("Game is already paused");
return;
}
ChangeGameState(GameState.Pause);
_isPaused = true;
}
///게임을 재개 합니다.
public void ResumeGame()
{
if (!_isPaused)
{
Debug.LogWarning("Game is not paused");
return;
}
ChangeGameState(GameState.Pause, true);
_isPaused = false;
}
///게임의 상태를 변경합니다.
public void ChangeGameState(GameState newGameState, bool resume = false)
{
//기존 State 종료
if (_currentState != GameState.None)
{
_states[_currentState].OnExit();
}
//일시정지 해제 시
if (_currentState == GameState.Pause && newGameState == GameState.Pause && resume)
{
_currentState = _previousState;
}
else//새 State로 전환
{
_previousState = _currentState;
_currentState = newGameState;
_states[_currentState].OnEnter();
}
//State전환 후 실행할 Action이 있으면 실행
GameStateChanged?.Invoke(_currentState);
}
///TimeController가 필요할땐 이 함수를 쓰시면 됩니다.
public TimeController GetTimeController()
{
return inGameController.timeController != null ? inGameController.timeController : null;
}
///DocumentController가 필요할땐 이 함수를 쓰시면 됩니다.
public DocumentController GetDocumentController()
{
return inGameController.docController != null ? inGameController.docController : null;
}
///Classification이 필요할땐 이 함수를 쓰시면 됩니다.
public Classification GetClassification()
{
return inGameController.classification != null ? inGameController.classification : null;
}
public GameState GetGameState()
{
return _currentState;
}
///일시정지(백그라운드 상태) 되었을 때
private void OnApplicationPause(bool pauseStatus)
{
//Debug.Log("OnApplicationPause: " + pauseStatus);
}
///게임이 종료되었을 때
private void OnApplicationQuit()
{
//Debug.Log("OnApplicationQuit");
}
public new void OnDestroy()
{
base.OnDestroy();
}
}
3. TitleState.cs
<hide/>
using Unity.VisualScripting;
using UnityEngine;
public class TitleState : IGameState
{
public void OnEnter()
{
//SceneController.TransitionToScene(SceneState.Title);
UIManager.Instance.titleUIController.ShowTitleUI();
UIManager.Instance.titleUIController.ShowMainMenuUI();
UIManager.Instance.titleUIController.ShowSubMenuUI();
// BGM 재생
AudioManager.Instance.BGM.PlayBGMByState(GameManager.Instance.GetGameState());
}
public void OnUpdate()
{
}
public void OnExit()
{
UIManager.Instance.titleUIController.HideTitleUI();
UIManager.Instance.titleUIController.HideMainMenuUI();
UIManager.Instance.titleUIController.HideSubMenuUI();
}
}
4. InGameState.cs
<hide/>
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
public class InGameState : IGameState
{
public void OnEnter()
{
GameManager.Instance.StartCoroutine(StartGame());
UIManager.Instance.inGameUIController.ShowInGameUI();
// BGM 재생
AudioManager.Instance.BGM.PlayBGMByState(GameManager.Instance.GetGameState());
}
public void OnUpdate()
{
}
public void OnExit()
{
}
IEnumerator StartGame()
{
yield return GameManager.Instance.inGameController.SetInitGame();
yield return GameManager.Instance.inGameController.RunSequence();
}
}
5. InGameController.cs
<hide/>
using System.Collections;
using UnityEngine;
using Object = UnityEngine.Object;
//인게임 주요 로직들을 제어합니다.
public class InGameController
{
public TimeController timeController;
public DocumentController docController;
public Classification classification;
public bool Initialized;
private bool _initComplete;
private bool _gameStarted;
private bool _gameFinished;
private bool _quitGame;
private bool _skipResultUI;
private bool _useRetry;
public IEnumerator Initialize()
{
//TODO: 게임 실행시 초기화 할 로직
//classification = new Classification();
//씬 내 타이머, 문서생성 오브젝트 찾기
if (timeController == null)
{
yield return new WaitUntil(() => timeController = Object.FindObjectOfType<TimeController>());
}
if (docController == null)
{
yield return new WaitUntil(() => docController = Object.FindObjectOfType<DocumentController>());
}
if(classification == null)
{
yield return new WaitUntil(() => classification = Object.FindObjectOfType<Classification>());
classification.Initialize();
}
Initialized = true;
}
//게임이 시작되면 이 시퀀스를 통해 진행됩니다.
public IEnumerator RunSequence()
{
//실행 필수 초기화 진행 체크
yield return new WaitUntil(() => Initialized);
//게임 시퀀스 실행
yield return new WaitUntil(() => _initComplete);
yield return StartGame();
yield return new WaitUntil(() => _gameFinished);
yield return EndGame();
}
//게임 시작 전 초기화
public IEnumerator SetInitGame()
{
//실행 필수 초기화 완료전 까지 대기
yield return new WaitUntil(() => Initialized);
//TODO: 게임 시작시 초기화 할 로직
_initComplete = false;
_gameStarted = false;
_gameFinished = false;
_quitGame = false;
_skipResultUI = false;
_useRetry = false;
//타이머 초기화
timeController.InitTimeController();
//서류 풀 초기화
docController.ReloadDocument(true);
//classification.InitScore();
GameManager.Instance.GetClassification().InitScore();
_initComplete = true;
}
//게임 시작
public IEnumerator StartGame()
{
_gameStarted = true;
//인게임UI 보이기
UIManager.Instance.inGameUIController.ShowTimeUI();
UIManager.Instance.inGameUIController.ShowInteractionUI();
UIManager.Instance.inGameUIController.ShowScoreUI();
UIManager.Instance.inGameUIController.ShowFeverUI();
UIManager.Instance.inGameUIController.ShowBackgroundUI();
UIManager.Instance.inGameUIController.ShowClockUI();
UIManager.Instance.inGameUIController.ShowClassificationUI();
// BGM 재생
AudioManager.Instance.BGM.PlayBGMByState(GameManager.Instance.GetGameState());
//타이머, 문서 생성 시작.
timeController.StartRunningTimer();
docController.InitDocuments();
while (!_gameFinished)
{
yield return null;
}
}
//게임 종료. 결과 보고
public IEnumerator EndGame()
{
//TODO: 게임 끝낼 시 실행할 로직
//시간 정지
timeController.StopTime();
//ex.게임오버 연출, 결과창UI등
var popupController = UIManager.Instance.popupUIController;
if (!_useRetry)//재시작 활성화 시 엔드연출 스킵
{
//게임오버
var gameOverUI = popupController.gameOverUIController;
yield return gameOverUI.ShowSequence(); //게임오버 연출동안 딜레이
}
if (!_skipResultUI)//필요 시 스킵
{
//결과창
var resultUI = popupController.resultUIController;
popupController.ShowResultUI();
resultUI.InitResultItem(new GameResultData(
timeController._day,
GameManager.Instance.GetClassification().GetMaxCombo(),
GameManager.Instance.GetClassification().GetScore()));
}
//게임이 끝난 후, 바로 돌아가지 않고 대기.
while (!_quitGame)
{
yield return null;
}
//초기화.
_initComplete = false;
_skipResultUI = false;
//재시작 필요 시.
if (_useRetry)
{
//새 게임 코루틴 활성화
yield return GameManager.Instance.StartCoroutine(SetInitGame());
GameManager.Instance.StartCoroutine(RunSequence());
//재시작을 위해 타이틀로 복귀하지 않고 기존 코루틴을 중단한다.
yield break;
}
//인게임 UI 닫기
UIManager.Instance.inGameUIController.HideInGameUI();
UIManager.Instance.inGameUIController.HideTimeUI();
UIManager.Instance.inGameUIController.HideInteractionUI();
UIManager.Instance.inGameUIController.HideScoreUI();
UIManager.Instance.inGameUIController.HideFeverUI();
UIManager.Instance.inGameUIController.HideBackgroundUI();
UIManager.Instance.inGameUIController.HideClockUI();
//타이틀 씬으로 복귀
GameManager.Instance.ReturnToTitle();
}
///게임 끝내기, 호출 시 진행중인 게임이 끝납니다.
public void Dispose()
{
//게임이 시작했을때만
if(_gameStarted) _gameFinished = true;
}
///게임 끝난 후, 호출 시 타이틀로 돌아갑니다.
public void QuitGame()
{
if(_gameFinished) _quitGame = true;
}
///결과 UI 스킵필요 시 게임 종료 전 호출
public void SkipResultUI()
{
_skipResultUI = true;
}
//재시작 필요 시 먼저 호출.
public void UseRetry()
{
_useRetry = true;
}
}
'Development > Internship' 카테고리의 다른 글
[멋사 로켓단 인턴쉽] 11일차 - 점수판 완성 및 버그 해결 (3) | 2025.08.21 |
---|---|
[멋사 로켓단 인턴쉽] 10일차 - Result UI & Effect (0) | 2025.08.20 |
[멋사 로켓단 인턴쉽] 8일차 - Stamp Effect 및 Result Effect (6) | 2025.08.18 |
[멋사 로켓단 인턴쉽] 추가 작업 - Stamp Effect (2) | 2025.08.18 |
[멋사 로켓단 인턴쉽] 7일차 - Audio Manager 완성 (0) | 2025.08.14 |