Advertisement

iOS SDK: Build a Facts Game - Game Logic

This post is part of a series called iOS SDK: Build a Facts Game.
iOS SDK: Build a Facts Game - Interface Creation

Welcome back to the third and final section of our Facts Game with Sprite Kit tutorial series. This tutorial will teach you how to use the Sprite Kit framework to create a question-based facts game. It is designed for both novice and advanced users. Along the way, you will apply the Sprite Kit core.


Introduction

In this tutorial, you will program the entire game logic including the player's life, the question, and the player's answer. This tutorial series is broken up into three sections: Project Setup, Facts Interface, and Game Logic. If you haven't yet completed the second section, you can download the project and pickup exactly where we left off. Each part produces a practical result, and the sum of all parts will produce the final game. Despite the fact that each part can be read independently, for a better understanding we suggest to follow the tutorial step by step. We also included the source code for each part separately, thus providing a way to start the tutorial in any part of the series.

This is what our game will look like upon completion:

Image0
Illustration of Final Result - Facts

1. Custom Facts Class

In the last tutorial, you defined a plist file for the questions. Each question has four properties. In order to manage them, you need to create a custom class to afford that task properly. Therefore, you need to form another Objective-C class. Name it factObject and define the NSObject superclass.

Now, let's edit the header file and add the four plist properties. Each plist property has its own features:

  • The statement Id is an int.
  • The statement is a NSString.
  • The isCorrect statement is an int.
  • The additional information is a NSString.

The end result should be similar to this:

  @property(nonatomic,readwrite) int factID;
  @property(nonatomic,readwrite,retain) NSString *statement;
  @property(nonatomic,readwrite) NSInteger isCorrect;
  @property(nonatomic,readwrite,retain) NSString *additionalInfo;

You don't need to use the implementation file (.m). We will parse the plist file to this custom class and use the values directly from the memory.


2. Facts Interface: Initialization

In the last tutorial, you defined the basic structure of the facts interface. It is now time to complete it with the logic steps. To complete this game, we need to create a question label, a customized background statement, a button that asks for another question, and a true and false interface. Those four statements translate into five properties defined in the FactsScene.h file. Once again, you can name them as you please. Our implementation is:

  @property (nonatomic, retain) UILabel *questionLabel;
  @property (nonatomic, retain) SKSpriteNode *backgroundStatement;
  @property (nonatomic, retain) UIButton *nextQuestion;
  @property (nonatomic, retain) SKSpriteNode* wrong;
  @property (nonatomic, retain) SKSpriteNode* correct;

Now move your attention to the FactsScene.m. You need to define several objects that are used internally in the class:

  • A NSMutableArray to store the questions
  • A random value that represents a random question
  • The question identifier
  • The minimum threshold for the right questions; this threshold indicates the minimum required correct answers for the user to advance to another level. In this tutorial, you will use value seven.

Your implementation file should look like this:

  NSMutableArray *statements;
  int randomQuestion;
  int questionNumber;
  int totalRightQuestions; // need 7 out of 10 to pass to the next level

It is now time to allocate a few values and begin with the logic. In the -(id) initWithSize:(CGSize)size inLevel:(NSInteger)level withPlayerLives:(int)lives method initiate the questionNumber and the totalRightQuestions. Since it is the first time you use it, the initiation is easy and can be done like:

        questionNumber = 1;
        totalRightQuestions=0;

Now it is time to use the custom class defined in the aforementioned step. Parse the plist file and use the information store in the plist to allocate and populate new factObject objects. Note that we will store each factObject object in a custom NSMutableArray already defined (statements). The complete snippet is below.

        statements = [[NSMutableArray alloc] init];
        NSString* plistPath = [[NSBundle mainBundle] pathForResource:@"LevelDescription" ofType:@"plist"];
        NSMutableDictionary* dictionary = [[NSMutableDictionary alloc] initWithContentsOfFile:plistPath];

        if ([dictionary objectForKey:@"Questions" ] != nil ){
            NSMutableArray *array = [dictionary objectForKey:@"Questions"];

            for(int i = 0; i < [array count]; i++){
                NSMutableDictionary *questions = [array objectAtIndex:i];
                factObject *stat = [factObject new];
                stat.factID = [[questions objectForKey:@"id"] intValue];
                stat.statement = [questions objectForKey:@"statement"];
                stat.isCorrect = [[questions objectForKey:@"isCorrect"] integerValue];
                stat.additionalInfo = [questions objectForKey:@"additionalInfo"];
                [statements addObject:stat];
            }
        }

