During the challenges I implemented the timer differently. I thought it was better to have the image controlled by the timer.cs module, where time is being tracked, rather than in quiz.cs as he did.
The only thing needing to be checked is if we are answering a question and it needs to be able to tell quiz.cs when the timer has expired for the question and when to get the next question after the timer for the answer has expired. I think this resulted in somewhat simpler code with less information being kept in variables, so I wanted to share it so everyone can see a slightly different way to do the same thing. It also turns off the image for the timer at the end.
The code is fairly well commented (I always do that so it makes sense to me when I go back to see how I did it - good practice for everyone!). In quiz.cs you will see that Update() is commented out by removing the trailing ‘/’ on what would otherwise be ‘***/’, which is a comment style for functions (methods) I have used ever since I started programming in C over 30 years ago. Works for Java and C# just as well as for C/C++, so I still use it to document what each function/method is for and what it does.
Enjoy!
quiz.cs:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;
using UnityEngine.UI;
public class Quiz : MonoBehaviour
{
[Header(“Questions”)]
[SerializeField]
List questions;
[SerializeField]
TextMeshProUGUI questionText;
QuestionSO currentQuestion;
[Header(“Answers”)]
[SerializeField]
GameObject answerButtons;
int correctAnswerIndex;
[Header(“Button Colors”)]
[SerializeField]
Sprite defaultAnswerSprite;
[SerializeField]
Sprite correctAnswerSprite;
[Header(“Scoring”)]
[SerializeField]
TextMeshProUGUI scoreText;
ScoreKeeper scoreKeeper;
bool buttonsEnabled = true;
Timer timer;
/***
* Start is called before the first frame update.
***/
void Start()
{
timer = FindObjectOfType<Timer>();
scoreKeeper = FindObjectOfType<ScoreKeeper>();
GetNextQuestion();
DisplayScore();
} // Start()
/***
* Update is called once per frame.
***
void Update()
{
} // Update()
/***
* DisplayQuestion is called when we are displaying a new currentQuestion and answers.
* It also enables the buttons so they can be clicked by setting buttonsEnabled to true.
***/
void DisplayQuestion()
{
questionText.text = currentQuestion.GetQuestion();
correctAnswerIndex = currentQuestion.GetCorrectAnswerIndex();
for (int i = 0; i < answerButtons.Length; i++)
{ // Change the text for all the buttons
TextMeshProUGUI buttonText;
buttonText = answerButtons[i].GetComponentInChildren<TextMeshProUGUI>();
buttonText.text = currentQuestion.GetAnswer(i);
} // for
buttonsEnabled = true;
} // DisplayQuestion()
/***
* GetNextQuestion is called when we get the next currentQuestion and answers.
***/
public void GetNextQuestion()
{
Image buttonImage = answerButtons[correctAnswerIndex].GetComponent<Image>();
buttonImage.sprite = defaultAnswerSprite; // Do this before we change the Question and Answer info
// Change Question and Answer info here
GetRandomQuestion();
if (currentQuestion == null)
{ // We have finished all the questions, so turn off the timer
timer.CancelTimer();
} // if
else
{ // We have a new currentQuestion to display
DisplayQuestion();
scoreKeeper.IncrementQuestionsSeen();
} // else
} // GetNextQuestion()
/***
* GetRandomQuestion is called to get a random question from the list and will
* remove the question from the list. It the list is empty it sets currentQuestion
* to null.
***/
void GetRandomQuestion()
{
if (questions.Count == 0)
{ // We have finished all the questions, so show that a null for currentQuestion
currentQuestion = null;
} // if
else
{
int index = Random.Range(0, questions.Count - 1);
currentQuestion = questions[index];
questions.RemoveAt(index);
} // else
} // GetRandomQuestion()
/***
* OnAnswerSelected is called when an answer has been selected by the player. It lets
* the player know if they selected the correct answer or what the correct answer should be.
***/
public void OnAnswerSelected(int index)
{
if (buttonsEnabled)
{
if (index == correctAnswerIndex)
{
questionText.text = "Correct!";
scoreKeeper.IncrementCorrectAnswers();
} // if
else
{
questionText.text = "Incorrect! The correct answer is:\n" + currentQuestion.GetAnswer(correctAnswerIndex);
} // else
ShowAnswer();
} // if
} // OnAnswerSelected(int index)
/***
* AreButtonsEnabled is called from outside to see if the state has changed.
***/
public bool AreButtonsEnabled()
{
return buttonsEnabled;
} // bool AreButtonsEnabled()
/***
* TimerExpired is called from outside to say the timer has expired and we should show the answer and disable the buttons.
***/
public void TimerExpired()
{
ShowAnswer();
questionText.text = "Time expired! The correct answer is:\n" + currentQuestion.GetAnswer(correctAnswerIndex);
} // TimerExpired()
/***
* ShowAnswer is called from several places to show what the correct answer is. It also disables the buttons.
***/
void ShowAnswer()
{
Image buttonImage = answerButtons[correctAnswerIndex].GetComponent<Image>();
buttonImage.sprite = correctAnswerSprite;
buttonsEnabled = false;
DisplayScore();
} // ShowAnswer()
/***
* DisplayScore is called from more than one place, so it was made a method.
***/
void DisplayScore()
{
scoreText.text = "Score: " + scoreKeeper.CalculateScore().ToString() + "%";
} // DisplayScore()
} // class Quiz
timer.cs:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class Timer : MonoBehaviour
{
[SerializeField]
Image timerImage;
[SerializeField]
Canvas quizCanvas;
[SerializeField]
float timeToAnswer = 30f;
[SerializeField]
float timeToShowAnswer = 10f;
float timerValue;
float startingTimerValue;
bool isAnsweringQuestion = true;
bool timerRunning = true;
Quiz quiz;
/***
* Start is called before the first frame update.
***/
void Start()
{
quiz = quizCanvas.GetComponent<Quiz>();
timerValue = startingTimerValue = timeToAnswer;
} // Start()
/***
* Update is called once per frame.
***/
void Update()
{
UpdateTimer();
} // Update()
/***
* CancelTimer is called from the Quiz script when the we try to fetch a question
* and there are no more left, so we turn off the timer and hide the image.
***/
public void CancelTimer()
{
timerRunning = false;
} // CancelTimer()
/***
* UpdateTimer is called to update the timer image based on how many seconds are left.
***/
void UpdateTimer()
{
if (timerRunning)
{ // Only need to do this if the timer is running
timerValue -= Time.deltaTime;
if (timerValue <= 0f || quiz.AreButtonsEnabled() != isAnsweringQuestion)
{
if (isAnsweringQuestion)
{ // Show the correct answer and start the timer for how long to show it
if (timerValue <= 0f)
quiz.TimerExpired(); // Only Report timer expired if <= 0
timerValue = timeToShowAnswer;
} // if
else
{ // We need to get the next currentQuestion and start the timer to answer it
timerValue = timeToAnswer;
quiz.GetNextQuestion();
} // else
isAnsweringQuestion = quiz.AreButtonsEnabled(); // The state has changed
startingTimerValue = timerValue;
} // if
} // if
else
{ // The time is no longer running so this will set timeImage.fillAmount to 0 to hide the image
timerValue = 0f;
} // else
timerImage.fillAmount = timerValue / startingTimerValue;
if (timerImage.fillAmount < 0)
timerImage.fillAmount = 0f;
} // UpdateTimer()
} // class Timer