목차
※ Object들을 만들 때부터 이름을 잘 정해놓아야 한다.
: 어지러운 Object들을 보면 만든 나도 구분을 못하고, 혼자 만드는 게 아니라 같이 만든다면 더욱 문제가 된다.
퀴즈 게임 만들기
25.02.14
카드 기능 구현
: 'Quiz Card' Prefab 수정
>> 'Buttons' 의 이름을 'Three Option Buttons'로 바꾸고 부모 오브젝트로 Options 생성
--> Options로 위치를 제어하기 위해 Three Option Buttons는 초기화
--> Anchor는 Alt + Shift
--> Options는 빈 게임 오브젝트로 생성
>> OX Buttons
: Three Options Button 복붙, 버튼 하나 제거 (맨 밑에 있는 버튼 제거) --> Button의 Text를 O, X로 수정
>> Quiz Card의 QuizCardController.cs에 바인딩
>> Button들 전부 선택하여 OnClick() 함수 바인딩
--> Button의 Index를 각각 설정 (위에서부터 0, 1, 2 / 0, 1)
--> 이름 변경 (First Option Button, Second Option Button, Third Option Button / O Button, X Button)
└ Front Panel
: 빈 게임 오브젝트로 만들어서 Text와 Options를 자식 오브젝트로 넣기
└ Correct Back Panel
: 빈 게임 오브젝트로 만들고 오른쪽으로 치워두기 (Left, Right 조정)
--> 이름 : Correct Back Panel
>> Description Text 추가
>> 빈 게임 오브젝트로 Buttons 추가
--> Anchor는 Alt + Shift
>> Buttons의 자식으로 Button 추가 (Next Quiz Button, Exit Button)
: Button의 Text 수정
--> 그 다음 NextQuizButton을 복사하여 Exit Button 생성 (Exit Button의 Text는 "종료")
>> 각 버튼에 맞게 OnClick() 함수 바인딩
--> Exit Button은 OnClickExitButton() 바인딩
└ Incorrect Back Panel
: Correct Back Panel을 복붙하고 위치 조정
>> 복사한 Next Quiz Button의 이름을 Retry Quiz Button으로 수정
: Text도 수정 ("다시 도전" 으로 수정) / Retry Quiz Button의 OnClick()에 바운딩된 함수도 수정해줘야 한다.
>> Text로 Heart Count Text (TMP) 생성
>> QuizCardController.cs에 각각 바인딩
HeartCount 구현
>> GameScene에 빈 게임 오브젝트로 GameManager 추가
>> Quiz Card Controller.cs 에 Heart Count Text 바인딩
>> UserInformations.cs 생성
: PlayerPrefs를 활용하여 HeartCount에 대한 정보를 저장 --> static property를 활용
>> 줄어든 HeartCount 다시 늘리기
1. 레지스트리 편집기 열기 --> 실행을 열어서 (윈도우 + R) 'regedit' 입력
2. 경로 : 컴퓨터\HKEY_CURRENT_USER\SOFTWARE\Unity\UnityEditor\(Company Name)\(Product Name)
3. HeartCount 값 조정
※ 혹시 경로에 설정한 Company Name이 없다면
: Company Name을 설정만 하고 Play를 한 적이 없어서 정보가 없는 것 --> 한번 Play하고 다시 가보자
>> 기존 값은 'DefaultCompany'에 저장되어 있을 것
└ Company Name, Product Name 설정
: Build Settings -> PlayerSettings
--> Company Name, Product Name 수정
※ 업데이트 할 때마다 늘어나는 버전 코드....? --> 알아보자
※ App Life Cycle
- Not Running : 실행되지 않거나 종료된 상태
- InActive : 앱이 Foreground 상태로 돌아가지만, 이벤트는 받지 않는 상태, 앱의 상태 전환 과정에서 잠깐 머무는 단계
- Active : 일반적으로 앱이 돌아가는 상태 (이벤트를 받는 단계)
- Background : 앱이 Suspended 상태 (유예 상태)로 진입되기 전 거치는 상태 --> 이 경우 앱이 종료되었다고 봐야하며 음악, 통화 앱 같은 경우는 Background에 머물고 보통 다른 앱들은 바로 Suspended 상태로 넘어간다.
- Suspended : 앱이 Background 상태에 있지만, 아무 코드도 실행하지 않는 상태, 시스템이 임의로 Background 상태의 앱을 Suspended 상태로 만든다. (리소스 해제) --> 일반적으로 Not Running과 동일한 상태.
--> 주요 작업은 Active와 Background 상태에서 수행된다.
※ OnApplicationPause(bool pauseStatus) 함수
스테이지 클리어 정보 저장
: UserInformations.cs에서 PlayerPrefs를 활용하여 LastStageIndex에 대한 정보를 저장 --> static property를 활용
--> QuizCardController에서 클리어한 quizCardIndex도 저장
※ PlayerPrefs
:
QuizData 수정
: QuizData-0.csv 파일을 퀴즈5까지 줄이고 복붙하여 QuizData-0부터 QuizData-4까지 총 5개의 QuizData 만듦
--> 이후 OX퀴즈는 보기가 필요 없으므로 제거
>> Stage 구분을 위해 QuizData의 Question을 수정
--> 다른 QuizData파일도 각각 맞게 수정
원형 타이머 제작
>> Image는 Figma를 통해 제작
>> Game panel의 GamePanelController.cs 를 비활성화
: GameScene에서 확인하기 위해
최종 코드
>> GameManager.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class GameManager : Singleton<GameManager>
{
[HideInInspector] public int heartCount;
private void Start()
{
heartCount = UserInformations.HeartCount;
}
public void StartGame()
{
SceneManager.LoadScene("Game");
}
public void QuitGame()
{
SceneManager.LoadScene("Main");
}
public void AllClearStage()
{
// TODO: 전체 스테이지 클리어 처리
QuitGame();
}
protected override void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
}
private void OnApplicationQuit() // 게임의 정상적인 종료 시, 호출
{
Debug.Log("OnApplicationQuit!");
UserInformations.HeartCount = heartCount;
}
}
>> GamePanelController.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class GamePanelController : MonoBehaviour
{
private GameObject _firstQuizCardObject;
private GameObject _secondQuizCardObject;
private List<QuizData> _quizDataList;
private int _lastGeneratedQuizIndex;
private int _lastStageIndex;
private void Start()
{
_lastStageIndex = UserInformations.LastStageIndex;
InitQuizCard(_lastStageIndex);
}
private void InitQuizCard(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);
// 마지막으로 생성된 Quiz Index
_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();
}
else
{
// TODO: 스테이지 클리어 연출
InitQuizCard(++_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(); // 같은 depth에서 마지막으로 이동 --> 카드가 앞으로 배치됨
}
else if (index == 1)
{
quizCardTransform.anchoredPosition = new Vector2(0, 160);
quizCardTransform.localScale = Vector3.one * 0.9f;
quizCardTransform.SetAsFirstSibling(); // 같은 depth에서 처음으로 이동
}
}
private void ChangeQuizCard()
{
if (_lastGeneratedQuizIndex >= Constants.MAX_QUIZ_COUNT) return;
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);
SetQuizCardPosition(_secondQuizCardObject, 1);
ObjectPool.Instance.ReturnObject(temp);
}
}
>> QuizCardController.cs
using System;
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
public struct QuizData
{
public string question;
public string description;
public int type;
public int answer;
public string firstOption; // 원래는 string[] Options로 했었다
public string secondOption;
public string thirdOption;
}
public class QuizCardController : MonoBehaviour
{
[SerializeField] private GameObject frontPanel;
[SerializeField] private GameObject correctBackPanel;
[SerializeField] private GameObject incorrectBackPanel;
// Front Panel
[SerializeField] private TMP_Text questionText; // 퀴즈
[SerializeField] private TMP_Text descriptionText; // 설명
[SerializeField] private Button[] optionButtons; // 보기 --> 타입을 TMP_Text로 해서 text를 직접 받아도 된다.
[SerializeField] private GameObject threeOptionButtons; // 퀴즈 타입에 따른 버튼
[SerializeField] private GameObject oxButtons; // 퀴즈 타입에 따른 버튼
// Incorrect Back Panel
[SerializeField] private TMP_Text heartCountText;
private enum QuizCardPanelType { FrontPanel, CorrectBackPanel, IncorrectBackPanel }
public delegate void QuizCardDelegate(int cardIndex);
private event QuizCardDelegate onCompleted;
private int _answer;
private int _quizCardIndex;
private Vector2 _correctBackPanelPosition;
private Vector2 _incorrectBackPanelPosition;
private void Awake()
{
// 숨겨진 패널의 좌표 저장
_correctBackPanelPosition = correctBackPanel.GetComponent<RectTransform>().anchoredPosition;
_incorrectBackPanelPosition = incorrectBackPanel.GetComponent<RectTransform>().anchoredPosition;
}
public void SetQuiz(QuizData quizData, int quizCardIndex, QuizCardDelegate onCompleted)
{
// 1. 퀴즈
// 2. 설명
// 3. 타입 (0: OX퀴즈, 1: 보기 3개 객관식)
// 4. 정답
// 5. 보기 (1, 2, 3)
// 퀴즈 카드 인덱스 할당
_quizCardIndex = quizCardIndex;
// front Panel 표시
SetQuizCardPanelActive(QuizCardPanelType.FrontPanel);
// 퀴즈 데이터 표현
questionText.text = quizData.question;
_answer = quizData.answer;
descriptionText.text = quizData.description;
if (quizData.type == 0) // 3지선다 퀴즈
{
threeOptionButtons.SetActive(true);
oxButtons.SetActive(false);
var firstButtonText = optionButtons[0].GetComponentInChildren<TMP_Text>();
firstButtonText.text = quizData.firstOption;
var secondButtonText = optionButtons[1].GetComponentInChildren<TMP_Text>();
secondButtonText.text = quizData.secondOption;
var thirdButtonText = optionButtons[2].GetComponentInChildren<TMP_Text>();
thirdButtonText.text = quizData.thirdOption;
}
else if (quizData.type == 1) // OX 퀴즈
{
oxButtons.SetActive(true);
threeOptionButtons.SetActive(false);
}
this.onCompleted = onCompleted;
// Incorrect Back Panel
heartCountText.text = GameManager.Instance.heartCount.ToString();
}
public void OnClickOptionButton(int buttonIndex)
{
if (buttonIndex == _answer) // 정답
{
Debug.Log("정답!");
// TODO: 정답 연출
SetQuizCardPanelActive(QuizCardPanelType.CorrectBackPanel);
}
else // 오답
{
Debug.Log("오답!");
// TODO: 오답 연출
SetQuizCardPanelActive(QuizCardPanelType.IncorrectBackPanel);
}
}
private void SetQuizCardPanelActive(QuizCardPanelType quizCardPanelType)
{
switch (quizCardPanelType)
{
case QuizCardPanelType.FrontPanel:
frontPanel.SetActive(true);
correctBackPanel.SetActive(false);
incorrectBackPanel.SetActive(false);
correctBackPanel.GetComponent<RectTransform>().anchoredPosition = _correctBackPanelPosition;
incorrectBackPanel.GetComponent<RectTransform>().anchoredPosition = _incorrectBackPanelPosition;
break;
case QuizCardPanelType.CorrectBackPanel:
frontPanel.SetActive(false);
correctBackPanel.SetActive(true);
incorrectBackPanel.SetActive(false);
correctBackPanel.GetComponent<RectTransform>().anchoredPosition = Vector2.zero;
incorrectBackPanel.GetComponent<RectTransform>().anchoredPosition = _incorrectBackPanelPosition;
break;
case QuizCardPanelType.IncorrectBackPanel:
frontPanel.SetActive(false);
correctBackPanel.SetActive(false);
incorrectBackPanel.SetActive(true);
correctBackPanel.GetComponent<RectTransform>().anchoredPosition = _correctBackPanelPosition;
incorrectBackPanel.GetComponent<RectTransform>().anchoredPosition = Vector2.zero;
break;
}
}
public void OnClickExitButton()
{
}
#region Correct Back panel
/// <summary>
/// 다음 버튼 이벤트
/// </summary>
public void OnClickNextQuizButton()
{
onCompleted?.Invoke(_quizCardIndex);
}
#endregion
#region Incorrect Back Panel
/// <summary>
/// 다시 도전 버튼 이벤트
/// </summary>
public void OnClickRetryQuizButton()
{
if (GameManager.Instance.heartCount > 0)
{
GameManager.Instance.heartCount--;
heartCountText.text = GameManager.Instance.heartCount.ToString(); // Incorrect Back Panel에 heartCount 표시
SetQuizCardPanelActive(QuizCardPanelType.FrontPanel);
}
else
{
// 하트가 부족해서 Retry 불가
// TODO: 하트 부족 알림 구현
}
}
#endregion
}
>> UserInformation.cs
: PlayerPrefs를 활용하여 HeartCount과 LastStageIndex에 대한 정보를 저장 --> static property를 활용
using UnityEngine;
public static class UserInformations
{
private const string HEART_COUNT = "HeartCount"; // string key 값 저장
private const string LAST_STAGE_INDEX = "LastStageIndex";
// 하트 수
public static int HeartCount
{
get
{
// "HeartCount"라는 이름의 정보를 Int로 가져오는데, Default 값은 5 --> 최초로 게임이 시작되면 저장된 값이 없기 때문
return PlayerPrefs.GetInt(HEART_COUNT, 5);
}
set
{
// value 값으로 PlayerPrefs에 저장
PlayerPrefs.SetInt(HEART_COUNT, value);
}
}
// 스테이지 클리어 정보
public static int LastStageIndex
{
get
{
return PlayerPrefs.GetInt(LAST_STAGE_INDEX, 0);
}
set
{
PlayerPrefs.GetInt(LAST_STAGE_INDEX, value);
}
}
}
>> Constants.cs
: 공통적인 상수 관리
public class Constants
{
public const int MAX_QUIZ_COUNT = 5; // 한 스테이지에 나오는 퀴즈의 수
public const int MAX_STAGE_COUNT = 3; // 전체 스테이지 수
}
>> MobicsTimer.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class MobicsTimer : MonoBehaviour
{
[Serializable]
public class FillSettings
{
public Color color;
}
public FillSettings fillSettings;
[Serializable]
public class BackgroundSettings
{
public Color color;
}
public BackgroundSettings backgroundSettings;
[SerializeField] private Image fillImage;
[SerializeField] private float totalTime;
public float CurrentTime { get; private set; } // 현재 시간 저장
private bool _isPaused; // 현재 Pause 상태인지 체크
private void Update()
{
if (!_isPaused)
{
CurrentTime += Time.deltaTime;
fillImage.fillAmount = CurrentTime / totalTime;
}
}
public void StartTimer()
{
_isPaused = false;
}
public void PauseTimer()
{
_isPaused = true;
}
public void ResetTimer()
{
CurrentTime = 0;
fillImage.fillAmount = 1;
}
}
'Development > C#' 카테고리의 다른 글
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 57일차 (0) | 2025.02.18 |
---|---|
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 56일차 (0) | 2025.02.17 |
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 54일차 (0) | 2025.02.13 |
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 53일차 (0) | 2025.02.12 |
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 52일차 (0) | 2025.02.11 |