This step removes the older parsing code from the -(void) didMoveToView:(SKView *)view method. You can remove it, since you will not use it anymore.


3. Facts Interface: Logic

It is now time to focus on the logic code itself. We need to show the question to the user. However, the question is always a random choice. Start to define a rectangle to afford the question and then allocate the necessary resources for the question's text. The following snippet will help you:

    CGRect labelFrame = CGRectMake(120,300, 530, 100);
    _questionLabel = [[UILabel alloc] initWithFrame:labelFrame];

    randomQuestion = [self getRandomNumberBetween:0 to:([statements count]-1)];

    NSString *labelText = [[statements objectAtIndex:randomQuestion] statement];
    [_questionLabel setText:labelText];
    [_questionLabel setTextColor:[UIColor whiteColor]];
    [_questionLabel setFont:[UIFont fontWithName:NULL size:23]];
    [_questionLabel setTextAlignment:NSTextAlignmentCenter];
    // The label will use an unlimited number of lines
    [_questionLabel setNumberOfLines:0];

Note that you will not use the SKLabelNode over the simple NSString because of a SKLabelNode limitation; it is for single-line text only. A warning will appear regarding the getRandomNumberBetween:0 to:X method. You need to declare and code it; its objective is to return a random value between two values. The next snippet will help you:

-(int)getRandomNumberBetween:(int)from to:(int)to {
    return (int)from + arc4random() % (to-from+1);
}

Now that you can see the question, we need to add some functionalities to the right and wrong button. Change both selectors and call a new method called: presentCorrectWrongMenu.

[_falseButton addTarget:self action:@selector(presentCorrectWrongMenu:) forControlEvents:UIControlEventTouchUpInside];
[_trueButton addTarget:self action:@selector(presentCorrectWrongMenu:) forControlEvents:UIControlEventTouchUpInside];

Additionally, define a tag for each button. The true button will be tag = 1 and the false tag = 0. These tags help you when you call the -(void)presentCorrectWrongMenu:(UIButton*)sender method to determine which button was tapped to call that same method.

  [_trueButton setTag:1];
  [_falseButton setTag:0];

The next step is to add the -(void)presentCorrectWrongMenu:(UIButton*)sender method. This method is complex and recognizes which button is tapped, adds a custom answer interface, and adds a button that calls the next question. Use the following snippet to achieve the above-mentioned topics:

