본문 바로가기
Development/Internship

[멋사 로켓단 인턴쉽] 11일차 - 점수판 완성 및 버그 해결

by Mobics 2025. 8. 21.

목차


    멋쟁이사자처럼 로켓단 인턴쉽

    25.08.21

    회의록

    >> 안건

    • 업무 업데이트
      • 질문 혹은 피드백 요청
    • 주요 안건
      • 게임 완전완성 지속중
      • 난이도 밸런싱 적용
    • 향후 마일스톤
      • 디테일, 버그 등 지속작업
      • itch.io 업로드

    >> 회의 내용

    • 금요일까지 완성하기.

     

    점수판 완성

    >> 점수판 Image 수정

    : 점수판 Image가 다른 팀원 분이 디자인 해주신대로 되어있지 않아서 완전히 적용

    (좌)기존 상태 / (우)새롭게 적용한 상태

     

    >> New Record시, 점수판 위에 Image 및 Animation 추가

    : New Record시, 점수판에 들어가있던 'New Record' 이미지가 위로 튀어오르도록 제작

    --> 'New Record' 이미지의 초기 위치를 점수판 안에 있도록 위치한 뒤, DOTween의 'DOAnchorPosY()' 를 사용하여 위치를 올려주는 Animation과 함께 'SetEase(Ease.OutBack)'을 사용하여 튀어오르는 느낌이 나도록 구현

     

    - 재시도를 하거나 게임을 계속 이어서 할 경우에 위치를 초기화하는 함수도 같이 제작

    - 점수판의 위치는 Unity에서 위치를 보고 값을 정했기 때문에 readonly로 설정

    - 현재 리더보드를 불러오는 기능이 없기 때문에 임시 코드로 'K'를 입력하면 효과가 나타나도록 제작

     

    ※ Readonly vs Const

     

    C#의 readonly 키워드와 불변성에 대해

    readonly vs const 처음 readonly를 접하면, 자연스럽게 const와 비교하게 된다. 언뜻 보기에는 둘다 변하지 않는 값을 선언하는 문법으로, 큰 차이를 느낄 수 없다. 다른 점을 찾아봐도 주로 const는 컴파

    tearsinrain.tistory.com

     

    - 적용한 모습

     

    └ 최종 코드

    1. ScoreUIController.cs

    <hide/>
    using System.Collections;
    using System.Collections.Generic;
    using TMPro;
    using UnityEngine;
    using UnityEngine.UI;
    using DG.Tweening;
    
    public class ScoreUIController : MonoBehaviour
    {
        public TMP_Text score;
        public TMP_Text scoreMag;
        [SerializeField] private Image newRecordImage;
        private readonly float _hiddenY = 23f;   // Unity에서 위치를 보고 맞춘 값
        private readonly float _shownY = 58f;   // Unity에서 위치를 보고 맞춘 값
    
        // New Record 이미지 위치 초기화
        public void InitNewRecordImage()
        {
            Vector2 newRecordPos = newRecordImage.rectTransform.anchoredPosition;
            newRecordPos.y = _hiddenY;
            newRecordImage.rectTransform.anchoredPosition = newRecordPos;
        }
    
        // New Record Animation
        public void ShowNewRecordImage()
        {
            newRecordImage.rectTransform.DOAnchorPosY(_shownY, 0.3f).SetEase(Ease.OutBack);
        }
    }

     

    2. InGameController.cs

    <hide/>
    //게임 시작 전 초기화
    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;
        
        // NewRecord 이미지 위치 초기화
        InGameUIController.Instance.scoreUIController.InitNewRecordImage();
    }

     

    3. InGameState.cs

    <hide/>
    public void OnUpdate()
    {
        // TODO: 유저의 최고기록 불러오기 (임시: 'K'를 누르면 연출 재생)
        // New Record시, 점수판에 New Record Image 연출 재생
        if (Input.GetKeyDown(KeyCode.K))
        {
            InGameUIController.Instance.scoreUIController.ShowNewRecordImage();
        }
    }

     

    버그 발견 및 수정

    >> 발견한 버그

    • 버튼을 연속으로 눌렀을 때 서류 분류 효과(?)의 잔상이 남아있음
    • 서류를 잘못 분류했을 때, '시간 감소' 글자가 사라진 뒤에야 일과시간이 줄어든다.
    • 서류를 올바르게 분류했을 때, 시간이 증가한다고 나오지만 숫자가 안 오를 때가 있음 --> 위와 마찬가지로 글자가 사라진 뒤에야 일과시간이 늘어나서 그런게 아닐까?
    • 장애물을 제거하지 않고 분류했을 때 일과시간이 더 많이 깎인다. --> Day1에는 6~7초 정도, Day3에는 10~11초 정도 줄어들었다. (원래 장애물을 제거하지 않았을 때 더 많이 깎였는가? + 그렇다해도 단계별이 아니라 Day 수에 비례해서 더 많이 깎이는 버그가 있다.)
    • 게임 1판을 끝내고 다시 게임을 시작했을 때나 일시정지 후 재시작을 했을 때, 시계의 프레임 색이 이전 상태를 유지한 채로 시작된다. 그리고 3초 후 본 게임이 시작될 때 다시 초기화 된다.
    • 게임 1판을 끝내고 다시 게임을 한 다음, 다시 결과창에 진입하면 New Record Image가 붙어있는 상태로 나온다.

     

    >> 버그 수정

    : 여러 버그들을 팀원들끼리 나눠서 수정했고, 나는 결과창의 New Record Image 관련 버그와 시계 프레임 색 관련 버그를 수정했다.

     

    1. New Record Image 버그

    : 기존 코드에서는 ResultUIController.cs가 Awake()할 때 newRecordImage를 SetActive(false)해서 재시작했을 때 Awake()는 호출되지 않으므로 발생한 문제였다.

    --> Awake()가 아니라 InitResultItem()에서 SetActive(false)를 호출하도록 수정

     

    - 수정된 모습

    (좌)수정 전 / (우) 수정 후

     

    - 수정한 코드

    <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;
        
        [SerializeField] private Image newRecordImage;
        [SerializeField] private CanvasGroup fadeOutCanvasGroup;
    
        void Awake()
        {
            _quitButton.onClick.AddListener(OnClickQuitButton);
        }
        
        public void ShowPopup()
        {
            base.ShowPopup(gameObject);
        }
        
        public void ClosePopup()
        {
            base.ClosePopup(gameObject);
        }
        
        public void InitResultItem(GameResultData resultData)
        {
            // FadeOut Panel 초기화
            fadeOutCanvasGroup.alpha = 0;
            
            // 퇴근 버튼 비활성화
            _quitButton.gameObject.SetActive(false);
            
            // New Record 이미지 비활성화
            newRecordImage.gameObject.SetActive(false);
            
            // 처음에는 0으로 초기화
            dayText.text = "0";
            maxComboText.text = "0";
            scoreText.text = "0";
            
            // TODO: 유저의 최고기록 불러오기 (임시: PlayerPrefs)
            float bestScore = PlayerPrefs.GetFloat("BestScore", 0f);
            
            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("N0"), resultData.Score, 1.5f)
                .OnComplete(() =>
                {
                    // 퇴근 버튼 활성화
                    _quitButton.gameObject.SetActive(true);
                    
                    // New Record 체크
                    if (resultData.Score > bestScore)
                    {
                        PlayerPrefs.SetFloat("BestScore", resultData.Score);
                        ShowNewRecordEffect();
                    }
                }));
        }
    
        // New Record 시, 효과
        private void ShowNewRecordEffect()
        {
            newRecordImage.gameObject.SetActive(true);
            
            // 초기화 (작고 안 보이는 상태)
            newRecordImage.color = new Color(1f, 1f, 1f, 0f);
            newRecordImage.rectTransform.localScale = Vector3.zero * 0.8f;
            
            Sequence seq = DOTween.Sequence();
            // Fade In + Scale Up
            seq.Append(newRecordImage.DOFade(1f, 0.5f));
            seq.Join(newRecordImage.rectTransform.DOScale(1.2f, 0.3f).SetEase(Ease.OutBack));
            // 살짝 튕기면서 원래 크기로
            seq.Append(newRecordImage.rectTransform.DOScale(1f, 0.2f).SetEase(Ease.OutBack));
            // 착! 강조
            seq.Append(newRecordImage.rectTransform.DOScale(0.95f, 0.1f).SetEase(Ease.InQuad));
            seq.Append(newRecordImage.rectTransform.DOScale(1f, 0.15f).SetEase(Ease.OutQuad));
        }
    
        public void OnClickQuitButton()
        {
            // Title로 가기 전, FadeOut
            fadeOutCanvasGroup.DOFade(1f, 1f)
                .OnComplete(() =>
                {
                    ClosePopup();
                    GameManager.Instance.ResumeGame();
                    GameManager.Instance.inGameController.QuitGame();
                });
        }
    }

     

    2. 시계 프레임 색 버그

    : 기존 코드에서는 남은 일과시간이 30초 이하일 때, 시계 프레임이 흰색과 빨간색을 번갈아가며 변하는 코드만 있었고, 재시작 할 때 초기화 해주는 코드가 없었다.

    --> 시계 프레임 색상을 초기화 해주는 함수를 작성한 후, 게임이 시작할 때 호출하도록 수정

     

    ▶ 기존 코드에서는 clockHandle과 clockFrame을 Awake()에서 GetChild()로 긁어와서 사용했는데, 그러다보니 InGameController.cs에서 초기화할 때 clockFrame을 찾지 못해 Null에러가 발생했다.

    --> Awake()를 없애고 clockHandle과 clockFrame을 모두 SerializeField로 받아서 작동하도록 수정

     

    - 수정된 모습

    (좌)수정 전 / (우)수정 후

     

    - 수정한 코드

    >> ClockUIController.cs

    <hide/>
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.UI;
    
    public class ClockUIController : MonoBehaviour
    {
        [SerializeField] GameObject clockHandle;
        [SerializeField] Image clockFrame;
    
        [Header("각도 변화량")]
        public float stepAngle = 30f;
    
        [Header("틱 간격")]
        public float tickInterval = 1.0f;
    
        [Header("경보 시간 설정")]
        public int setTime = 30;
    
        float targetZ;
        float timer;
    
        void Update()
        {
            if (!TimeController.Instance._isTimeRunning) return;
    
            ClockHandAnimation();
            ClockFrameColor();
        }
    
        public void ClockHandAnimation() // 시계 바늘 움직이는 기능
        {
            timer += Time.deltaTime;
    
            if (timer >= tickInterval) // 틱 간격이 지났을 때
            {
                // 목표 각도를 현재 각도에서 stepAngle 만큼 증가
                targetZ += -stepAngle; // 시계방향으로 회전하기위해 -를 붙임
                timer = 0f;
            }
    
            // 목표 각도로 보간
            clockHandle.transform.rotation = Quaternion.Lerp(clockHandle.transform.rotation, Quaternion.Euler(0, 0, targetZ), Time.deltaTime * 10f);
        }
    
        public void ClockFrameColor() // 시계 프레임 색상 변경 기능
        {
            float remainedTime = TimeController.Instance._remainedTimerTime;
    
            if (remainedTime <= setTime) // 일과 시간이 30초 이하일 때
            {
                float t = Mathf.PingPong(Time.time * 2f, 1f); // 색상 변경 딜레이
                clockFrame.color = Color.Lerp(Color.white, Color.red, t); // 흰색에서 빨간색으로 보간
    
                /* SFX, VFX 등 추가할거 있으면 여기에 추가 및 수정하시면 됩니다 */
            }
            else
            {
                InitClockFrameColor(); // 기본 색상으로 설정
            }
        }
        
        public void InitClockFrameColor() // 시계 프레임 색상 초기화
        {
            clockFrame.color = Color.white;
        }
    }

     

    >> InGameController.cs

    <hide/>
    //게임 시작 전 초기화
    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;
        
        // NewRecord 이미지 위치 초기화
        InGameUIController.Instance.scoreUIController.InitNewRecordImage();
        
        // 시계 프레임 색상 초기화
        InGameUIController.Instance.clockUIController.InitClockFrameColor();
    }

     

    게임에 쓰일 BGM 및 SFX를 구하기 전 고민

    : 게임에 필요한 BGM 및 SFX가 무엇이 있을지 혼자 고민해보았다.

     

    - 게임에 필요한 BGM 및 SFX 목록 정리

    • BGM
      • Title BGM
      • InGame BGM
    • SFX
      • Click SFX
      • UI Close SFX
      • InGame Count SFX
      • (InGame Start SFX)
      • Stamp SFX
      • (InGame NewRecord SFX)
      • Obstacle SFX - 종류별로
      • Result NewRecord SFX