상세한 게임 진행 메커니즘은 아래와 같다.
핵심 메커니즘 구현 이전에, 곁가지가 되는 타이머와 점수 계산기를 먼저 만들어둔다.
// 타이머 스크립트
// Timer.cs
// 문제 풀이 시간, 정답 표시 시간
[SerializeField] float timeToCompleteQuestion = 30f;
[SerializeField] float timeToShowCorrectAnswer = 10f;
// 정답 표시 후 다음 문제로 넘어가는 체크를 위한 bool 변수
public bool loadNextQuestion;
// 제한 시간 변수
float timerValue;
// 문제 풀이 중인지 정답 확인 중인지 구분을 위한 bool 변수
public bool isAnsweringQuestion = false;
// 타이머 이미지 조절용 변수
public float fillFraction;
void Update()
{
UpdateTimer();
}
// 정답 선택 등의 경우에 타이머 종료를 위한 메소드
public void CancelTimer()
{
timerValue = 0;
}
// 타이머 기능 메소드
void UpdateTimer()
{
timerValue -= Time.deltaTime;
// 문제 풀이 중인 경우
if (isAnsweringQuestion)
{
if (timerValue > 0)
{
// 타이머 = 문제 풀이 시간
fillFraction = timerValue / timeToCompleteQuestion;
}
// 문제 풀이 시간 종료 시
else
{
// 문제 풀이중인지 체크용 bool값 변경
isAnsweringQuestion = false;
// 정답 표시 시간으로 타이머 변경
timerValue = timeToShowCorrectAnswer;
}
}
// 정답 표시 중인 경우
else
{
if (timerValue > 0)
{
// 타이머 = 정답 표시 시간
fillFraction = timerValue / timeToShowCorrectAnswer;
}
// 정답 표시 시간 종료 시
else
{
// 문제 풀이 체크 bool 값 변경
isAnsweringQuestion = true;
// 문제 풀이 시간으로 타이머 변경
timerValue = timeToCompleteQuestion;
// 다음 문제 호출용 bool값 변경
loadNextQuestion = true;
}
}
}
// 점수계산기 스크립트
// ScoreKeeper.cs
// 맞힌 정답 수
int correctAnswers = 0;
// 이때까지 푼 문제 수
int questionsSeen = 0;
// 맞힌 정답 수 반환 메소드
public int GetCorrectAnswerIndex()
{
return correctAnswers;
}
// 맞힌 정답 수 증가 메소드
public void IncrementCorrectAnswers()
{
correctAnswers++;
}
// 이때가지 푼 문제수 반환 메소드
public int GetQuestionsSeen()
{
return questionsSeen;
}
// 이때까지 푼 문제수 증가 메소드
public void IncrementQuestionsSeen()
{
questionsSeen++;
}
// 현재 점수 계산 메소드
public int CalculateScore()
{
// (맞힌 문제 수/이때까지 푼 문제 수) 백분율 반환
return Mathf.RoundToInt(correctAnswers / (float)questionsSeen * 100);
}
이렇게 핵심 메커니즘 구현에 필요한 타이머, 점수 계산기 스크립트를 모두 작성했다. 이제 Quiz 스크립트를 작성할 차례다.
코드가 비교적 긴 편이라, 각 메소드별로 나누어서 작성한다.
void DisplayAnswer(int index)
{
// 버튼 이미지 선언
Image buttonImage;
// 선택한 버튼이 정답인 경우 처리
if (index == currentQuestion.GetCorrectAnswerIndex())
{
questionText.text = "Correct!";
// 선택한 버튼 이미지 변경
buttonImage = answerButtons[index].GetComponent<Image>();
buttonImage.sprite = correctAnswerSprite;
// 정답 맞힌 수 증가
scoreKeeper.IncrementCorrectAnswers();
}
// 선택한 버튼이 오답인 경우 처리
else
{
// 정답 인덱스
correctAnswerIndex = currentQuestion.GetCorrectAnswerIndex();
// 정답 스트링
string correctAnswer = currentQuestion.GetAnswer(correctAnswerIndex);
// 정답 안내 문구 출력
questionText.text = "Wrong, the correct answer was;\n" + correctAnswer;
// 정답 선택지의 버튼 이미지 변경
buttonImage = answerButtons[correctAnswerIndex].GetComponent<Image>();
buttonImage.sprite = correctAnswerSprite;
}
}
public void OnAnswerSelected(int index)
{
// 시간초과 전에 선택했으므로 bool값 수정
hasAnsweredEarly = true;
// 정답 체크
DisplayAnswer(index);
// 버튼 비활성화
SetButtonState(false);
// 타이머 스크립트의 초기화 메소드 호출
timer.CancelTimer();
// 점수 갱신
scoreText.text = "Score:" + scoreKeeper.CalculateScore() + " %";
}
void GetRandomQuestion()
{
int index = Random.Range(0, questions.Count);
currentQuestion = questions[index];
// 받아온 랜덤 문제 남은 문제 목록에서 삭제
if (questions.Contains(currentQuestion))
{
questions.Remove(currentQuestion);
}
}
void SetButtonState(bool state)
{
for (int i = 0; i < answerButtons.Length; i++)
{
Button button = answerButtons[i].GetComponent<Button>();
button.interactable = state;
}
}
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);
}
}
void SetDefaultButtonSprites()
{
Image buttonImage;
for (int i = 0; i < answerButtons.Length; i++)
{
buttonImage = answerButtons[i].GetComponent<Image>();
buttonImage.sprite = defalutAnswerSprite;
}
}
void GetNextQuestion()
{
if (questions.Count > 0)
{
// 버튼 활성화
SetButtonState(true);
// 버튼 이미지 초기화
SetDefaultButtonSprites();
// 랜덤으로 문제 받아오기
GetRandomQuestion();
// 선택지 표시
DisplayQuestion();
// 진행도 증가
progressBar.value++;
// 이때까지 푼 문제수 증가
scoreKeeper.IncrementQuestionsSeen();
}
}
이제, 위 메소드들을 활용하여 Quiz 스크립트를 작성한다.
// 직렬화 값들을 한눈에 볼 수 있도록 Header로 정리
[Header("Questions")]
[SerializeField] TextMeshProUGUI questionText;
[SerializeField] List<QuestionSO> questions = new List<QuestionSO>();
QuestionSO currentQuestion;
[Header("Answers")]
[SerializeField] GameObject[] answerButtons;
int correctAnswerIndex;
// 시간 초과 전에 정답을 고른 경우 체크용 bool
bool hasAnsweredEarly = true;
[Header("Button Colors")]
[SerializeField] Sprite defalutAnswerSprite;
[SerializeField] Sprite correctAnswerSprite;
[Header("Timer")]
[SerializeField] Image timerImage;
Timer timer;
[Header("Scoring")]
[SerializeField] TextMeshProUGUI scoreText;
ScoreKeeper scoreKeeper;
[Header("ProgressBar")]
[SerializeField] Slider progressBar;
// 게임 종료 여부 체크용 bool
public bool isComplete;
void Awake()
{
// 변수 초기화 작업
timer = FindObjectOfType<Timer>();
scoreKeeper = FindObjectOfType<ScoreKeeper>();
progressBar.maxValue = questions.Count;
progressBar.value = 0;
}
void Update()
{
// 현재 남은 제한시간에 맞추어 타이머 표시
timerImage.fillAmount = timer.fillFraction;
// 정답 확인 시간 만료 시
if (timer.loadNextQuestion)
{
// 진행도가 가득 찬 경우
if (progressBar.value == progressBar.maxValue)
{
// 완료 bool 값 변경
isComplete = true;
return;
}
// 정답 선택 확인 bool 값 변경
hasAnsweredEarly = false;
// 다음 문제로
GetNextQuestion();
// 다음 문제 호출 체크용 bool 값 변경
timer.loadNextQuestion = false;
}
// 진행도가 가득 차지 않은 경우
// 타이머 만료까지 정답을 고르지 않은 경우
else if (!hasAnsweredEarly && !timer.isAnsweringQuestion)
{
DisplayAnswer(-1);
SetButtonState(false);
}
}
이렇게 게임의 핵심 메커니즘 구현을 마쳤다.