-(void)presentCorrectWrongMenu:(UIButton*)sender{

    int userData = sender.tag;

    // background
    _backgroundStatement = [SKSpriteNode spriteNodeWithImageNamed:@"background.png"];
    _backgroundStatement.position = CGPointMake(CGRectGetMidX(self.frame),CGRectGetMidY(self.frame));
    _backgroundStatement.size = CGSizeMake(768, 1024);
    _backgroundStatement.zPosition = 10;
    _backgroundStatement.alpha = 0.0;
    [self addChild:_backgroundStatement];

    _nextQuestion = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    _nextQuestion.frame = CGRectMake(CGRectGetMidX(self.frame)-100, CGRectGetMidY(self.frame)+90, 200, 70.0);
    _nextQuestion.backgroundColor = [UIColor clearColor];
    [_nextQuestion setTitleColor:[UIColor blackColor] forState:UIControlStateNormal ];
    [_nextQuestion setTitle:@"Tap Here to Continue" forState:UIControlStateNormal];
    [_nextQuestion addTarget:self action:@selector(nextQuestion) forControlEvents:UIControlEventTouchUpInside];
    _nextQuestion.alpha = 1.0;
    [self.view addSubview:_nextQuestion];

    [_backgroundStatement runAction:[SKAction fadeAlphaTo:1.0f duration:0.2f]];
    _trueButton.alpha = 0.0;
    _falseButton.alpha = 0.0;

A warning will appear, however do not fix it right away. First, end the method declaration. Now that you have a custom interface for the answer, you need to test the player's answer. To achieve that, you need to know which button the player tapped and the inherent question's answer. You already know this, so you only need to create a simple logic test condition. To do this, you need to test if the answer is correct or incorrect, play a sound accordingly, and proceed to the properties update. The next snippet will help you. Note that you must place it where the last snippet of code ended.

if( ([[statements objectAtIndex:randomQuestion] isCorrect] == 0 && userData == 0) || ([[statements objectAtIndex:randomQuestion] isCorrect] == 1 && userData == 1) ){

        if ([[statements objectAtIndex:randomQuestion] isCorrect] == 0)
            _questionLabel.text = [[statements objectAtIndex:randomQuestion] additionalInfo];

        _correct = [SKSpriteNode spriteNodeWithImageNamed:@"correct.png"];
        _correct.scale = .6;
        _correct.zPosition = 10;
        _correct.position = CGPointMake(CGRectGetMidX(self.frame),800);
        _correct.alpha = 1.0;

        totalRightQuestions++;

        [self touchWillProduceASound:@"True"];
        [self addChild:_correct];
    }
    else{
        if ([[statements objectAtIndex:randomQuestion] isCorrect] == 0)
            _questionLabel.text = [[statements objectAtIndex:randomQuestion] additionalInfo];

        _wrong = [SKSpriteNode spriteNodeWithImageNamed:@"wrong.png"];
        _wrong.scale = .6;
        _wrong.zPosition = 10;
        _wrong.position = CGPointMake(CGRectGetMidX(self.frame),800);
        _wrong.alpha = 1.0;

        [self removePlayerLife];

        [self touchWillProduceASound:@"False"];
        [self addChild:_wrong];
    }
}

Do you remember the last warning? Now you should see several warnings. Don't worry, they're warning you that several methods are missing. We can correct that. The first method to define is the -(void)nextQuestion. As the name suggests, it calls the next question. Besides presenting a new question, it resets the timer, increments the question number, updates the current question label, removes the presented question from the array, and tests the logic needed to move to another level. The complete source code of -(void)nextQuestion is:

-(void)nextQuestion{
    [self resetTimer];

    questionNumber++;
    _currentLevelLabel.text = [[NSString alloc] initWithFormat:@"Level: %ld of 10", (long)questionNumber];

    _wrong.alpha = 0.0;
    _correct.alpha = 0.0;
    _backgroundStatement.alpha = 0.0;
    _nextQuestion.alpha = 0.0;

    [statements removeObject:[statements objectAtIndex:randomQuestion]];

    //random question
    randomQuestion = [self getRandomNumberBetween:0 to:([statements count]-1)];
    [_questionLabel setText:[[statements objectAtIndex:randomQuestion] statement]];

    _trueButton.alpha = 1.0;
    _falseButton.alpha = 1.0;

    if (questionNumber == 10 && totalRightQuestions > 7){
        int nexLevel = playerLevel+2;
        [defaults setInteger:nexLevel forKey:@"actualPlayerLevel"];
        [self removeUIViews];
        SKTransition* transition = [SKTransition doorwayWithDuration:2];
        LevelSelect* levelSelect = [[LevelSelect alloc] initWithSize:CGSizeMake(CGRectGetMaxX(self.frame), CGRectGetMaxY(self.frame))];
        [self.scene.view presentScene:levelSelect transition:transition];
    }
}

Note that you hard-coded the maximum questions (10) for this level and the threshold for the next level (7). Once again, a new warning will appear. No resetTimer method exists; this method only resets the maximumTime property to 60 and updates the label accordingly:

-(void)resetTimer{
    maximumTime = 60;
    [_timerLevel setText:@"60"];
}

In the last tutorial, you defined the touchWillProduceASound method. However in this tutorial, you need to modify it further. The objective is to pass it a object that represents the correct or incorrect answer. Then the corresponding sound will play. The complete method is:

