Friday, August 13, 2010

How to Create a Quiz in Flash - ActionScript 3 Tutorial - PART 2

[Read PART 1 here]

In the first part of this lesson, we added the functionality that will allow the user to take the quiz - we added the questions and the correct answers, and we enabled the user to submit his or her own answers by using the input field and clicking on the submit button. All of that happens in frame 1 of our simple Flash quiz.

In this part of the How to Create a Quiz in Flash using ActionScript 3 tutorial, we will be working on the following:
  1. Displaying the user's answers alongside the correct answers
  2. Checking the user's answers and computing the score
  3. Displaying the user's score
All of these will happen in frame 2 of our Flash movie. So let's go ahead and take a look at the elements that we have on this frame. Select frame 2 of the main timeline and take a look at the stage. You will see that there are 9 dynamic text fields. Out of these 9, there are 8 dynamic text fields that are distributed into 2 columns. Each column has 4 dynamic text fields. The ones on the left column will be used to display the answers that the user gave. The ones on the right column will display the correct answers. So what we want to do here is to just show the user some feedback so that he or she can compare his or her answers with the correct ones. And then there's one more dynamic text field right below the 2 columns. This one will be used to display the user's score. The instance names of of the dynamic text fields are as follows:

TextFields for the USER's answers - userAnswer0_txt, userAnswer1_txt, userAnswer2_txt, userAnswer3_txt

TextFields for the CORRECT answers - correctAnswer0_txt, correctAnswer1_txt, correctAnswer2_txt, correctAnswer3_txt

TextField for the SCORE - score_txt

The TextField names for the answers have numbers in them so that they will match the index values of the answers in the arrays (aUserAnswers and aCorrectAnswers). userAnswer0_txt will be for aUserAnswers[0], userAnswer1_text will be for aUserAnswers[1], and so on...

So now let's go ahead and add the code. Make sure you select frame 2 of Actions layer and then go to the Actions Panel. Let's first create a number variable:
var nScore:Number = 0;
This variable is going to be used to store the user's score. I'm initializing it to 0 just in case the user does not get any answer correctly. If that happens then the score will just stay at 0. Ok, so we'll go back to that variable later. For now, let's work on the code that will display the answers in their respective text fields. We will get the data from the arrays using array access notation, and assign each piece of data to it's corresponding text field using the text property of the TextField class. We can do it this way:
userAnswer0_txt.text = aUserAnswers[0];
userAnswer1_txt.text = aUserAnswers[1];
userAnswer2_txt.text = aUserAnswers[2];
userAnswer3_txt.text = aUserAnswers[3];

correctAnswer0_txt.text = aCorrectAnswers[0];
correctAnswer1_txt.text = aCorrectAnswers[1];
correctAnswer2_txt.text = aCorrectAnswers[2];
correctAnswer3_txt.text = aCorrectAnswers[3];
If you add in this code and then test the movie, you should see all the answers displayed in the text fields after you answer all the questions. So at this point, we can actually move on to the next part of the code that will check whether the answers the user gave are correct. But before we do that, I'd like to show you another way of displaying the answers in the TextFields. Instead of manually assigning each item to its corresponding text field one by one, we'll use a for loop instead. So go ahead and remove or comment out the code above, and we'll try this other method of displaying the answers.

Ok, so in the first method, we displayed the answers this way:
textField.text = array[index];
ex.
correctAnswer0_txt.text = aCorrectAnswers[0];

For this other method that we're doing, we need to learn another way of targeting our TextField. For example, instead of typing in correctAnswer0_txt.text, we can replace correctAnswer0_txt with this["correctAnswer0_txt"] instead. So this is what we'll have:
this["correctAnswer0_txt"].text = aCorrectAnswers[0];

This is but another way of targeting our display objects (like Buttons, MovieClips and TextFields). Let's take a look at the example again:
this["correctAnswer0_txt"]

Here, since our code is placed in the main timeline, then the this keyword in this instance would refer to the main timeline. And then inside the square brackets, we place the name of the child object that we want to target (the name must be specified as a string, so it should be in quotation marks). So what we're doing here is we are telling Flash to target the correctAnswer0_txt TextField which can be found inside this (which in this example would be the main timeline).

