-
Notifications
You must be signed in to change notification settings - Fork 0
/
QuizManager.cs
386 lines (337 loc) · 13.3 KB
/
QuizManager.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml;
using System.Xml.Serialization;
using UnityEngine;
public class QuizManager : MonoBehaviour
{
[SerializeField]
List<QuizButton> buttons;
[SerializeField]
QuizMonitor monitor;
[SerializeField]
QuizLivesIndicator livesIndicator;
[SerializeField]
List<QuizScenario> scenarios;
[SerializeField]
SceneChanges sceneChanges;
List<QuizQuestion> remainingQuestions;
List<QuizQuestion> answeredCorrectly;
List<QuizQuestion> answeredIncorrectly;
QuizQuestion currentQuestion;
QuizScenario currentScenario;
float score = 500;
QuizState state = QuizState.Asleep;
// Configuration variables.
readonly float delayBeforeRevealAnswer = 1.0f; // Delay after submitting your answer and before revealing the correct one.
readonly float delayBeforeAward = 0.2f; // Delay after revealing the correct answer and before awarding points.
readonly float delayBeforeNextQuestion = 0.7f; // Delay after awarding points (or not) and before loading the next question.
readonly float delayBeforeLoadMenu = 5.0f; // Delay after revealing final score and loading the main menu.
readonly float scoreDrainPerSecond = 5.0f; // Amount of points drained per second, encouraging the player to answer as fast as possible.
readonly int maxIncorrect = 2; // Maximum allowed incorrect answers. Another incorrect one after this results in a game over. Default is 2.
// Start is called before the first frame update.
void Start()
{
// TODO: Add a start button to the scene so the player can prepare and then start the quiz manually.
// For now this method is called automatically after 5s.
StartCoroutine(DelayStartQuiz(5.0f));
}
// Update is called once per frame.
void Update()
{
DrainScore();
}
/// <summary>
/// TEMPORARY method for starting the quiz after a delay.
/// This method can be removed once the quiz is activated manually by the player.
/// </summary>
/// <param name="delay">Delay in seconds.</param>
/// <returns></returns>
IEnumerator DelayStartQuiz(float delay)
{
yield return new WaitForSeconds(delay);
StartQuiz();
yield return null;
}
/// <summary>
/// Boot up the quiz, loading questions and showing the first one.
/// </summary>
void StartQuiz()
{
state = QuizState.Initializing;
answeredCorrectly = new List<QuizQuestion>();
answeredIncorrectly = new List<QuizQuestion>();
remainingQuestions = LoadQuestionsFromFile("Quiz/Questions");
remainingQuestions.Shuffle();
StartNextQuestion();
}
/// <summary>
/// Load the next (random, non-duplicate) question.
/// </summary>
void StartNextQuestion()
{
// Load a random remaining question as the current question.
int i = UnityEngine.Random.Range(0, remainingQuestions.Count - 1);
currentQuestion = remainingQuestions[i];
remainingQuestions.RemoveAt(i);
// TODO: Show the question's media clip if it contains one, wait for it to complete before asking the question and allowing input.
// TODO: Set a timer for when time runs out.
if (currentQuestion.Scenario == "")
{
// Immediately show the question and answers, awaiting user input.
AwaitAnswer();
}
else
{
// Load and handle the supplied scenario first, THEN show the question and answers.
monitor.Clear();
LoadScenario(currentQuestion.Scenario);
}
}
/// <summary>
/// Start waiting for user input.
/// </summary>
void AwaitAnswer()
{
ShowQuestionAndAnswers(currentQuestion);
state = QuizState.AwaitingAnswer;
}
/// <summary>
/// Load the scenario that belongs to this quiz question.
/// Won't continue the quiz until this scenario calls EndScenario() on its own.
/// </summary>
/// <param name="scenario">Scenario name to look for. Has to match with an entry from the serializable scenarios property.</param>
void LoadScenario(String scenario)
{
state = QuizState.ShowingScenario;
currentScenario = FindScenarioByName(scenario);
currentScenario.Load();
}
/// <summary>
/// Find a scenario that matches with the supplied name.
/// </summary>
/// <param name="name">Name to look for.</param>
/// <returns>The scenario with a matching name or null if no matching scenario was found.</returns>
QuizScenario FindScenarioByName(string name)
{
return scenarios.Where(i => i.Name == name).First();
}
/// <summary>
/// Show the question and its (shuffled) answers in the world for the player to see.
/// </summary>
/// <param name="q">Question object, also contains the answers.</param>
void ShowQuestionAndAnswers(QuizQuestion q)
{
monitor.ShowQuestion(q);
// TODO: Disable some buttons visually if the question has fewer answers than four.
Debug.Log("QUESTION:");
Debug.Log(String.Format($"Q: {q.Question}"));
foreach (string a in q.Answers)
{
Debug.Log(String.Format($"A: {a}"));
}
}
/// <summary>
/// Load the desired list of questions from a locally stored file.
/// </summary>
/// <param name="path">Path to file containing questions.</param>
/// <returns></returns>
List<QuizQuestion> LoadQuestionsFromFile(string path)
{
TextAsset file = (TextAsset)Resources.Load(path);
XmlDocument doc = new XmlDocument();
doc.LoadXml(file.text);
List<QuizQuestion> questions = new List<QuizQuestion>();
XmlNodeList QuestionNodeList;
XmlNode root = doc.DocumentElement;
QuestionNodeList = root.SelectNodes("descendant::QuizQuestion");
foreach (XmlNode q in QuestionNodeList)
{
int score = int.Parse(q.SelectSingleNode("descendant::Score").InnerText);
string question = q.SelectSingleNode("descendant::Question").InnerText;
float duration = float.Parse(q.SelectSingleNode("descendant::Duration").InnerText);
string scenario = q.SelectSingleNode("descendant::Scenario").InnerText;
List<string> answers = new List<string>();
XmlNode ans = q.SelectSingleNode("descendant::Answers");
XmlNodeList answerNodeList = ans.SelectNodes("descendant::Answer");
foreach (XmlNode a in answerNodeList)
{
answers.Add(a.InnerText);
}
questions.Add(new QuizQuestion(score, question, answers, duration, scenario));
}
return questions;
}
/// <summary>
/// Check whether the supplied answer index matches the question's correct answer.
/// </summary>
/// <param name="a">Index of your answer, corresponds to QuizQuestion's answers list.</param>
/// <param name="q">Original question containing all the possible answers and the correct one.</param>
/// <returns></returns>
bool IsCorrectAnswer(int a, QuizQuestion q)
{
return (q.Answers[a] == q.CorrectAnswer);
}
/// <summary>
/// After submitting your answer, wait for a short duration before revealing the result.
/// </summary>
/// <param name="yourAnswer">The index of your submitted answer.</param>
/// <returns></returns>
IEnumerator WaitRevealAnswer(int yourAnswer)
{
monitor.ShowConfirming(yourAnswer);
Debug.Log("Answer submitted. Now waiting before showing result.");
Debug.Log("Drum roll, please...");
yield return new WaitForSeconds(delayBeforeRevealAnswer);
if (IsCorrectAnswer(yourAnswer, currentQuestion))
{
answeredCorrectly.Add(currentQuestion);
ShowResultAsCorrect(yourAnswer);
StartCoroutine(WaitUpdateScoreAndNextQuestion(true));
}
else
{
answeredIncorrectly.Add(currentQuestion);
ShowResultAsIncorrect(yourAnswer, currentQuestion.Answers.IndexOf(currentQuestion.CorrectAnswer));
StartCoroutine(WaitUpdateScoreAndNextQuestion(false));
}
yield return null;
}
/// <summary>
/// Show the result as correctly answered in the scene.
/// </summary>
/// <param name="yourAnswer">The index of your submitted answer.</param>
void ShowResultAsCorrect(int yourAnswer)
{
monitor.ShowAnswerAsCorrect();
livesIndicator.ShowLives((maxIncorrect + 1) - answeredIncorrectly.Count);
Debug.Log("Answered correctly!");
}
/// <summary>
/// Show the result as incorrectly answered in the scene.
/// </summary>
/// <param name="yourAnswer">The index of your submitted answer.</param>
/// <param name="correctAnswer">The index of the correct answer.</param>
void ShowResultAsIncorrect(int yourAnswer, int correctAnswer)
{
monitor.ShowAnswerAsIncorrect(correctAnswer);
livesIndicator.ShowLives((maxIncorrect + 1) - answeredIncorrectly.Count);
Debug.Log(String.Format($"Answered incorrectly... the correct answer was: {currentQuestion.CorrectAnswer}"));
}
/// <summary>
/// After showing the result, wait a short duration before updating score and advancing to the next question.
/// Will end the game if no questions remain.
/// </summary>
/// <param name="correct">Boolean that reflects whether the current question has been answered correctly.</param>
/// <returns></returns>
IEnumerator WaitUpdateScoreAndNextQuestion(bool correct)
{
yield return new WaitForSeconds(delayBeforeAward);
if (correct)
IncrementScore(currentQuestion.Score);
yield return new WaitForSeconds(delayBeforeNextQuestion);
if (answeredIncorrectly.Count > maxIncorrect)
{
// No lives remaining: game over.
EndGame(false);
}
else if (remainingQuestions.Count > 0)
{
// More questions available: next question.
StartNextQuestion();
}
else
{
// No more questions left: victory.
EndGame(true);
}
yield return null;
}
/// <summary>
/// Increment the player's score.
/// </summary>
/// <param name="increment">Amount to add to the original score. Can be negative.</param>
void IncrementScore(float increment)
{
score += increment;
if (score < 0.0f)
score = 0.0f;
ShowUpdateScore(score);
}
/// <summary>
/// Show the updated player score in the scene.
/// </summary>
/// <param name="newScore">The updated score.</param>
void ShowUpdateScore(float newScore)
{
int scoreAsInt = Convert.ToInt32(newScore);
monitor.UpdateScore(scoreAsInt);
}
/// <summary>
/// End the game. No more questions can be answered.
/// Will also allow the player to return to the central hub.
/// </summary>
/// <param name="completed">Whether the quiz was completed (all questions answered) or if it's a game over (no lives remaining).</param>
void EndGame(bool completed)
{
state = QuizState.EndingGame;
int scoreAsInt = Convert.ToInt32(score);
monitor.ShowGameEnded(completed, scoreAsInt);
Debug.Log("The game has ended.");
Debug.Log(String.Format($"Your final score is: {score}"));
StartCoroutine(WaitLoadMainMenu(delayBeforeLoadMenu));
}
/// <summary>
/// Load the main menu scene after giving the player time to read their final score.
/// </summary>
/// <param name="delay">Delay before loading main menu.</param>
/// <returns></returns>
IEnumerator WaitLoadMainMenu(float delay)
{
yield return new WaitForSeconds(delay);
sceneChanges.RequestNewScene("MenuScene");
yield return null;
}
/// <summary>
/// Drain the player's score over time (every frame) while awaiting input.
/// </summary>
void DrainScore()
{
float f = scoreDrainPerSecond * Time.deltaTime;
if (state == QuizState.AwaitingAnswer)
IncrementScore(-f);
}
/// <summary>
/// Submit an answer.
/// Called by QuizButton script.
/// </summary>
/// <param name="qb"></param>
public void SubmitAnswer(QuizButton qb)
{
if (state == QuizState.AwaitingAnswer)
{
// Security check to make sure this button is relevant.
// E.g. the fourth button isn't relevant for a question with only three answers.
if ((buttons.IndexOf(qb) + 1) > currentQuestion.Answers.Count)
{
Debug.Log("Ignoring input from this button as no answer has been assigned.");
return;
}
qb.PlaySubmitSound();
state = QuizState.ShowingResult;
StartCoroutine(WaitRevealAnswer(buttons.IndexOf(qb)));
}
}
/// <summary>
/// End the current scenario.
/// Called by QuizScenario once it's completed.
/// It's up to each individual scenario to determine when this is called.
/// </summary>
public void EndScenario()
{
AwaitAnswer();
}
}