-(void) touchWillProduceASound:(NSString*)answer{
    long soundFlag = [defaults integerForKey:@"sound"];

    if (soundFlag == 1){
        SKAction* sound;
        if ([answer isEqualToString:@"False"]) {
            sound = [SKAction playSoundFileNamed:@"wrong.mp3" waitForCompletion:YES];
        } else {
            sound = [SKAction playSoundFileNamed:@"right.mp3" waitForCompletion:YES];
        }
        [self runAction:sound];
    }
}

You still need to define the -(void)removePlayerLife method. As the name states, it tests the life of a player and acts accordingly. If the player has more than one life, a life is reduced and the inherent asset is updated or is moved to the home screen. The complete method is below.

-(void)removePlayerLife{
    if (playerLives > 1){

        for(NSInteger i = 0; i < playerLives; i++){
            SKSpriteNode* node = [heartArray objectAtIndex:i];
            if (i == (playerLives-1)){
                node.alpha = .1;
            }
        }
        playerLives--;
    } else {
        [self moveToHome];
    }
}

At this point, we're nearly finished. It is now time to update the - (void)updateTimer defined in the last tutorial. This new method is responsible for updating the timer value and testing the player's life. It automatically reacts when the timer hits zero. At that time, it tests the player's life and acts accordingly. It goes to the main menu if the player's life is less than one or calls another question otherwise (decreasing the player's life). The complete snippet is below.

- (void)updateTimer{
    maximumTime--;
    if (maximumTime == 0){
        if (playerLives < 1){
            [self touchWillProduceASound:@"False"];
            [self moveToHome];
        } else{
            [self presentCorrectWrongMenu:_trueButton];
            [self touchWillProduceASound:@"False"];
            [self removePlayerLife];
        }
    }
    [_timerLevel setText:[[NSNumber numberWithInt:maximumTime] stringValue]];
}

4. Additional Methods

Two additional methods were created: -(void)moveToHome and -(void)removeUIViews. We need to define them because we'll use them more than once. It is good practice to reuse code instead of typing it all again. The -(void)moveToHome is just a call to a SKTransition and MyScene class. The code is:

-(void)moveToHome{
    SKTransition* transition = [SKTransition fadeWithDuration:2];
    MyScene* myscene = [[MyScene alloc] initWithSize:CGSizeMake(CGRectGetMaxX(self.frame), CGRectGetMaxY(self.frame))];
    [self.scene.view presentScene:myscene transition:transition];
}

The -(void)removeUIViews removes the UIKit views from the superview. Here is what the code looks like:

-(void)removeUIViews{
    [_trueButton removeFromSuperview];
    [_falseButton removeFromSuperview];
    [_questionLabel removeFromSuperview];
}

Now that everything is correct, Run the project. Every time you correctly answer a question, you will see an interface similar to the next image:

Image1
Illustration of a correct answer

On the other hand, when you incorrectly answer a question, you will see an interface that looks like this:

Image2
Illustration of an incorrect answer

5. Final Improvements

There's just one more step before we're done. We need to correctly initialize the actualPlayerLevel at the level selection. Switch your attention to the MyScene.m class (the first one created by the Xcode) and let's add some lines of code. Initially, add an object of the type NSUserDefaults to the @implementation section. The following snippet will help you:

  @implementation MyScene {
    // code
    NSUserDefaults* defaults;
}

Now inside the -(void) didMoveToView:(SKView *)view, add the inherent NSUserDefaults initialization. The default value is one, so that a new player always starts a new game at Level 1. Moreover, if the player didn't achieve the minimum requisites to pass the level, it starts again at that same level. The result is below.

    defaults = [NSUserDefaults standardUserDefaults];
    [defaults setInteger:1 forKey:@"actualPlayerLevel"];

    //more code..

Several other adjustments can be made to this game. You can customize the correct answer rates for questions, animations, and transitions, or alter the landscape and portrait mode. We think that you are ready for such a task. Try to implement them on your own.


Conclusion

At the end of this Facts tutorial, you should be able to create a SpriteKit game, create and configure several SpriteKit views and actions, configure several UIKit views, use them in parallel with SpriteKit, and parse Property lists. Don't hesitate to use the comment section below for suggestions or comments. Thanks for reading!

Advertisement