- Published on
섹션 4: 퀴즈 마스터
#섹션 4: 퀴즈 마스터
Unity UI 시스템을 활용한 퀴즈 게임 개발 가이드입니다. Canvas와 TextMeshPro를 이용한 UI 구성부터 ScriptableObject를 통한 데이터 관리까지 학습합니다.
#목차
#{ 유니티 UI 버튼에 스크립터블 오브젝트에서 가져온 텍스트를 표시하기 }
// Unity에서 사용하는 기본 네임스페이스들
using System.Collections;
using System.Collections.Generic;
// TextMeshPro(텍스트 컴포넌트)를 사용하기 위한 네임스페이스
using TMPro;
// Unity 엔진 관련 클래스들을 쓰기 위한 네임스페이스
using UnityEngine;
// Quiz 클래스는 MonoBehaviour를 상속받아 유니티 오브젝트에 붙일 수 있는 스크립트입니다
public class Quiz : MonoBehaviour
{
// 화면에 질문을 보여줄 텍스트 UI 컴포넌트 (TextMeshProUGUI)
[SerializeField] TextMeshProUGUI questionText;
// 질문과 답안 데이터를 담고 있는 스크립터블 오브젝트 참조
[SerializeField] QuestionSo questions;
// 답변 버튼 오브젝트들을 배열로 저장
[SerializeField] GameObject[] answerButtons;
// 유니티에서 게임 시작 시 자동으로 호출되는 메서드
void Start()
{
// 질문 텍스트 UI에 스크립터블 오브젝트에서 받아온 질문을 설정
questionText.text = questions.GetQuestion();
// 버튼 배열의 길이만큼 반복하면서 각 버튼에 텍스트를 설정
for (int i = 0; i < answerButtons.Length; i++) {
// 현재 버튼 오브젝트의 자식 중 TextMeshProUGUI 컴포넌트를 찾아 저장
TextMeshProUGUI buttonText = answerButtons[i].GetComponentInChildren<TextMeshProUGUI>();
// 해당 인덱스의 답안을 받아와 버튼 텍스트로 설정
buttonText.text = questions.GetAnswer(i);
}
}
}
#1. 버튼 정보를 담을 변수 만들기
🎈 목적
답안을 표시할 버튼 오브젝트들을 코드에서 다루기 위해 변수로 저장해야 함
🧠 개념 설명
버튼은 GameObject로 다루기로 함
→ 왜냐면 우리가 주로 조작할 것은 버튼 안의 텍스트나 이미지이기 때문
버튼이 여러 개 있으므로 배열([])로 만들기
[SerializeField] GameObject[] answerButtons;
SerializeField: 유니티 인스펙터에서 배열을 설정할 수 있도록 함
GameObject[]: 여러 개의 버튼을 저장할 수 있는 배열 타입
#2. 인스펙터에서 버튼 연결하기
QuizCanvas 또는 해당 스크립트가 연결된 오브젝트 선택
인스펙터에서 Answer Buttons라는 배열이 생긴 걸 확인
버튼 4개를 배열에 드래그해서 각각 넣기
💡 인스펙터 잠금 기능(자물쇠 아이콘)을 사용하면 빠르게 드래그 가능!
#3. 버튼에 텍스트 넣기
🎈 목적
각 버튼에 질문에 대한 답안 텍스트를 표시해야 함
🧠 개념 설명
버튼은 여러 오브젝트를 자식으로 가짐
텍스트는 그 자식 오브젝트의 TextMeshProUGUI 컴포넌트임
그래서 GetComponentInChildren<TextMeshProUGUI>() 사용해서 찾을 수 있음
#4. 버튼 4개에 자동으로 텍스트 할당 (for 루프 사용)
#개념 설명 - for 루프
for (초기화; 조건; 증가) {
// 반복 코드
}
int i = 0: 반복자 변수 선언
i < answerButtons.Length: 버튼 개수만큼 반복
i++: 반복자가 1씩 증가
#{ 버튼 클릭 정답 오답 기능 추가 }
// 퀴즈 게임의 전체 흐름을 제어하는 클래스입니다
public class Quiz : MonoBehaviour
{
// UI에 질문을 표시할 텍스트 필드
[SerializeField] TextMeshProUGUI questionText;
// 질문과 정답 데이터를 담고 있는 ScriptableObject
[SerializeField] QuestionSo questions;
// 보기(정답 버튼들)를 담고 있는 배열
[SerializeField] GameObject[] answerButtons;
// 정답의 인덱스를 저장할 변수
int correctAnswerIndex;
// 보기 버튼의 기본 이미지 (보통 파란색 등)
[SerializeField] Sprite defaltAnswerButtonSprite;
// 정답 버튼으로 표시할 이미지 (주황색 등)
[SerializeField] Sprite correctAnswerButtonSprite;
// 게임 시작 시 실행되는 함수
void Start()
{
// 질문 텍스트를 UI에 표시
questionText.text = questions.GetQuestion();
// 각 버튼에 보기 텍스트를 표시
for (int i = 0; i < answerButtons.Length; i++)
{
// 버튼 내부에 있는 TextMeshProUGUI 컴포넌트 가져오기
TextMeshProUGUI buttonText = answerButtons[i].GetComponentInChildren<TextMeshProUGUI>();
// 해당 버튼에 맞는 보기 텍스트 설정
buttonText.text = questions.GetAnswer(i);
}
}
// 사용자가 버튼을 클릭했을 때 실행되는 함수
// index는 클릭한 버튼이 몇 번째인지 알려줍니다 (0~3)
public void OnAnswerSelected(int index)
{
// 버튼의 이미지 컴포넌트를 저장할 변수
Image buttonImage;
// 정답을 맞췄을 경우
if (index == questions.GetCorrectAnswerIndex())
{
// 텍스트에 정답 메시지를 표시
questionText.text = "정답입니다!";
// 클릭한 버튼의 이미지 컴포넌트 가져오기
buttonImage = answerButtons[index].GetComponent<Image>();
// 버튼의 이미지를 정답 이미지로 변경
buttonImage.sprite = correctAnswerButtonSprite;
}
// 오답을 선택했을 경우
else
{
// 정답의 인덱스를 가져오기
correctAnswerIndex = questions.GetCorrectAnswerIndex();
// 정답 텍스트 가져오기
string correctAnser = questions.GetAnswer(correctAnswerIndex);
// 텍스트에 오답 메시지와 함께 정답을 표시
questionText.text = "오답입니다! 정답은 " + correctAnser + "입니다.";
// 실제 정답 버튼의 이미지 컴포넌트 가져오기
buttonImage = answerButtons[correctAnswerIndex].GetComponent<Image>();
// 정답 버튼의 이미지를 변경해서 강조
buttonImage.sprite = correctAnswerButtonSprite;
}
}
}
#목표
질문에 대한 정답 여부를 확인하고
화면에 피드백을 주며
버튼의 이미지(스프라이트)를 정답에 따라 변경
#1단계: 질문 표시하기 (준비 완료 상태)
questionText.text = questions.GetQuestion();
questions는 Scriptable Object로부터 가져온 질문
questionText는 질문을 출력할 TextMeshProUGUI
게임이 시작되면 질문을 UI에 표시
#2단계: 버튼에 정답 텍스트 표시하기
for (int i = 0; i < answerButtons.Length; i++)
{
TextMeshProUGUI buttonText = answerButtons[i].GetComponentInChildren<TextMeshProUGUI>();
buttonText.text = questions.GetAnswer(i);
}
각 버튼 안의 텍스트를 질문의 보기 항목으로 채워줌
questions.GetAnswer(i)를 통해 보기 텍스트를 받아옴
#3단계: 클릭 시 동작할 메서드 만들기
public void OnAnswerSelected(int index)
플레이어가 버튼을 클릭하면 이 메서드가 실행됨
index는 어떤 버튼을 클릭했는지를 나타내는 값
#4단계: 정답 확인 및 피드백 주기
#정답일 경우
if (index == questions.GetCorrectAnswerIndex())
{
questionText.text = "정답입니다!";
buttonImage = answerButtons[index].GetComponent<Image>();
buttonImage.sprite = correctAnswerButtonSprite;
}
questions.GetCorrectAnswerIndex()로 정답 인덱스를 가져옴
맞춘 경우 questionText에 "정답입니다!" 표시
버튼 이미지(Sprite)를 정답용으로 교체
#오답일 경우
else
{
correctAnswerIndex = questions.GetCorrectAnswerIndex();
string correctAnswer = questions.GetAnswer(correctAnswerIndex);
questionText.text = "오답입니다! 정답은 " + correctAnswer + "입니다.";
buttonImage = answerButtons[correctAnswerIndex].GetComponent<Image>();
buttonImage.sprite = correctAnswerButtonSprite;
}
오답일 경우 정답의 인덱스를 다시 확인
정답 내용을 출력
정답 버튼의 이미지로 교체 (정답을 강조해줌)
#5단계: 버튼에 OnClick 이벤트 연결하기
✅ 방법
유니티 에디터에서 네 개의 버튼을 모두 선택
Button 컴포넌트의 OnClick 이벤트 영역에 + 버튼 클릭
Quiz 스크립트가 붙은 오브젝트(QuizCanvas 등)를 드래그해서 연결
함수 선택 → Quiz -> OnAnswerSelected(int)
각 버튼마다 0, 1, 2, 3 인덱스를 입력
🧠 개념
버튼을 누르면 어떤 인덱스인지 OnAnswerSelected(index)에 전달
모든 버튼은 동일한 메서드를 사용하지만 index 값으로 어떤 버튼인지 구분
#6단계: 버튼 이미지 Sprite 연결
Quiz 오브젝트를 선택
defaltAnswerButtonSprite, correctAnswerButtonSprite 필드에 원하는 스프라이트 넣기
예: 기본은 파란색, 정답은 주황색
#{ 버튼 비 활성화 시키기 }
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
public class Quiz : MonoBehaviour
{
// 질문 텍스트를 표시할 UI 텍스트 객체 (TextMeshPro 사용)
[SerializeField] TextMeshProUGUI questionText;
// 퀴즈 질문과 정답 정보를 담고 있는 ScriptableObject
[SerializeField] QuestionSo questions;
// 정답 선택지 버튼들을 담은 배열 (게임 오브젝트)
[SerializeField] GameObject[] answerButtons;
// 정답 인덱스를 저장할 변수
int correctAnswerIndex;
// 기본 버튼 이미지 (선택 전)
[SerializeField] Sprite defaltAnswerButtonSprite;
// 정답 선택 시 바뀔 버튼 이미지
[SerializeField] Sprite correctAnswerButtonSprite;
// 게임 시작 시 처음 호출되는 메서드
void Start()
{
// 질문을 표시하는 메서드 호출 (초기 문제 세팅)
DisplayQuestion();
// 다음 문제를 불러오는 방식이 준비되어 있지만, 현재는 주석처리 되어 있음
//GetNextQuestion();
}
// 사용자가 정답 버튼을 클릭했을 때 호출되는 메서드
public void OnAnswerSelected(int index)
{
Image buttonImage;
// 사용자가 선택한 버튼 인덱스가 정답일 경우
if (index == questions.GetCorrectAnswerIndex())
{
// "정답입니다!" 텍스트 표시
questionText.text = "정답입니다!";
// 선택한 버튼의 이미지 가져와서 정답 스프라이트로 변경
buttonImage = answerButtons[index].GetComponent<Image>();
buttonImage.sprite = correctAnswerButtonSprite;
}
else
{
// 정답 인덱스를 가져옴
correctAnswerIndex = questions.GetCorrectAnswerIndex();
// 정답 텍스트 내용 가져오기
string correctAnser = questions.GetAnswer(correctAnswerIndex);
// "오답입니다!" + 정답 표시
questionText.text = "오답입니다! 정답은 " + correctAnser + "입니다.";
// 정답 버튼을 찾아서 이미지 변경
buttonImage = answerButtons[correctAnswerIndex].GetComponent<Image>();
buttonImage.sprite = correctAnswerButtonSprite;
}
// 모든 버튼을 비활성화하여 한 번만 선택 가능하게 함
SetButtonState(false);
}
// 다음 문제를 불러올 때 사용할 메서드
void GetNextQuestion()
{
// 버튼들을 다시 활성화 (클릭 가능하게)
SetButtonState(true);
// 버튼 이미지 초기화 (기본 스프라이트로 설정)
SetDefaultButtonSprites();
// 새로운 질문과 보기 표시
DisplayQuestion();
}
// 질문과 보기를 UI에 표시하는 메서드
private void DisplayQuestion()
{
// 질문 텍스트 설정
questionText.text = questions.GetQuestion();
// 각 버튼에 보기 텍스트 설정
for (int i = 0; i < answerButtons.Length; i++)
{
// 버튼 안의 텍스트 컴포넌트 가져오기
TextMeshProUGUI buttonText = answerButtons[i].GetComponentInChildren<TextMeshProUGUI>();
// 보기 텍스트 설정
buttonText.text = questions.GetAnswer(i);
}
}
// 모든 버튼의 상태(활성/비활성)를 설정하는 메서드
void SetButtonState(bool state)
{
for(int i = 0; i < answerButtons.Length; i++)
{
// 버튼 컴포넌트 가져오기
Button button = answerButtons[i].GetComponent<Button>();
// 버튼을 활성화(true) 또는 비활성화(false)
button.interactable = state;
}
}
// 버튼의 스프라이트(이미지)를 초기 상태로 되돌리는 메서드
void SetDefaultButtonSprites()
{
for(int i = 0; i < answerButtons.Length; i++)
{
// 버튼 이미지 컴포넌트 가져오기
Image buttonImage = answerButtons[i].GetComponent<Image>();
// 버튼 이미지를 기본 스프라이트로 설정
buttonImage.sprite = defaltAnswerButtonSprite;
}
}
}
#목표
버튼을 눌렀을 때 정답 여부를 보여주고, 다른 버튼을 더 이상 누를 수 없게 만들자.
#1. 🎈개념 먼저 이해하기
✅ 문제 상황
사용자가 정답/오답을 선택했는데도 계속 다른 버튼을 눌러 결과를 바꿀 수 있다.
우리는 퀴즈에서 정답을 고르면 그 순간 선택을 마무리해야 한다.
✅ Unity에서 버튼을 "비활성화"한다는 건?
버튼 오브젝트에는 Button 컴포넌트가 붙어 있음.
button.interactable = false 로 설정하면 버튼을 클릭할 수 없음!
interactable 토글을 꺼서 다시 클릭을 못하게 해야한다
#2. 💡 코드 정리하기
✅ Start() → 게임이 시작될 때 호출됨
기존에는 문제를 보여주는 역할만 했어요:
void Start()
{
DisplayQuestion();
}
이제는 다음 문제를 준비하는 새로운 메서드를 만들었으니 아래처럼 바꿉니다:
void Start()
{
GetNextQuestion(); // 정리된 방식으로 문제 표시
}
#3. ✏️ 새로운 메서드들 추가하기
#`GetNextQuestion()` – 다음 문제 준비하기
void GetNextQuestion()
{
SetButtonState(true); // 모든 버튼 활성화
SetDefaultButtonSprites(); // 버튼 모양 초기화
DisplayQuestion(); // 질문과 보기 출력
}
버튼을 다시 클릭 가능하도록 만들고
이전 정답 표시(색상 등)를 초기화하고
새 문제를 보여주는 역할을 합니다.
#`SetButtonState(bool state)` – 버튼 활성화/비활성화
void SetButtonState(bool state) {
for(int i = 0; i < answerButtons.Length; i++) {
Button button = answerButtons[i].GetComponent<Button>();
button.interactable = state;
}
}
true면 버튼 클릭 가능
false면 버튼 클릭 불가능
#`SetDefaultButtonSprites()` – 버튼 모양 초기화
void SetDefaultButtonSprites() {
for(int i = 0; i < answerButtons.Length; i++) {
Image buttonImage = answerButtons[i].GetComponent<Image>();
buttonImage.sprite = defaltAnswerButtonSprite; // 기본 스프라이트로 초기화
}
}
버튼을 누른 후 색이 바뀌었다면
다음 문제 시작 전에 원래 모습으로 되돌리는 역할
#`OnAnswerSelected(int index)` – 사용자가 버튼을 누르면 실행
public void OnAnswerSelected(int index)
{
Image buttonImage;
if (index == questions.GetCorrectAnswerIndex())
{
questionText.text = "정답입니다!";
buttonImage = answerButtons[index].GetComponent<Image>();
buttonImage.sprite = correctAnswerButtonSprite;
}
else
{
int correctAnswerIndex = questions.GetCorrectAnswerIndex();
string correctAnswer = questions.GetAnswer(correctAnswerIndex);
questionText.text = "오답입니다! 정답은 " + correctAnswer + "입니다.";
buttonImage = answerButtons[correctAnswerIndex].GetComponent<Image>();
buttonImage.sprite = correctAnswerButtonSprite;
}
SetButtonState(false); // 선택 후 버튼 비활성화
}
#{ 타이머 시스템 구현 }
#최종 목표는?
문제를 푸는 시간(예: 30초) 동안 타이머가 줄어들고,
시간이 끝나면 정답을 보여주는 시간(예: 10초)이 시작되고,
다시 문제 풀이 시간으로 돌아가는 구조를 반복하는 것.
#타이머 스크립트 만들기
Assets > Scripts 폴더에 가서
우클릭 → Create > C# Script
이름을 Timer로 정함 (첫 글자 대문자 꼭!)
캔버스에 Timer 을 만들고 Timer 스크립트를 넣어준다
( 타이머 스크립트 내용 작성은 아래에서 계속 )
#타이머가 어떻게 동작하나요?
// Unity에 필요한 네임스페이스 (기본적인 클래스, 컬렉션 등을 사용 가능)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Timer : MonoBehaviour
{
// 문제 풀이 시간 (초 단위, 인스펙터에서 수정 가능)
[SerializeField] float timeToCompleteQuestion = 30f;
// 정답 보여주는 시간 (초 단위, 인스펙터에서 수정 가능)
[SerializeField] float timeToShowCorrectAnswer = 10f;
// 현재 상태가 '문제를 푸는 중인지' 여부를 나타내는 변수
public bool isAnsweringQuestion = false;
// 현재 남아있는 시간
float timerValue;
// 매 프레임마다 호출됨 (매 순간 시간을 줄여나가기 위해 필요)
void Update()
{
UpdateTimer(); // 타이머 값을 계산하고 상태를 전환하는 함수 호출
}
// 타이머 로직을 담당하는 함수
void UpdateTimer()
{
// 매 프레임마다 지난 시간만큼 타이머 값을 줄임
timerValue -= Time.deltaTime;
// 현재 상태가 '문제를 푸는 중'일 경우
if (isAnsweringQuestion)
{
// 시간이 다 되었으면
if (timerValue <= 0)
{
// 상태를 '정답 보여주는 중'으로 전환
isAnsweringQuestion = false;
// 정답 보여주는 시간으로 타이머를 초기화
timerValue = timeToShowCorrectAnswer;
}
}
// 현재 상태가 '정답 보여주는 중'일 경우
else
{
// 시간이 다 되었으면
if (timerValue <= 0)
{
// 다시 '문제 풀이 중' 상태로 전환
isAnsweringQuestion = true;
// 문제 풀이 시간으로 타이머를 초기화
timerValue = timeToCompleteQuestion;
}
}
// 현재 남은 시간을 로그로 출력 (디버깅 용도)
Debug.Log(timerValue);
}
}
#1. **필요한 변수 만들기**
[SerializeField] float timeToCompleteQuestion = 30f;
[SerializeField] float timeToShowCorrectAnswer = 10f;
public bool isAnsweringQuestion = false;
float timerValue;
timeToCompleteQuestion: 문제를 푸는 데 주어지는 시간 (예: 30초).
timeToShowCorrectAnswer: 정답을 보여주는 시간 (예: 10초).
isAnsweringQuestion: 지금 플레이어가 문제를 풀고 있는지 여부 (true면 문제 푸는 중).
timerValue: 실제로 줄어들며 남은 시간을 표시하는 변수.
#2. **게임 실행 중 계속 시간 체크하기**
void Update()
{
UpdateTimer();
}
Unity에서는 Update() 함수가 매 프레임마다 호출돼요.
여기에 우리가 만든 UpdateTimer()를 호출해서 매 순간 시간을 줄여요.
#`UpdateTimer()` 함수의 동작 순서
void UpdateTimer()
{
timerValue -= Time.deltaTime;
Time.deltaTime은 프레임마다 경과한 시간(초)입니다.
즉, 매 프레임마다 남은 시간(timerValue)을 조금씩 줄입니다.
#3. **시간이 다 됐을 때 상태 바꾸기**
if (isAnsweringQuestion)
{
if (timerValue <= 0)
{
isAnsweringQuestion = false;
timerValue = timeToShowCorrectAnswer;
}
}
else
{
if (timerValue <= 0)
{
isAnsweringQuestion = true;
timerValue = timeToCompleteQuestion;
}
}
이제 중요한 부분이에요!
만약 문제를 푸는 중인데 시간이 다 되면:
isAnsweringQuestion = false 로 상태를 바꿔서 정답 보기 상태로 전환.
타이머를 timeToShowCorrectAnswer 값(예: 10초)으로 설정.
만약 정답을 보고 있는 중인데 시간이 다 되면:
isAnsweringQuestion = true 로 바꿔서 다시 문제 풀이 상태로 전환.
타이머를 timeToCompleteQuestion 값(예: 30초)으로 설정.
이렇게 두 상태를 계속 번갈아가며 전환하는 구조예요.
#1단계: 캔버스에 이미지 타이머 만들기
**Hierarchy (계층 뷰)**에서 Canvas를 선택하거나 새로 만듭니다.
Canvas 우클릭 → UI → Image 선택
생성된 이미지 오브젝트 이름을 TimerImage로 바꿉니다.
Inspector 창에서 설정을 바꿔줍니다:
#2단계: Image 컴포넌트 설정하기
🔧 Image 설정
Source Image: 타이머로 쓸 원형 이미지 (Sprite)를 넣으세요 (예: 둥근 오렌지 원).
Image Type: Filled로 설정
Fill Method: Radial 360
Fill Origin: 원하는 방향 (예: Top)
Clockwise: 체크 해제 (반시계 방향)
Fill Amount: 1.0이 꽉 찬 상태, 0.0이 빈 상태
지금은 수동으로 1.0으로 설정돼 있지만, 나중에 스크립트에서 이걸 자동으로 조절할 거예요!
#`Image Type` 종류
Simple (기본값)
이미지를 원본 그대로 보여줘요.
크기 조절 시 비율이 왜곡될 수 있어요.
Sliced
9-slice 방식으로 이미지를 나눠서 보여줘요 (UI 버튼 같은 데 많이 사용).
이미지가 늘어나도 테두리는 그대로 유지돼요.
단, Sprite에서 Borders를 설정해줘야 작동해요.
Tiled
이미지를 반복해서 타일처럼 채워요.
백그라운드 패턴 등 반복 텍스처에 유용해요.
Filled ✅
말한 것처럼 게이지나 타이머 UI에 최적화된 방식이에요.
Fill Method, Fill Origin, Fill Amount 등을 설정해서 이미지의 일부만 보이게 할 수 있어요.
게이지가 줄어드는 효과, 타이머 회전 같은 거 만들 때 자주 씀!
#3단계: Inspector에서 이미지 연결하기
#**{ 타이머(Timer) 시스템 구현 -2- }**
타이머(Timer) 기능을 만드는 스크립트를 다루고 있고, 다음 단계인 퀴즈(Quiz) 시스템과 연결하기 위한 준비
#스크립트 구성
#주요 변수들
[SerializeField] float timeToCompleteQuestion = 30f;
[SerializeField] float timeToShowCorrectAnswer = 10f;
public bool loadNextQuestion;
public bool isAnsweringQuestion = false;
public float fillFraction;
float timerValue;
timeToCompleteQuestion: 문제를 푸는 데 주어진 시간 (30초)
timeToShowCorrectAnswer: 정답을 보여주는 시간 (10초)
fillFraction: UI 타이머 이미지에 표시될 퍼센트 (0~1)
timerValue: 현재 남은 시간
isAnsweringQuestion: 지금 문제를 푸는 중인지 여부
loadNextQuestion: 다음 문제를 보여줘야 할지 여부
#타이머 로직 흐름 이해하기
timerValue -= Time.deltaTime;
Time.deltaTime은 "한 프레임이 걸린 시간"을 의미해요.
즉, timerValue를 매 순간 줄여나가면서 시간이 흘러가는 효과를 내죠.
#1단계: 문제 푸는 중 (isAnsweringQuestion == true)
if (isAnsweringQuestion)
{
if (timerValue > 0)
{
fillFraction = timerValue / timeToCompleteQuestion;
}
else
{
isAnsweringQuestion = false;
timerValue = timeToShowCorrectAnswer;
}
}
타이머가 0보다 크면 남은 시간 / 전체 시간 = 퍼센트로 계산해서 fillFraction에 저장해요.
시간이 다 되면 정답 보여주는 시간으로 전환해요 (timerValue = 10초).
#2단계: 정답 보여주는 중 (isAnsweringQuestion == false)
else
{
if (timerValue > 0)
{
fillFraction = timerValue / timeToShowCorrectAnswer;
}
else
{
isAnsweringQuestion = true;
timerValue = timeToCompleteQuestion;
loadNextQuestion = true;
}
}
타이머가 0보다 크면 fillFraction = 현재 남은 시간 / 10초
시간이 다 되면 다시 문제 푸는 상태로 전환하고 다음 문제를 불러오게 loadNextQuestion = true
#fillFraction이 뭐지?
타이머 이미지는 1에서 0까지 줄어드는 방식으로 채워져 있어요.
그래서 남은 시간 ÷ 전체 시간으로 비율을 구하면 이걸 이미지에 넣어 시각화할 수 있어요.
예:
timerValue = 5, timeToCompleteQuestion = 10 → fillFraction = 0.5
즉, 절반만큼 채워진 상태!
#다른 스크립트(Quiz)와 연결을 위한 훅
public bool loadNextQuestion;
이 변수는 Timer가 “이제 다음 문제 보여줄 시간이야!” 라고 알려주는 신호예요.
Quiz 스크립트에서 이걸 보고 다음 문제를 보여줄 수 있어요.
#CancelTimer 기능
public void CancelTimer()
{
timerValue = 0;
}
퀴즈에서 유저가 정답을 누르면 굳이 시간이 끝날 때까지 기다릴 필요 없겠죠?
이걸 실행하면 바로 시간이 0이 돼서 다음 단계로 넘어갈 수 있어요.
#**{ 타이머(Timer) 시스템 구현 -3- }**
#목표
타이머가 동작하고,
시간에 따라 자동으로 정답이 표시되며,
사용자가 답을 누르면 즉시 처리되고,
다음 문제로 넘어가는 기본 퀴즈 게임 루프 완성!
#`Quiz.cs` 전체 코드
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;
using UnityEngine.UI;
public class Quiz : MonoBehaviour
{
[Header("Questions")]
[SerializeField] TextMeshProUGUI questionText; // 문제 텍스트 UI
[SerializeField] List<QuestionSO> questions = new List<QuestionSO>(); // 문제 리스트
QuestionSO currentQuestion; // 현재 문제
[Header("Answers")]
[SerializeField] GameObject[] answerButtons; // 보기 버튼 배열
int correctAnswerIndex; // 정답 번호
bool hasAnsweredEarly = false; // 사용자가 타이머 끝나기 전에 답했는지 여부
[SerializeField] Sprite defaltAnswerButtonSprite; // 초기 버튼 이미지
[SerializeField] Sprite correctAnswerButtonSprite; // 정답 시 버튼 이미지
[Header("Timer")]
[SerializeField] Image timerImage; // 타이머 이미지
Timer timer; // 타이머 스크립트 참조
void Start()
{
// 타이머 스크립트를 씬에서 찾아서 저장
timer = FindObjectOfType<Timer>();
}
void Update()
{
// 매 프레임마다 타이머 바 UI 채움
timerImage.fillAmount = timer.fillFraction;
// 시간이 다 되었고, 사용자가 답을 안 골랐으면 자동 정답 표시
if (timer.loadNextQuestion)
{
hasAnsweredEarly = false;
GetNextQuestion();
timer.loadNextQuestion = false;
}
else if (!timer.isAnsweringQuestion && !hasAnsweredEarly)
{
DisplayAnswer(-1); // 아무것도 선택 안 한 상태
SetButtonState(false);
}
}
void GetNextQuestion()
{
// 기본 버튼 상태로 초기화
SetButtonState(true);
SetDefaultButtonSprites();
// 무작위로 다음 문제 선택
GetRandomQuestion();
DisplayQuestion();
}
void GetRandomQuestion()
{
int index = Random.Range(0, questions.Count);
currentQuestion = questions[index];
// 나중에는 중복 안 되게 제거하거나 섞는 로직 추가 필요
}
void DisplayQuestion()
{
// 문제 텍스트 표시
questionText.text = currentQuestion.GetQuestion();
// 각 보기 버튼 텍스트 지정
for (int i = 0; i < answerButtons.Length; i++)
{
TextMeshProUGUI buttonText = answerButtons[i].GetComponentInChildren<TextMeshProUGUI>();
buttonText.text = currentQuestion.GetAnswer(i);
}
correctAnswerIndex = currentQuestion.GetCorrectAnswerIndex();
}
public void OnAnswerSelected(int index)
{
hasAnsweredEarly = true;
// 정답/오답 표시
DisplayAnswer(index);
// 버튼 잠금
SetButtonState(false);
// 타이머 강제 종료 (다음 문제로 넘어가기 위한 시간 계산 시작)
timer.CancelTimer();
}
void DisplayAnswer(int index)
{
// 정답 버튼 강조
Image buttonImage;
if (index == correctAnswerIndex)
{
questionText.text = "정답입니다!";
buttonImage = answerButtons[index].GetComponent<Image>();
buttonImage.sprite = correctAnswerButtonSprite;
}
else
{
// 오답일 경우
string correctAnswer = currentQuestion.GetAnswer(correctAnswerIndex);
questionText.text = $"오답입니다! 정답은:\n{correctAnswer}";
// 정답 버튼 색상 변경
buttonImage = answerButtons[correctAnswerIndex].GetComponent<Image>();
buttonImage.sprite = correctAnswerButtonSprite;
}
}
void SetButtonState(bool state)
{
// 버튼을 클릭 가능/불가능하게 설정
foreach (GameObject button in answerButtons)
{
button.GetComponent<Button>().interactable = state;
}
}
void SetDefaultButtonSprites()
{
// 버튼 이미지 초기화
foreach (GameObject button in answerButtons)
{
button.GetComponent<Image>().sprite = defaltAnswerButtonSprite;
}
}
}
#[Header("")] 추가
위와 같이 Quiz 부분에 헤더를 추가해서 변수들을 나눠줄 수 있다
#타이머 이미지 넣어주기
타이머 이미지 오브젝트를 넣어줘서 사용 가능하게 해준다
#1. 타이머 UI 연결
timerImage.fillAmount = timer.fillFraction;
fillAmount는 이미지가 얼마나 채워졌는지를 나타내는 0~1 사이 값.
fillFraction은 Timer.cs에서 계산된 시간 비율을 반환함.
이렇게 하면 타이머처럼 보이는 UI가 완성됨.
#2. 타이머가 끝나면 자동 정답 표시
타이머가 끝나면 timer.isAnsweringQuestion == false가 되고,
사용자가 아직 답을 선택하지 않았다면 DisplayAnswer(-1)로 정답만 보여줌.
hasAnsweredEarly를 체크해서 중복으로 정답을 표시하지 않게 막음.
* -1 을 사용하는 이유?
-1은 "빈 선택"을 나타내는 깔끔한 방식이에요.
이런 식으로 특별한 숫자나 값을 신호로 사용하는 걸 Sentinel Pattern이라고도 해요.
Unity에서 선택지 번호는 일반적으로 배열 인덱스로 다루기 때문에:
선택지가 4개라면 인덱스는 0, 1, 2, 3
그 외의 숫자 (-1)는 정상적인 선택이 아님을 의미함
그래서 -1을 넘기면:
“사용자가 아무 것도 선택하지 않았지만, 정답은 보여줘야 해.” 라는 뜻이 돼요.
#3. 사용자가 답을 고르면
OnAnswerSelected(int index) 함수 호출됨.
사용자가 직접 답했을 때:
hasAnsweredEarly = true로 설정
정답/오답 판별
버튼 잠금
타이머 즉시 중단 (CancelTimer())
#4. 정답 표시 (`DisplayAnswer`)
클릭한 인덱스와 정답 인덱스를 비교
정답이면 "정답입니다!" 표시
오답이면 "오답입니다! 정답은 ~~" 표시
정답 버튼을 시각적으로 강조함 (이미지 변경)
#5. 버튼 상태 처리
SetButtonState(true/false)는 모든 보기 버튼의 interactable 값을 제어
SetDefaultButtonSprites()는 모든 보기 버튼의 이미지를 초기화 (다음 문제 대비)
#6. 다음 문제로 넘어가기
timer.loadNextQuestion == true일 때 GetNextQuestion() 호출
문제를 새로 불러오고 UI 갱신함
loadNextQuestion = false로 다시 초기화해서 반복 가능
#핵심 정리
Timer.cs는 시간 흐름을 계산하고, Quiz.cs는 그 흐름에 따라 문제 표시와 정답 판단을 함.
모든 버튼/타이머/UI는 유기적으로 연결되어 있음.
사용자의 행동(버튼 클릭)과 시스템의 행동(시간 종료)을 각각 감지해서 대응함.
#Timer.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Timer : MonoBehaviour
{
[SerializeField] float timeToCompleteQuestion = 30f; // 문제를 푸는 시간
[SerializeField] float timeToShowCorrectAnswer = 10f; // 정답 보여주는 시간
public bool isAnsweringQuestion = false; // 문제 푸는 중인지
public float fillFraction; // 타이머 UI용 비율 (0~1)
public bool loadNextQuestion; // 다음 문제로 넘어가야 하는지
float timerValue; // 실제 시간 카운터
void Update()
{
UpdateTimer();
}
public void CancelTimer()
{
// 사용자가 답을 일찍 선택하면 타이머 강제 종료용
timerValue = 0;
}
void UpdateTimer()
{
timerValue -= Time.deltaTime; // 프레임마다 시간 감소
if (isAnsweringQuestion)
{
// 문제 푸는 시간 중에는 타이머 UI에 현재 남은 시간 비율 계산
fillFraction = timerValue / timeToCompleteQuestion;
if (timerValue <= 0)
{
// 시간이 다 되면 정답 보여주기 모드로 전환
isAnsweringQuestion = false;
timerValue = timeToShowCorrectAnswer;
}
}
else
{
// 정답 보여주는 시간일 때도 fillFraction 계산 (예: 타이머 역방향)
fillFraction = timerValue / timeToShowCorrectAnswer;
if (timerValue <= 0)
{
// 정답 보여주기 끝났으므로 다음 문제로 넘어가기
isAnsweringQuestion = true;
timerValue = timeToCompleteQuestion;
loadNextQuestion = true; // Quiz.cs가 감지할 수 있도록 true 설정
}
}
}
}
#`Timer.cs` 와 `Quiz.cs`의 연결 관계
#연결 구조 요약
#1. 새 문제 시작
isAnsweringQuestion = true
timerValue = 30초
fillFraction = 1 → 0으로 줄어듬
Quiz.cs는 매 프레임 fillFraction으로 타이머 UI 바를 그려줌
#2. 시간 초과됨
timerValue <= 0이 되면:
isAnsweringQuestion = false
timerValue = timeToShowCorrectAnswer (예: 10초)
이제 사용자가 정답을 못 고른 상태라면 Quiz.cs가 정답을 자동으로 보여줌
#3. 정답 보여주는 시간 흐름
이때도 fillFraction이 줄어들며, 타이머 UI가 역방향처럼 움직일 수 있음
정답 보여주는 동안 사용자는 버튼을 누를 수 없음
#4. 다시 다음 문제 시작
timerValue <= 0 되면:
isAnsweringQuestion = true
timerValue = 30초
loadNextQuestion = true로 설정
Quiz.cs는 loadNextQuestion == true일 때 다음 문제를 불러옴 → 다시 처음으로 반복
#5. 사용자가 일찍 답했을 경우
Quiz.cs → OnAnswerSelected()에서:
hasAnsweredEarly = true
timer.CancelTimer() 실행 → timerValue = 0
이로 인해 바로 UpdateTimer()가 timerValue <= 0 조건에 들어가고
정답 보여주는 시간으로 전환됨
#핵심 포인트 정리
Timer.cs는 "타이머의 상태"만 관리하는 백엔드 컨트롤러
Quiz.cs는 그 상태에 맞춰 UI를 보여주고 문제를 진행하는 프론트엔드 매니저
#{ 리스트(List)를 사용한 퀴즈 문제 관리 }
#1. ✅ **퀴즈에 여러 문제 추가하기**
기존에는 하나의 질문만 표시했지만,
이제 여러 개의 문제를 다루기 위해 리스트(List) 를 사용할 거예요.
#2. 📦 리스트(List)란?
배열(array) 과 비슷하게 여러 데이터를 묶어서 저장할 수 있어요.
하지만 배열은 크기가 고정되어 있는 반면, 리스트는 크기를 마음대로 늘리거나 줄일 수 있어요.
리스트 안의 각 요소는 인덱스 번호로 접근하며, 0번부터 시작합니다.
#3. 🧾 리스트 만드는 방법
List<int> oddNumbers = new List<int>();
List<데이터타입> 형태로 선언
new List<>()로 초기화
#4. 🛠 리스트에서 자주 사용하는 메서드
Count → 요소 개수 확인
Contains(item) → 해당 요소가 있는지 확인
Add(item) → 요소 추가
Remove(item) → 특정 요소 제거
RemoveAt(index) → 인덱스로 요소 제거
Clear() → 모든 요소 삭제
#5. 🧩 퀴즈 코드 변경 - 준비 작업
#5.1 기존 질문 변수 이름 바꾸기
기존 변수 이름이 헷갈릴 수 있으므로 question → currentQuestion 으로 변경
이름을 바꾸면 관련된 코드 전체를 함께 바꿔줘야 함
#5.2 새로운 질문 리스트 만들기
[SerializeField] List<QuestionSo> questions = new List<QuestionSo>();
SerializedField를 붙여 인스펙터에서 직접 질문들을 드래그할 수 있도록 설정
#6. 🎲 랜덤 질문 선택 구현
#6.1 새로운 메서드 만들기
// 무작위로 하나의 질문을 선택하고, 그 질문을 목록에서 제거하는 메서드
void GetRandomQuestion()
{
// 1. 0부터 questions 리스트의 개수(Count) - 1 사이에서 무작위로 숫자 하나를 뽑습니다.
// 이 숫자는 리스트의 인덱스로 사용됩니다.
int index = Random.Range(0, questions.Count);
// 2. 위에서 선택한 인덱스를 사용해서, 해당 위치에 있는 질문을 currentQuestion 변수에 저장합니다.
currentQuestion = questions[index];
// 3. 만약 이 질문이 리스트 안에 실제로 존재한다면 (대부분의 경우 참입니다),
// 리스트에서 이 질문을 제거합니다. 이렇게 하면 같은 질문이 다시 나오지 않게 됩니다.
if (questions.Contains(currentQuestion))
{
questions.Remove(currentQuestion);
}
}
리스트에서 무작위로 하나의 질문을 선택해서 currentQuestion에 저장
이미 사용한 문제는 Remove로 제거하여 중복 방지
#7. 🚨 버그 수정 ①: 첫 문제에서 두 개 제거되는 문제
Start() 함수에서 GetNextQuestion()이 중복 호출되고 있었음
해결 방법: Start() 안의 GetNextQuestion() 호출 코드를 삭제
#8. ⚠️ 버그 수정 ②: 리스트가 비었을 때 오류
리스트가 비었는데 질문을 또 꺼내려고 해서 오류 발생
void GetNextQuestion()
{
if (questions.Count > 0)
{
SetButtonState(true);
SetDefaultButtonSprites();
GetRandomQuestion();
DisplayQuestion();
}
}
questions.Count > 0 조건을 먼저 확인하여 방지
#9. 🧪 테스트 방법
인스펙터 창에서 질문 리스트에 문제들을 드래그하여 추가
플레이 모드에서 질문이 무작위로 잘 나오고, 중복되지 않는지 확인
리스트에 문제 넣어주기
#{ 유저의 퀴즈 진행 상황을 추적하기 위한 점수 시스템 구현 }
#1. **캔버스에 스코어 텍스트 추가하기**
목표: 유저가 퀴즈를 진행하면서 점수를 확인할 수 있도록 화면에 점수 텍스트(UI)를 보여줌.
위치: Quiz Canvas의 왼쪽 상단.
방법:
Canvas 안에서 우클릭 → UI → Text - TextMeshPro 선택.
새로 생성된 텍스트를 왼쪽 상단으로 끌어서 위치 조정.
이름을 Score Text로 변경.
텍스트 내용은 "Score: 100%" 같은 예시로 설정.
폰트 크기를 약 48로 설정하고 정렬도 적절히 조절.
UI가 잘 보이도록 기존 UI와 비교하면서 배치.
#2. **ScoreKeeper 스크립트 생성 및 설정**
역할: 유저가 얼마나 많은 문제를 보고, 몇 개를 맞혔는지 추적하고 백분율 점수를 계산.
설정 순서:
Scripts 폴더에서 우클릭 → Create → C# Script 선택 후 이름을 ScoreKeeper로 설정.
Hierarchy에서 Empty Object 생성 → 이름을 Score Keeper로 변경.
새로 만든 ScoreKeeper 스크립트를 이 오브젝트에 드래그 앤 드롭해서 붙임.
#3. **ScoreKeeper 스크립트 내용 작성**
#필드 선언:
int correctAnswers = 0;
int questionsSeen = 0;
correctAnswers: 유저가 맞힌 문제 수
questionsSeen: 유저가 본 전체 문제 수
#Getter 메서드 생성:
public int GetCorrectAnswers() { return correctAnswers; }
public int GetQuestionsSeen() { return questionsSeen; }
#Setter 메서드 생성 (값 1씩 증가):
public void IncrementCorrectAnswers() { correctAnswers++; }
public void IncrementQuestionsSeen() { questionsSeen++; }
#점수 계산 메서드:
public float CalculateScore()
{
return Mathf.RoundToInt(correctAnswers / (float)questionsSeen * 100);
}
이 함수는 **맞힌 문제 수(correctAnswers)**를 **전체 본 문제 수(questionsSeen)**로 나눠서 정확도(%)를 계산하는 함수야.
(float)questionsSeen 이렇게 float로 바꾸는 이유는, 정수끼리 나누면 소수점이 사라지기 때문이야.
예: 3 / 5 → 0 이 되는데, 3 / 5.0f → 0.6 이 돼.
* 100은 0.6처럼 나온 값을 60%처럼 보이게 백분율로 바꾸는 거고,
Mathf.RoundToInt()는 그 결과를 반올림해서 정수로 만드는 함수야.
(예: 66.7 → 67)
#4. **Quiz 스크립트에 점수 시스템 연동**
#필드 추가:
[Header("Score")]
[SerializeField] TextMeshProUGUI scoreText;
ScoreKeeper scoreKeeper;
#Start() 메서드에서 ScoreKeeper 참조 설정:
scoreKeeper = FindObjectOfType<ScoreKeeper>();
#문제를 보자마자 Seen 수 증가시키기:
scoreKeeper.IncrementQuestionsSeen();
#정답 맞혔을 경우 정답 수 증가시키기:
scoreKeeper.IncrementCorrectAnswers();
#점수 텍스트 업데이트:
scoreText.text = "점수: " + scoreKeeper.CalculateScore() + "%";
선택한 답변을 처리하는 OnAnswerSelected() 메서드에 넣음.
유저가 어떤 문제에 어떻게 답했는지 반영되도록 즉시 업데이트.
#5. **유니티에서 연결 마무리**
Canvas로 돌아가기:
Score Text UI 요소를 Quiz 스크립트의 scoreText 필드에 드래그해서 연결.
게임 실행 후 점수 확인:
문제를 맞히면 점수가 올라가고,
틀리면 점수가 내려감.
실시간으로 "점수: 50%" 같은 형태로 표시됨.
#{ 슬라이더를 이용한 진행 표시줄 만들기 }
#1. **슬라이더 추가**
캔버스에서 우클릭 → UI → Slider 선택
너무 작으면 크기를 키우고 보이기 좋은 위치로 옮긴다.
#2. **슬라이더 구조 이해하기**
슬라이더는 생각보다 구조가 복잡함. 아래 3가지 핵심 오브젝트가 있음:
Background: 슬라이더의 배경
Fill: 채워지는 부분
Handle: 슬라이더를 드래그할 수 있는 조절기 (꼭 필요하지 않음)
슬라이더 구성 요소 설명
Fill Rect, Handle Rect: 슬라이더 자식 오브젝트들과 연결됨
Direction: 슬라이더 방향 설정 (Left To Right 추천)
Min Value / Max Value: 슬라이더의 최소/최대 범위 설정 (기본은 0 ~ 1)
Whole Numbers: 정수만 사용하고 싶을 때 켜기 (퀴즈 문제 수 카운팅에 적합)
On Value Changed: 슬라이더 값이 바뀔 때 이벤트 실행 가능 (이 강의에선 사용 안 함)
#3. **슬라이더 꾸미기**
슬라이더 위치 조절 및 크기 조정
예: 아래쪽에 길쭉하고 얇게 배치
색상 변경
배경은 흰색 + 투명도 0.5
Fill은 파란색 (혹은 네온 스프라이트)
Handle 사용 여부
필요 없으면 꺼도 됨
#4. **슬라이더를 코드에 연결**
#📌 Quiz.cs에 다음 내용 추가:
[Header("ProgressBar")]
[SerializeField] Slider progressBar;
public bool isComplete;
#`Start()`에서 초기 설정:
progressBar.maxValue = questions.Count;
progressBar.value = 0;
#문제 넘길 때 슬라이더 값 증가:
progressBar.value++;
#마지막 문제 후 게임 완료 상태 체크:
if(progressBar.value == progressBar.maxValue) {
isComplete = true;
}
#{ 축하 메시지 UI 만들기 }
#슬라이더 버그 수정
슬라이더가 유저와 상호작용하지 않도록 설정
QuizCanvas에서 슬라이더를 선택함.
슬라이더 컴포넌트의 Interactable 옵션을 끄기.
이렇게 하면 유저가 슬라이더를 직접 조작할 수 없고, 오직 코드로만 값이 바뀜.
#Win Screen UI 만들기
1. 기존 퀴즈 캔버스 숨기기
핵심 게임 화면이 끝났으므로, QuizCanvas를 숨김 처리함.
2. 새 캔버스 생성
계층 뷰에서 우클릭 → UI → Canvas 선택.
이름을 WinCanvas로 변경.
3. 정렬 순서 설정
WinCanvas의 Canvas 컴포넌트에서 Sort Order를 2로 변경.
항상 다른 캔버스(퀴즈/배경) 위에 보이도록 함.
4. 축하 메시지 텍스트 추가
WinCanvas 우클릭 → UI → Text - TextMeshPro 추가.
위치 조정 후 텍스트를 "축하 합니다. 점수: 0%"로 설정.
폰트 크기를 65, 정렬을 중앙/중앙으로 설정.
이름을 FinalScoreText로 변경.
5. 다시 플레이 버튼 추가
WinCanvas 우클릭 → UI → Button - TextMeshPro 추가.
이름을 ReplayButton으로 변경.
위치 조정 후 버튼에 사용될 스프라이트 이미지(네온 오렌지 스퀘어)로 설정.
버튼의 텍스트를 "Play Again"으로 변경.
폰트 크기 65, 텍스트 색상은 검은색으로 설정.
6. 불필요한 캔버스 삭제
PrevisualizationCanvas는 더 이상 사용하지 않으므로 삭제.
7. 계층 정리
WinCanvas를 QuizCanvas 바로 아래로 위치시켜 계층을 깔끔하게 정리함.
#EndScreen 스크립트 작성
1. 스크립트 생성
Scripts 폴더에서 새 C# 스크립트 생성 → 이름: EndScreen.cs.
WinCanvas 오브젝트에 스크립트 부착.
2. 필요 없는 코드 제거
Update() 메서드 및 주석 등 불필요한 부분 삭제.
3. 필드 선언
using TMPro;
public class EndScreen : MonoBehaviour
{
[SerializeField] TextMeshProUGUI finalScoreText;
ScoreKeeper scoreKeeper;
}
4. ScoreKeeper 참조
void Start()
{
scoreKeeper = FindObjectOfType<ScoreKeeper>();
}
5. 최종 점수 출력 메서드 작성
public void ShowFinalScore()
{
finalScoreText.text = "축하 합니다. 최종 점수:\n" + scoreKeeper.CalculateScore() + "%";
}
\n으로 줄바꿈 처리.
긴 줄을 두 줄로 나누어도 C#에서는 무관함 (단, 문자열을 더하는 위치에서 나눌 것).
6. 스크립트 저장 및 Unity로 돌아가기
WinCanvas에서 FinalScoreText 를 EndScreen.cs 에 드래그하여 연결.
#{ 게임 매니저로 퀴즈와 종료 화면 전환하기 }
#1. 게임 매니저 오브젝트 만들기
**계층 구조(Hierarchy)**에서 우클릭 → Empty Object 생성
이름을 GameManager로 변경
이 오브젝트는 게임 전체의 흐름을 관리하는 역할을 합니다.
#2. GameManager 스크립트 생성 및 연결
Scripts 폴더에서 우클릭 → C# Script → 이름을 GameManager로
아이콘이 다르게 보이는 이유: Unity에서 자주 쓰는 스크립트이기 때문
생성한 GameManager 스크립트를 GameManager 오브젝트에 드래그해서 연결
이제 이 스크립트는 오브젝트의 기능을 담당합니다.
#3. 스크립트 구조 설정
Quiz quiz;
EndScreen endScreen;
퀴즈 화면과 종료 화면을 스크립트에서 제어하기 위해 변수로 선언합니다.
void Awake() {
quiz = FindObjectOfType<Quiz>();
endScreen = FindObjectOfType<EndScreen>();
}
게임 시작 시 Quiz와 EndScreen 오브젝트를 찾아 변수에 할당
Unity의 스크립트 실행 순서상, Awake는 Start보다 먼저 실행되므로 초기화에 적합
Awake()는 유니티에서 게임이 시작될 때 가장 먼저 실행되는 함수 중 하나입니다.
FindObjectOfType을 통해 씬에 있는 Quiz와 EndScreen 오브젝트를 찾아 변수에 저장합니다.
void Start() {
quiz.gameObject.SetActive(true);
endScreen.gameObject.SetActive(false);
}
게임이 시작되면 퀴즈 화면을 보이게 하고, 종료 화면은 숨깁니다.
SetActive(true) → 활성화 / SetActive(false) → 비활성화
#4. 게임이 끝났을 때 화면 전환
void Update() {
if (quiz.isComplete) {
quiz.gameObject.SetActive(false);
endScreen.gameObject.SetActive(true);
endScreen.ShowFinalScore();
}
}
매 프레임마다 게임이 완료되었는지 확인합니다.
isComplete가 true이면,
퀴즈 화면을 숨기고,
종료 화면을 보이게 하고,
최종 점수를 보여줍니다.
#5. 다시 플레이하기 기능
using UnityEngine.SceneManagement;
public void OnReplayLevel() {
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
}
다시 플레이 버튼 클릭 시 현재 씬을 다시 로드해서 게임을 재시작
Unity에서 SceneManager를 사용하기 위해 상단에 using UnityEngine.SceneManagement; 추가
SceneManager.LoadScene()을 사용해 현재 씬을 다시 불러옵니다.
buildIndex는 씬 목록에서 씬의 순번입니다.
게임이 초기화되어 다시 시작됩니다.
#6. 종료 화면 버튼 연결
종료 화면에서 "다시 플레이" 버튼 선택
OnClick() 이벤트에 GameManager 오브젝트를 연결
드롭다운에서 GameManager.OnReplayLevel() 선택
Unity에서 다시 플레이 버튼을 선택합니다.
OnClick 이벤트 항목에 GameManager 오브젝트를 드래그해서 넣고, 드롭다운에서 OnReplayLevel()을 선택합니다.
이제 버튼을 누르면 게임이 다시 시작됩니다.
#7. 점수 표시 문제 해결
endScreen.ShowFinalScore() 호출이 빠졌기 때문에 점수가 안 보였던 것 → Update()에서 호출하도록 수정
Update()에서 종료 화면을 켤 때 endScreen.ShowFinalScore()를 꼭 호출해야 합니다.
이 메서드를 호출해야 최종 점수가 업데이트되어 화면에 표시됩니다.
#8. NullReferenceException 에러 해결
초기화 코드를 Awake()에서 실행하면 실행 순서 문제를 피할 수 있음
Start()보다 Awake()가 먼저 실행되므로 안정적인 초기화 가능
#9. 퀴즈 스크립트 버그 수정
게임 시작 직후 Update() 루프에서 정답이 없는 상태로 정답을 표시하려 해서 경고 발생
해결 방법:
게임 시작 시 문제 정보가 없어서 null 에러가 발생할 수 있습니다.
해결 방법:
hasAnsweredEarly라는 불리언 변수를 true로 초기화해서, 처음에 정답을 표시하려고 하지 않도록 만듭니다.
bool hasAnsweredEarly = true; // 선언 시 true로 초기화
처음에는 정답을 표시하지 않도록 설정
#10. 마지막 문제에서 바로 종료화면으로 넘어가는 문제 해결
isComplete = true 설정 위치를 수정
답을 선택할 때가 아니라, 마지막 문제 이후 Update()에서 설정
이후에 return 추가해서 불필요한 실행 방지