본문 바로가기
Development/C#

멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 60일차

by Mobics 2025. 2. 21.

 

목차


    퀴즈 게임 만들기

    25.02.21

    Card가 Flip하지 않던 버그 해결

    • 문제 원인 : Animation 상태가 지속되어 Rotation이 바뀔 수 없었던 것 --> 만든 Animation이 Transform에 영향을 주면 다른 Animation에 영향을 줄 수 있다.
    • 해결 방법 : Animator의 'Apply Root Motion'을 체크하면 만든 애니메이션은 동작하지만 실제 Transform은 움직이지 않도록 고정시켜준다.

     

    Popup Panel 만들기

    : 만들었던 'Shop Panel'의 틀이 여러 곳에서 동일하게 사용되기 때문에 이를 여러 곳에서 사용할 수 있도록

    --> 이름을 'Popup Panel'로 수정

     

    >> 빈 게임 오브젝트로 'Panel' 생성

    : 만들어둔 Background, Image 등등 전부 아래로 내려가고 위로 올라오는 Animation을 만들 것이기 때문에 전부 묶어주기

    --> 전체적인 PosY를 Panel로 관리하도록 수정 및 Canvas Group 추가

    --> Anchor는 Alt + Shift

     

    >> Canvas Group으로 관리함에 따라 전체적으로 위치 조정

     

    >> PopupPanelController.cs 생성

    : 미리 만들어뒀던 SettingsPanelController.cs, ShopPanelController.cs, StagePanelController.cs 삭제

     

    >> Popup Panel Controller.cs 추가 및 바인딩

     

    >> Close Button의 OnClick()에 함수 바인딩

     

    >> Popup Panel Prefab화 후, Prefab Variant 생성

    : Hierarchy에서 Popup Panel 제거

    --> Prefab Variant 생성, 이름은 'Setting Popup Panel'

     

    ※ Setting Panel의 우측 상단에 희미하게 틈이 생기는 것 수정

    : Popup Panel의 Source Image를 None으로 설정하면 해결

    --> Popup Panel을 수정해야 다른 Panel들도 한꺼번에 해결된다.

     

    Settings Popup Panel 만들기

    >> MainPanel에 Canvas와 Setting Popup Panel 바인딩

     

    >> Settings Button의 OnClick()에 함수 바인딩

     

    활동

    >> Setting Popup Panel UI 만들기

    : 아래 사진과 같이 만들기

     

    >> Title Text (TMP)의 Color 변경

    : (195, 198, 207, 255)

     

    >> 빈 게임 오브젝트 'Settings' 생성

    : Texts와 Buttons를 가로로 Layout --> Grid Layout Group으로 할 수 있지 않을까?

     

    >> 'Settings'의 자식으로 빈 게임 오브젝트 'Texts'와 'Buttons' 생성

    --> Vertical Layout Group의 Child Alignment는 Upper Left든 Middle Center든 상관없는 듯하다

     

    >> 'Texts'의 자식으로 'SFX Text (TMP)'와 'BGM Text (TMP)' 생성

    : Text로 생성

    --> BGM Text도 Text 내용 제외하고 동일

     

    >> 'Buttons'의 자식으로 'SFX Button'와 'BGM Button' 생성

    : Button으로 생성, 자식의 Text를 삭제하고 자식으로 Image 추가 --> 이름은 각각 'SFX Handle', 'BGM Handle'

    • Source Image 추가하고 Set Native Size
    • Button의 OnClick()에 함수 바인딩

    - BGM Button도 설정은 대부분 동일하고 함수만 다르게 바인딩

     

    - SFX Handle, BGM Handle

     

    >> 'Evaluate Button' 생성

    : Button으로 생성

     

    >> 빈 게임 오브젝트로 'Logo' 생성

     

    >> 'Logo'의 자식으로 'Logo Image', 'Version Text (TMP)', 'Mocapot Image', 'Mocapot Text (TMP)' 생성

    --> Version Text의 Color는 (118, 118, 118, 255)

     

    >> 추후 수정사항

    • 만든 Buttons의 Animation이 제대로 작동하도록 --> Toggle은 잘 움직이는데 Color가 정한 값이 아니라 마음대로 변하는 버그 존재
    • Button의 기능 추가

     

    ※ Play 했을 때, Panel이 끝까지 올라가지 않는 버그 수정

    >> 버그가 발생한 모습

     

    >> 해결 방법

    : Anchor의 MaxY를 Panel이 나타나고자 하는 위치까지 내려주고 Top을 0으로 설정

     

    Shop Popup Panel 만들기

    ※ 상점의 Button들이 같은 Layout이 반복되기 때문에 하나를 Prefab으로 만들어서 전체적으로 사용 가능

    >> Popup Panel의 Prefab Variant를 'Shop Popup Panel'라는 이름으로  생성

     

     

    >> ShopPopupPanelController.cs 생성

    : 'Shop Popup Panel'에 추가

     

    >> Shop Button의 OnClick()에 함수 바인딩

     

    >> MainPanelController.cs에 Shop Popup Panel Prefab 바인딩

     

    >> Scroll View 생성

    - 좌우로 스크롤은 하지 않을 것이기 때문에 Scroll Rect에서 Horizontal 체크 해제

    - 설정창을 닫는 버튼과 Title Text는 스크롤에 영향이 없도록 Top을 300으로 설정

    - Source Image를 None으로 설정

    - Color의 Alpha값을 255로 변경

    --> Anchor는 Alt + Shift

    --> 구현할 때는 Color를 바꿔서 보기 좋게 만들면 편하다

     

    ※ Viewport의 Mask가 화면을 벗어난 부분을 가려준다.

    ※ Content는 Scroll View에서 보여줄 요소들이 배치된 영역

    : Content의 자식으로 있는 것들을 관리한다. --> Content의 Height가 화면보다 짧으면 스크롤이 활성화되지 않는다.

     

    >> Content의 자식으로 'Shop Item Button' 추가

     

    >> 'Shop Item Button'의 Text를 수정

    : 이름을 'Title Text (TMP)'로 수정하고 나머지 설정 --> 이후 복붙하여 'Price Text (TMP)'도 설정

    --> Anchor는 Alt + Shift

     

    --> 다시 Title Text와 Price Text 위치 조정함

     

    >> 만든 'Shop Item Button'을 Prefab화 후, Hierarchy에서 삭제

    : 만든 Prefab을 Content의 자식으로 추가

     

    >> Content에 Vertical Layout Group 추가 후, Control Child Size의 Width 체크

    : 자식 Object의 Width도 같이 조정할 수 있도록 체크

     

    >> Sprite의 형태가 안 늘어나도록 Sprite Editor로 Slice하고 적용하기

    : 'example_button_gray_bg-normal'

     

    ※ Image Resource의 용량을 줄이는 법

    : 실제 Image의 크기대로 만드는 것이 아니라 모양을 유지할 수 있을 정도의 사이즈로 작게 만듦

    --> 어차피 잘라서 Unity에서 늘리면 되기 때문에

     

    >> Content에 'Content Size Filtter' 추가 후, Vertical Fit 설정

    : Content의 Height를 자식 Object의 개수와 크기에 따라 자동으로 맞춰준다. (코드로도 늘릴 수 있다.)

    --> Padding 및 Spacing 조정

     

    >> ScrollBar의 디자인 변경

    - ScriollBar Vertical의 Color는 Scroll Bar 외의 빈 공간의 색

    - Handle의 Color는 Scroll Bar의 색

     

    1. 'scrollbar_bg'를 Sprite Editor로 자르기

     

    2. Scrollbar Vertical의 Source Image, Color, Image Type, PosX 변경

    : Color는 (242, 242, 242, 255)로 설정

     

    3. Handle의 Source Image, Color, Image Type 변경

    : Color는 임의로 (140, 140, 140, 255)로 설정

     

    >> 'Shop Item Button' Prefab에 'Hot Icon Image'  추가

    : Image로 생성 --> 모든 버튼이 Hot 상품은 아니므로 Active를 체크 해제

    --> Anchor는 Alt + Shift

     

    >> Shop Popup Panel UI 제작

    : 아래 사진과 같이 제작

    --> 'Shop Item Button' Prefab을 Content의 자식으로 추가 및 복붙

    --> Button마다 사진에 맞게 Color나 Title Text, Price Text 등을 모두 변경

     

    >> 만든 버튼마다 OnClick()에 함수 바인딩

    : index는 각각 맞게 0부터 8까지 설정 --> 한꺼번에 선택해서 추가하고 index만 바꾸면 편함

     

    >> 'ShopPopupPanelController.cs'에 Shop Item Button(8) 바인딩

    : 구매 복원 버튼은 IOS 환경에서만 필요하기 때문에

     

    Level Popup Panel 만들기

    >> Popup Panel의 Prefab Variant를 'Level Popup Panel'라는 이름으로  생성

     

    >> Scroll View 생성

    --> Anchor는 Alt + Shift

     

    >> Content에 'Grid Layout Group' 추가

    - Button의 Width, Height를 Grid Layout Group의 Cell Size에서 수정 가능

    - Constraint : Column의 개수를 3개로 고정

     

    >> Content의 자식으로 'Level Item Button' 추가

    : Button으로 생성

     

    최종 코드

    >> MainPanelController.cs

    using System.Collections;
    using System.Collections.Generic;
    using TMPro;
    using UnityEngine;
    
    public class MainPanelController : MonoBehaviour
    {
        [SerializeField] private TMP_Text heartText;        // 남은 하트 수
        [SerializeField] private TMP_Text stageText;        // 현재 스테이지
        [SerializeField] private Transform canvasTransform; // Canvas의 위치
        
        [SerializeField] private GameObject settingsPopupPanelPrefab;
        [SerializeField] private GameObject shopPopupPanelPrefab;
        
        /// <summary>
        /// Play Button을 눌렀을 때 호출되는 Method
        /// </summary>
        public void OnClickPlayButton()
        {
            GameManager.Instance.StartGame();
        }
    
        #region Main Menu 버튼 클릭 함수
        
        /// <summary>
        /// Shop 아이콘 터치 시, 호출되는 Method
        /// </summary>
        public void OnClickShopButton()
        {
            Instantiate(shopPopupPanelPrefab, canvasTransform);
        }
    
        /// <summary>
        /// Stage 아이콘 터치 시, 호출되는 Method
        /// </summary>
        public void OnClickStageButton()
        {
            
        }
        
        /// <summary>
        /// Leaderboard 아이콘 터치 시, 호출되는 Method
        /// </summary>
        public void OnClickLeaderboardButton()
        {
            
        }
    
        /// <summary>
        /// Settings 아이콘 터치 시, 호출되는 Method
        /// </summary>
        public void OnClickSettingsButton()
        {
            Instantiate(settingsPopupPanelPrefab, canvasTransform); // Canvas의 자식 오브젝트로 생성되도록
        }
    
        #endregion
    }

     

    >> PopupPanelController.cs

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using TMPro;
    using UnityEngine;
    using UnityEngine.UI;
    using DG.Tweening;
    
    public class PopupPanelController : MonoBehaviour
    {
        [SerializeField] private TMP_Text titleText;        // 화면 상단의 Title Text
        [SerializeField] private GameObject panelObject;    // 팝업창 오브젝트
        
        private Image _backgroundImage;
    
        private void Awake()
        {
            _backgroundImage = GetComponent<Image>();
            
            var color = _backgroundImage.color;     // color는 Property기 때문에 바로 접근하여 수정 불가능하다
            color.a = 0;
            _backgroundImage.color = color;
            
            panelObject.GetComponent<CanvasGroup>().alpha = 0;
        }
    
        private void Start()
        {
            ShowPopupPanel();
        }
    
        /// <summary>
        /// 타이틀 텍스트에 타이틀 지정 함수
        /// </summary>
        /// <param name="title">타이틀</param>
        public void SetTitleText(string title)
        {
            titleText.text = title;
        }
    
        /// <summary>
        /// 닫기 버튼을 클릭했을 때 실행되는 함수
        /// </summary>
        public void OnClickCloseButton()
        {
            HidePopupPanel();
        }
    
        private void ShowPopupPanel()
        {
            // 초기화
            _backgroundImage.DOFade(0, 0);
            panelObject.GetComponent<CanvasGroup>().DOFade(0, 0);
            panelObject.GetComponent<RectTransform>().DOAnchorPosY(-500f, 0);
            
            _backgroundImage.DOFade(1f, 0.2f);
            panelObject.GetComponent<CanvasGroup>().DOFade(1f, 0.2f);
            panelObject.GetComponent<RectTransform>().DOAnchorPosY(0, 0.2f);
        }
    
        private void HidePopupPanel()
        {
            _backgroundImage.DOFade(1f, 0);
            panelObject.GetComponent<CanvasGroup>().DOFade(1f, 0);
            panelObject.GetComponent<RectTransform>().DOAnchorPosY(0, 0);
    
            _backgroundImage.DOFade(0, 0.2f);
            panelObject.GetComponent<CanvasGroup>().DOFade(0, 0.2f);
            panelObject.GetComponent<RectTransform>().DOAnchorPosY(-500f, 0.2f)
                .OnComplete(() => Destroy(gameObject));
        }
    }

     

    >> ShopPopupPanelController.cs

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    [RequireComponent(typeof(PopupPanelController))]
    public class ShopPopupPanelController : MonoBehaviour
    {
        [SerializeField] private GameObject restorePurchaseButtonObject;
        
        private void Start()
        {
            GetComponent<PopupPanelController>().SetTitleText("SHOP");
    
    #if UNITY_IOS // IOS일 때만 적용되고 아니라면 아래 코드가 제거된다.
            restorePurchaseButtonObject.SetActive(true);
    #else
            restorePurchaseButtonObject.SetActive(false);
    #endif
        }
    
        public void OnClickShopItemButton(int index)
        {
            switch (index)
            {
                case 0: // TODO: 하트 60개 + 광고 제거
                    break;
                case 1: // TODO: 광고 보고 하트 3개 받기
                    break;
                case 2: // TODO: 광고 제거
                    break;
                case 3: // TODO: 하트 20개
                    break;
                case 4: // TODO: 하트 60개
                    break;
                case 5: // TODO: 하트 150개
                    break;
                case 6: // TODO: 하트 320개
                    break;
                case 7: // TODO: 하트 450개
                    break;
                case 8: // TODO: 구매 복원 (아이폰만 필요)
                    break;
            }
        }
    }

     

     

    >> SettingPopupPanelController.cs

    : 스스로 제작 중

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.UI;
    using DG.Tweening;
    
    [RequireComponent(typeof(PopupPanelController))]
    public class SettingPopupPanelController : MonoBehaviour
    {
        // BGM, SFX Button Animation
        [SerializeField] private Image sfxButton;
        [SerializeField] private Image bgmButton;
        [SerializeField] private RectTransform sfxToggle;
        [SerializeField] private RectTransform bgmToggle;
        private bool onSFX = true;
        private bool onBGM = true;
    
        private void Start()
        {
            GetComponent<PopupPanelController>().SetTitleText("SETTINGS");
        }
        
        public void OnClickSFXButton()
        {
            if (onSFX)
            {
                onSFX = false;
                // 초기화
                //sfxButton.DOColor(new Color(242, 68, 149, 255), 0);
                //sfxToggle.DOAnchorPosX(35f, 0);
                
                sfxButton.DOColor(new Color(195, 198, 207, 255), 0.2f);
                sfxToggle.DOAnchorPosX(-35f, 0.2f);
            }
            else
            {
                onSFX = true;
                sfxButton.DOColor(new Color(242, 68, 149, 255), 0.2f);
                sfxToggle.DOAnchorPosX(35f, 0.2f);
            }
        }
    
        public void OnClickBGMButton()
        {
            if (onBGM)
            {
                onBGM = false;
                // 초기화
                //bgmButton.DOColor(new Color(242, 68, 149, 255), 0);
                //bgmToggle.DOAnchorPosX(35f, 0);
                
                bgmButton.DOColor(new Color(195, 198, 207, 255), 0.2f);
                bgmToggle.DOAnchorPosX(-35f, 0.2f);
            }
            else
            {
                onBGM = true;
                bgmButton.DOColor(new Color(242, 68, 149, 255), 0.2f);
                bgmToggle.DOAnchorPosX(35f, 0.2f);
            }
        }
    }