So what is the benefit of writing it this way instead?
In this other method of targeting display objects, we are specifying the name of the instances as strings. Because we're doing it this way, we can replace the numbers in the names with variables instead. That way, we can just have that variable be incremented inside a for loop so that we don't have to hard code in the numbers for each text field (here you see why it's important that we numbered our text fields within their respective names). If this sounds a bit confusing, let's take a look at how the new text assignment statement will look inside of the for loop:
this["correctAnswer" + i + "_txt"].text = aCorrectAnswers[i];
Here, the numbers are replaced with the variable i. For the text field's name, the join operator (+) is used to combine the variable with the rest of the name. Then as the for loop updates, so does i, therefore allowing us to iterate through the text fields and the items in the arrays. Note that i is a number, and it is being combined with strings. In this example, there is no need to convert i into a string. Because the number data is being combined with strings, Flash will automatically convert that number data into a string. So now let's go ahead and write our for loop. We'll start with the variable i being equal to 0, and then as long as i is less then aQuestions.length, we'll keep the for loop running (you can also use aCorrectAnswers.length or aUserAnswers.length since they all have the same lengths anyway). So our for loop will look like this:
for(var i:Number = 0; i < aQuestions.length; i++)
{
     this["userAnswer" + i + "_txt"].text = aUserAnswers[i];
     this["correctAnswer" + i + "_txt"].text = aCorrectAnswers[i];
}
So the first time the for loop runs, i will be equal to 0, therefore aUserAnswers[0] and aCorrectAnswers[0] will be assigned to the userAnswer0_txt and correctAnswer0_txt TextFields respectively. Then as i updates, the rest of the items in the arrays will be assigned to their respective text fields as well.

Go ahead and test your movie and you should see that this will yield the same results as with the previous method that we used. This second method would be more efficient to use since we won't have to type in the text assignment statements one by one. Imagine if our quiz had 100 questions!

Ok, so now we can move on to the part that checks whether the user's answers are correct. So how do we write the code that will determine whether each answer is correct or not? If the answer that the user submitted is the same as the corresponding item in the aCorrectAnswers array, then it means that the user got the correct answer. We know that the user's answers are stored in the aUserAnswers array so we could just compare if the items in aUserAnswers match their partners (the ones with the same index value) in the aCorrectAnswers array. We'll use an if statement for that. For each match that is detected, we will give the user a point by incrementing nScore by 1 (nScore is that variable that we created earlier, which was initialized to 0). We will place that if statement in the for loop as well, since we want to go through each item in both of the arrays. So our updated for loop is now:
for(var i:Number = 0; i < aQuestions.length; i++) 
{
     this["userAnswer" + i + "_txt"].text = aUserAnswers[i];
     this["correctAnswer" + i + "_txt"].text = aCorrectAnswers[i];
     if(aUserAnswers[i] == aCorrectAnswers[i])
     {
          nScore++;
          trace(nScore);
     }
}
So what the if statement in the for loop will do is it will compare the items with the same index value from both arrays. Each time they match, then nScore gets incremented by 1. I've added in a trace statement so we could verify if the correct computation is being made. But there's still one more modification that we need to make with regard to the comparison of the answers. You see, when strings are being compared, the process is case-sensitive. So a lower case a will not be considered equal to an uppercase A. So what happens here is that the user may have submitted the correct word, but if the casing of even just one letter is different from the item in the aCorrectAnswers array, then it will not be considered equal and the user will not be given the corresponding point. In order to fix this, we need to make sure that when the items are compared, the strings from both arrays have identical casings. It doesn't matter if they are in uppercase or lowercase, the important thing is that they are the same. So what we can do is, we can convert the casing to either uppercase or lowercase when the items are being compared in the if statement. We can convert the items by using either the toUpperCase() or toLowerCase() methods of the String class. I'm going to use the toUpperCase() method. So let's go ahead and update the code:
for(var i:Number = 0; i < aQuestions.length; i++) 
{
     this["userAnswer" + i + "_txt"].text = aUserAnswers[i];
     this["correctAnswer" + i + "_txt"].text = aCorrectAnswers[i];
     if(aUserAnswers[i].toUpperCase() == aCorrectAnswers[i].toUpperCase()) 
     {
          nScore++;
          trace(nScore);
     }
}
So now, when each pair is compared, they will be converted into the same casing. Note that this will not affect how the words are displayed in the text fields. The conversion to upper case (or lower case, if that's what you chose), will only be for the if statement condition in this example.

So now, if you test the movie and try out the quiz again, you should see nScore update in the output window (provided that you get at least one answer right, if you don't get any answers right, then nScore never gets updated and won't get traced).

Lastly, we'd like to display the final score in the score_txt text field. We'll display the score once the for loop has finished checking all the answers. To check whether the for loop is finished, we can check whether i is equal to aQuestions.length - 1. If i is equal to the array length minus one, then it means that the for loop is already at the last item (we subtract the length by 1 because the index values start at 0, where as the length count starts at 1). So we'll use another if statement inside the for loop to check for that. If i is equal to aQuestions.length - 1, then we assign nScore to score_txt using the text property of the TextField class (be sure to convert nScore to a string using the toString() method). So let's go ahead and update the code:
for(var i:Number = 0; i < aQuestions.length; i++) 
{
     this["userAnswer" + i + "_txt"].text = aUserAnswers[i];
     this["correctAnswer" + i + "_txt"].text = aCorrectAnswers[i];
     if(aUserAnswers[i].toUpperCase() == aCorrectAnswers[i].toUpperCase()) 
     {
          nScore++;
     }     
     if(i == aQuestions.length - 1) 
     {
          score_txt.text = nScore.toString();
     }
}
So there you have it. Our quiz app is now complete.

Here is the code in full:

FRAME 1
stop();

var nQNumber:Number = 0;
var aQuestions:Array = new Array();
var aCorrectAnswers:Array = new Array("Jupiter", "Mars", "war", "Titan");
var aUserAnswers:Array = new Array();

aQuestions[0] = "What is the biggest planet in our solar system?";
aQuestions[1] = "Which planet in our solar system is the 4th planet from the sun?";
aQuestions[2] = "Mars is named after the Roman god of ___.";
aQuestions[3] = "What is the name of Saturn's largest moon?";

questions_txt.text = aQuestions[nQNumber];

submit_btn.addEventListener(MouseEvent.CLICK, quiz);

function quiz(e:MouseEvent):void 
{
     aUserAnswers.push(answers_txt.text);
     answers_txt.text = "";
     nQNumber++;
     if(nQNumber < aQuestions.length) 
     {
          questions_txt.text = aQuestions[nQNumber];
     }
     else
     {
          nextFrame();
     }
}
FRAME 2
var nScore:Number = 0;

for(var i:Number = 0; i < aQuestions.length; i++) 
{
     this["userAnswer" + i + "_txt"].text = aUserAnswers[i];
     this["correctAnswer" + i + "_txt"].text = aCorrectAnswers[i];
     if(aUserAnswers[i].toUpperCase() == aCorrectAnswers[i].toUpperCase()) 
     {
          nScore++;
     }
     if(i == aQuestions.length - 1) 
     {
          score_txt.text = nScore.toString();
     }
}
PREV: How to Create a Quiz in Flash - ActionScript 3 Tutorial - PART 1

20 comments:

  1. This throws an Access of defined property aCorrectAnsers on actons frame 2 line 8 and 9

    ReplyDelete
  2. Check the variable names. Could be a typo somewhere.

    ReplyDelete
  3. The score frame keeps on returning me this error:

    Error #1010: A term is undefined and has no properties.

    I used the complete frame 2 code and has assigned a dynamic text score_txt to display the scores, still... an error. How do I solve this?

    Thanks...

    P.S. I'm relatively new to AS3.

    ReplyDelete
  4. This is really very interesting thing to me and the way, you have explained is awesome.

    Is there any possibility to get score in % (percent) and then make it SCORM compatable to upload it on LMS?

    Looking for early response.

    Thanks,
    Vishwajit Vatsa |9873929087

    ReplyDelete
  5. FANTASTIC tutorial! Simple without being childish, well broken-up into parts, and useful! Keep up the great work!

    ReplyDelete
  6. FANTASTIC! This tutorial really helps me a lot, thanks man, much appreciated! tc

    ReplyDelete
  7. Really would love too see a randomize was of the script picking questions.

    I added another 50 questions and have no idea how to randomize it :(

    ReplyDelete
  8. Wow, thanks. That's great. I successfully did it.

    ReplyDelete
  9. This is very helpful... Can you make the questions appear randomly?

    ReplyDelete
  10. Hi. i have tried your codes, the way you have given us above.

    but .. my scoring part was a bit down. if the first question is wrong, the overall scores will automatically be 0 ..

    ReplyDelete
  11. Works perfectly, great tutorial, thank you.

    ReplyDelete
  12. Nice lesson, thanks.

    But I don't understand one thing...

    I tried to add this code with some green star symbols, which should mark correct answers. So I placed 4 star mc's on the stage with instance names: star1_mc, star2_mc, etc.

    And I tried to use such, similar to yours, this. trick:

    for(var k:Number = 0; k < aQuestions.length; k++ )
    {
    this["star" + i + "_mc"].alpha = 0;
    }

    But this gives an error:
    TypeError: Error #1010: A term is undefined and has no properties.
    at Arrays1_fla::MainTimeline/Arrays1_fla::frame2()

    What is wrong? Maybe this 'this.' trick is not fit for movieclips?


    ReplyDelete
  13. I run this script and made each score worth +20. But the total only reaches 60 points with all 4 questions correct. I can't figure out why it only runs the loop 3 times.

    ReplyDelete
  14. how to customizing score? please help me!

    ReplyDelete
  15. This is very awesome. I think this will help my thesis but i have a questions what if i random the question and put a buttons for the choices

    ReplyDelete
  16. how to the questions.. be random???

    ReplyDelete
  17. That is a great tutorial, but like a few people have asked, how do you randomize the questions? In fact, it would be great if on could randomize the order of the answers for the same questions every time you play a new game.

    ReplyDelete
  18. wow, it is such a great tutorial, can you please make the questions randomized. I would like us to chat outside the forum.

    ReplyDelete