Simple Parse Tutorial with Sprite Kit game

In this tutorial we are going to add scoring logic to Sprite Kit game we build in previous tutorial and save the score on Parse. Once the score is saved on Parse we are going to fetch the highest score for the user and display it.

Adding Scoring Logic to game

We are going to add a variable ‘player_score’ to keep track of user’s current score in MyScene.m


NSInteger player_score;

Our scoring logic is fairly simple, we give user a point on every missile appearing on the screen. You can pick any logic you like to calculate score for your game.


-(id)initWithSize:(CGSize)size {
    if (self = [super initWithSize:size]) {
        self.backgroundColor = [SKColor whiteColor];
        [self initalizingScrollingBackground];
        [self addShip];

        //Making self delegate of physics World
        self.physicsWorld.gravity = CGVectorMake(0,0);
        self.physicsWorld.contactDelegate = self;
        player_score = 0; //initializing score as zero
    }

    return self;
}

-(void)addMissile
{
    //initalizing spaceship node
    SKSpriteNode *missile;
    missile = [SKSpriteNode spriteNodeWithImageNamed:@"red-missile.png"];
    [missile setScale:0.15];

    //Adding SpriteKit physicsBody for collision detection
    missile.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:missile.size];
    missile.physicsBody.categoryBitMask = obstacleCategory;
    missile.physicsBody.dynamic = YES;
    missile.physicsBody.contactTestBitMask = shipCategory;
    missile.physicsBody.collisionBitMask = 0;
    missile.physicsBody.usesPreciseCollisionDetection = YES;
    missile.name = @"missile";

    //selecting random y position for missile
    int r = arc4random() % 300;
    missile.position = CGPointMake(self.frame.size.width + 20,r);

    player_score = player_score + 1; //adding to score when ever new missile is created
    [self addChild:missile];

}

We have to update our initWithSize and addMissile methods to incorporate scoring logic.

Now we want to display score at the end of game screen. To do that we going to modify our GameOverScene.m initWithSize method and add declaration in GameOverScene.h

GameOverScene.h


#import <SpriteKit/SpriteKit.h>

@interface GameOverScene : SKScene

-(id)initWithSize:(CGSize)size score: (NSInteger)player_score;

@end

GameOverScene.m



#import "GameOverScene.h"
#import "MyScene.h"
#import <Parse/Parse.h>

@implementation GameOverScene
-(id)initWithSize:(CGSize)size score: (NSInteger)player_score{ //updated the existing method
    if (self = [super initWithSize:size]) {

        // 1
        self.backgroundColor = [SKColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1.0];

        // 2
        NSString * message;
        message = @"Game Over";
        // 3
        SKLabelNode *label = [SKLabelNode labelNodeWithFontNamed:@"Chalkduster"];
        label.text = message;
        label.fontSize = 40;
        label.fontColor = [SKColor blackColor];
        label.position = CGPointMake(self.size.width/2, self.size.height/2);
        [self addChild:label];

        NSString * retrymessage;
        retrymessage = @"Replay Game";
        SKLabelNode *retryButton = [SKLabelNode labelNodeWithFontNamed:@"Chalkduster"];
        retryButton.text = retrymessage;
        retryButton.fontColor = [SKColor blackColor];
        retryButton.position = CGPointMake(self.size.width/2, 50);
        retryButton.name = @"retry";
        [retryButton setScale:.5];

        [self addChild:retryButton];

        NSString * playerscoremsg = [NSString stringWithFormat:@"Player Score: %ld",(long)player_score];

        SKLabelNode *playerscore = [SKLabelNode labelNodeWithFontNamed:@"Chalkduster"];
        playerscore.text = playerscoremsg;
        playerscore.fontColor = [SKColor blackColor];
        playerscore.position = CGPointMake(self.size.width/2, 250);
        playerscore.name = @"Player Score";
        [playerscore setScale:.5];

        [self addChild:playerscore];        
    }
    return self;
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [touches anyObject];
    CGPoint location = [touch locationInNode:self];
    SKNode *node = [self nodeAtPoint:location];

    if ([node.name isEqualToString:@"retry"]) {
        SKTransition *reveal = [SKTransition flipHorizontalWithDuration:0.5];

        MyScene * scene = [MyScene sceneWithSize:self.view.bounds.size];
        scene.scaleMode = SKSceneScaleModeAspectFill;
        [self.view presentScene:scene transition: reveal];

    }

}
@end

In GameOverScene.m we have added another label which is displaying players score. Before we try out our scoring logic we need to make one more change in MyScene.m


- (void)didBeginContact:(SKPhysicsContact *)contact
{
    SKPhysicsBody *firstBody, *secondBody;
    if (contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask)
    {
        firstBody = contact.bodyA;
        secondBody = contact.bodyB;
    }
    else
    {
        firstBody = contact.bodyB;
        secondBody = contact.bodyA;
    }

    if ((firstBody.categoryBitMask & shipCategory) != 0 &&
        (secondBody.categoryBitMask & obstacleCategory) != 0)
    {
        [ship removeFromParent];
        SKTransition *reveal = [SKTransition flipHorizontalWithDuration:0.5];
        SKScene * gameOverScene = [[GameOverScene alloc] initWithSize:self.size score:player_score]; //updated this method so we can send players score to game over scene.
        [self.view presentScene:gameOverScene transition: reveal];

    }
}

Now we are ready to run our app and test out scoring logic.

iOS Simulator Screen shot Jan 13, 2014 11.39.01 PM

Saving Score on Parse

Download Parse SDK and follow the steps given in this guide to add Parse Framework to your app.

Once you have added Parse framework to your app, create a account on Parse.

Login to you account to get started.

create_new_app

Once you have created a new app, navigate to setting tab and copy Application ID and ClientKey.app-keys-big

Once you have the key copied Navigate to Data Browser to start creating your database.

create-new-table

After adding the class we going to add columns to PlayerScore class.

create-custom

We are going to add first column user_id which takes string value. Add another column ‘score’ which take number value.

new-table

Your class should look like this at the end.

Alright now are ready to add Parse related code in our app.

Step 1) Adding Parse Keys to app.

In AppDelegate.m update method didFinishLaunchingWithOptions to following


- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    [Parse setApplicationId:@"Application Key" //replace these values with keys you got from Parse account
                  clientKey:@"Client Key"];
    
    return YES;
}

Step 2) Push user score to Parse at end of the game.

Before we push user score to Parse we need a way to identify individual user, which we are going to save in “user_id” column in our Parse Class. We are going to use users “identifierForVendor” as unique identifier for now.

identifierForVendor:  The value of this property is the same for apps that come from the same vendor running on the same device. A different value is returned for apps on the same device that come from different vendors, and for apps on different devices regardless of vendor. You can read more about it here.

Note:  Every time you delete the app from your simulator and install it again you are going to get a different ID. So you will be considered as different user every time you delete the app and install it again.

We are going to update GameOverScene.m initialization method with the following code.



-(id)initWithSize:(CGSize)size score: (NSInteger)player_score{
    if (self = [super initWithSize:size]) {

        self.backgroundColor = [SKColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1.0];

        NSString * message;
        message = @"Game Over";

        SKLabelNode *label = [SKLabelNode labelNodeWithFontNamed:@"Chalkduster"];
        label.text = message;
        label.fontSize = 40;
        label.fontColor = [SKColor blackColor];
        label.position = CGPointMake(self.size.width/2, self.size.height/2);
        [self addChild:label];

        NSString * retrymessage;
        retrymessage = @"Replay Game";
        SKLabelNode *retryButton = [SKLabelNode labelNodeWithFontNamed:@"Chalkduster"];
        retryButton.text = retrymessage;
        retryButton.fontColor = [SKColor blackColor];
        retryButton.position = CGPointMake(self.size.width/2, 50);
        retryButton.name = @"retry";
        [retryButton setScale:.5];

        [self addChild:retryButton];

        NSString * playerscoremsg = [NSString stringWithFormat:@"Player Score: %ld",(long)player_score];

        SKLabelNode *playerscore = [SKLabelNode labelNodeWithFontNamed:@"Chalkduster"];
        playerscore.text = playerscoremsg;
        playerscore.fontColor = [SKColor blackColor];
        playerscore.position = CGPointMake(self.size.width/2, 250);
        playerscore.name = @"Player Score";
        [playerscore setScale:.5];

        [self addChild:playerscore];

        UIDevice * currentDevice = [UIDevice currentDevice];
        NSString *deviceIDString = [currentDevice.identifierForVendor UUIDString]; //getting unique id for the user

        NSNumber *playerScoreNum = [NSNumber numberWithInt:player_score]; //converting score into NSNumber format in which Parse expect the score

       PFObject *scoreObject = [PFObject objectWithClassName:@"PlayerScore"];
        [scoreObject setObject:deviceIDString forKey:@"user_id"]; //user's unique id

        [scoreObject setObject:playerScoreNum forKey:@"score"]; //user's score 

        [scoreObject saveInBackground]; //saving in background, so our application is not halted while saving the score.

    }
    return self;
}

If you run the code now, you will at the end of the game your score is getting pushed to Parse.

Data Browser   Parse

Step 3)  Fetch score from Parse and display the highest score so far

We are going to fetch all previous scores of current user and sort them descending order to get the highest score on top. We are going to add the following lines to GameOverScene.m initialization method.


-(id)initWithSize:(CGSize)size score: (NSInteger)player_score{
    if (self = [super initWithSize:size]) {

        self.backgroundColor = [SKColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1.0];

        NSString * message;
        message = @"Game Over";

        SKLabelNode *label = [SKLabelNode labelNodeWithFontNamed:@"Chalkduster"];
        label.text = message;
        label.fontSize = 40;
        label.fontColor = [SKColor blackColor];
        label.position = CGPointMake(self.size.width/2, self.size.height/2);
        [self addChild:label];

        NSString * retrymessage;
        retrymessage = @"Replay Game";
        SKLabelNode *retryButton = [SKLabelNode labelNodeWithFontNamed:@"Chalkduster"];
        retryButton.text = retrymessage;
        retryButton.fontColor = [SKColor blackColor];
        retryButton.position = CGPointMake(self.size.width/2, 50);
        retryButton.name = @"retry";
        [retryButton setScale:.5];

        [self addChild:retryButton];

        NSString * playerscoremsg = [NSString stringWithFormat:@"Player Score: %ld",(long)player_score];

        SKLabelNode *playerscore = [SKLabelNode labelNodeWithFontNamed:@"Chalkduster"];
        playerscore.text = playerscoremsg;
        playerscore.fontColor = [SKColor blackColor];
        playerscore.position = CGPointMake(self.size.width/2, 250);
        playerscore.name = @"Player Score";
        [playerscore setScale:.5];

        [self addChild:playerscore];

        UIDevice * currentDevice = [UIDevice currentDevice];
        NSString *deviceIDString = [currentDevice.identifierForVendor UUIDString]; 

        NSNumber *playerScoreNum = [NSNumber numberWithInt:player_score]; 

        PFQuery *query = [PFQuery queryWithClassName:@"PlayerScore"]; //creating query for Parse
        [query whereKey:@"user_id" equalTo:deviceIDString];
        [query orderByDescending:@"score"]; //Sorting the score so we have highest score on the top

        [query findObjectsInBackgroundWithBlock:^(NSArray *scoreArray, NSError *error) {

            NSNumber* hightestScore = [scoreArray.firstObject objectForKey:@"score"]; //highest score is first object
            NSLog(@"highest score %@ devise %@",hightestScore, deviceIDString);

            if (hightestScore < playerScoreNum){
                hightestScore = playerScoreNum; //if highest score so far is smaller than current score, display current score
            }
            NSString * highscoremsg = [NSString stringWithFormat:@"Highest Score: %@",hightestScore]; 

            SKLabelNode *highscore = [SKLabelNode labelNodeWithFontNamed:@"Chalkduster"]; //Displaying highest score label
            highscore.text = highscoremsg;
            highscore.fontColor = [SKColor greenColor];
            highscore.position = CGPointMake(self.size.width/2, 200);
            [highscore setScale:.5];
            [self addChild:highscore];

        }];

       PFObject *scoreObject = [PFObject objectWithClassName:@"PlayerScore"];
        [scoreObject setObject:deviceIDString forKey:@"user_id"];

        [scoreObject setObject:playerScoreNum forKey:@"score"];

        [scoreObject saveInBackground];

    }
    return self;
}

We are querying for the highest score in the background so that it does not halt the flow of the app, and user is displayed other information without a delay. We only create the label for highest score once the data is fetched from Parse.

Now run the app and see your highest score.

iOS Simulator Screen shot Jan 20, 2014 1.55.36 PM

 

I hope this was helpful.  You can download the code for this tutorial here.

Simple Sprite Kit game tutorial- Part1

In this tutorial we are gonna make  a basics 2D games with Sprite Kit. You can get the full source code of this tutorial on github.

iOS Simulator Screen shot Oct 28, 2013 10.51.25 PM

Getting Started

Start Xcode and select File\New\Project from the main menu. Select the iOS\Application\SpriteKit Game template and click Next.

spritekit_project copy

Enter your game name for Product Name, choose iPhone for devices and leave the Class Prefix blank and click Next

create-project copy

On Device Orientation deselect Portrait.
only-landscape copy
In XCode’s toolbar, select the iPhone (4-inch) Simulator and click Run
run
You will see a single label that says “Hello World!” and when you click anywhere on the screen, a rotating space ship will appear.
hello copy spaceship copy

SpriteKit Physics and Collision Detection

SpriteKit comes with Physics engine built-in. Which helps in simulating realistic movements and collision detection.

We are going to use SpriteKit physics engine to move our spaceship and detect the collision between spaceship and missile.

We are build our game in following steps

  1. Adding Main character of the game(spaceship) on the screen (Part1)
  2. Adding scrolling background (Part1)
  3. Adding Missile with which spaceship can collide with (Part2)
  4.  Show end of game screen when spaceship collides with missile and give replay option (Part2)
Before starting these steps download the art folder from here, and add it to your Xcode project.

Adding Main Character

Open MyScene.m and you will see the code for displaying rotating space ship. For now delete everything in MyScene.m and replace it by.


#import "MyScene.h"
@implementation MyScene{
    SKSpriteNode *ship;
    SKAction *actionMoveUp;
    SKAction *actionMoveDown;
    NSTimeInterval _lastUpdateTime;
    NSTimeInterval _dt;
    NSTimeInterval _lastMissileAdded;
}

-(id)initWithSize:(CGSize)size {
    if (self = [super initWithSize:size]) {
        self.backgroundColor = [SKColor whiteColor];        
        //Making self delegate of physics World
        self.physicsWorld.gravity = CGVectorMake(0,0);
        self.physicsWorld.contactDelegate = self;

    }
    return self;
}
@end

Let’s go over what this does line by line

Variables ship, actionMoveUp,actionMoveDown,_lastUpdateTime,_dt and _lastMissileAdded we will using later on in code.

initWithSize: is the method that gets called when scene is first created.

        self.backgroundColor = [SKColor whiteColor];        

We set the background color to white.

        self.physicsWorld.gravity = CGVectorMake(0,0);
        self.physicsWorld.contactDelegate = self;

These lines set up the physics world to have no gravity, and sets the scene as the delegate to be notified when two physics bodies collide.

Add these lines right after importing MyScene.h
        
#import "MyScene.h"

static const uint32_t shipCategory =  0x1 << 0;
static const uint32_t obstacleCategory =  0x1 << 1;

These will setup two categories we will need in a bit
Now we are going to add spaceship on the screen. Add this method after initWithSize method
 
-(void)addShip
{
        //initalizing spaceship node
        ship = [SKSpriteNode spriteNodeWithImageNamed:@"Spaceship"];
        [ship setScale:0.5];
        ship.zRotation = - M_PI / 2;

        //Adding SpriteKit physicsBody for collision detection
        ship.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:ship.size];
        ship.physicsBody.categoryBitMask = shipCategory;
        ship.physicsBody.dynamic = YES;
        ship.physicsBody.contactTestBitMask = obstacleCategory;
        ship.physicsBody.collisionBitMask = 0;
        ship.name = @"ship";
        ship.position = CGPointMake(120,160);

        [self addChild:ship];

        actionMoveUp = [SKAction moveByX:0 y:30 duration:.2];
        actionMoveDown = [SKAction moveByX:0 y:-30 duration:.2];
}

Edit initWithSize method and add [self addShip] after setting background color to white
        self.backgroundColor = [SKColor whiteColor];  
        [self addShip];
Lets go over addShip method step-by-step
        ship = [SKSpriteNode spriteNodeWithImageNamed:@"Spaceship"];
        [ship setScale:0.5];
        ship.zRotation = - M_PI / 2;
This creates a new SpriteKit node with Spaceship image and reduces the size of the image half. After that we rotate this ship so it points in the right direction.

        ship.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:ship.size]; //1
        ship.physicsBody.categoryBitMask = shipCategory; //2
        ship.physicsBody.dynamic = YES; //3
        ship.physicsBody.contactTestBitMask = obstacleCategory; //4
        ship.physicsBody.collisionBitMask = 0; //5
        ship.name = @"ship"; //6
        ship.position = CGPointMake(120,160); //7
       [self addChild:ship]; //8

        actionMoveUp = [SKAction moveByX:0 y:30 duration:.2]; //9
        actionMoveDown = [SKAction moveByX:0 y:-30 duration:.2]; //10
  1. We are creating a rectangle physics body for the ship which has same size as ship node.
  2. Sets the category bit mask to be the shipCategory you defined earlier.
  3. Setting dynamic YES means that physics engine will not control the movement of the ship
  4. contactTestBitMask means  what categories of objects this object should notify the contact listener when they intersect.
  5. On collision with missile we don’t want the ship to bounce off, so we set collisionBitMask as 0
  6. Naming ship node.
  7. Setting position of ship on screen.
  8. Adding ship as child node of the scene.
  9. Defining SKAction for moving ship up
  10. Defining SKAction for moving ship down
Before we move further we need to replace MyScene.h by following

#import <SpriteKit/SpriteKit.h>
@interface MyScene : SKScene <SKPhysicsContactDelegate>

@end
If you run the project now, it wont show the spaceship where we want it be on the screen. To fix the issue, remove all the methods from ViewController and replace it by following


- (void)viewWillLayoutSubviews{
    [super viewWillLayoutSubviews];

    SKView * skView = (SKView *)self.view;

    if (!skView.scene) {
        skView.showsFPS = YES;
        skView.showsNodeCount = YES;

        // Create and configure the scene.
        SKScene * scene = [MyScene sceneWithSize:skView.bounds.size];
        scene.scaleMode = SKSceneScaleModeAspectFill;

        // Present the scene.
        [skView presentScene:scene];
    }
}

-(BOOL)prefersStatusBarHidden{
    return YES;
}

We are now creating the scene from viewWillLayoutSubviews method instead of viewDidLoad. When viewDidLoad is called it is not aware of the layout of changes, hence it will not set the bounds of the scene correctly
iOS Simulator Screen shot Oct 28, 2013 8.02.43 PM
Now we are going to add capability to move the ship on its y axis based on where you have touched on the screen. In MyScene.m add following method

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch *touch = [touches anyObject];
    CGPoint touchLocation = [touch locationInNode:self.scene]; //1
    if(touchLocation.y >ship.position.y){ //2
        if(ship.position.y < 270){ //3             
            [ship runAction:actionMoveUp]; //4         
        }     
    }else{        
        if(ship.position.y > 50){ 
            [ship runAction:actionMoveDown]; //5
        }
    }
}
  1. When track the location of the touch on the screen
  2. If the location is higher than the ship’s location then we want to move the ship up.
  3. Setting offset from the edge so ship completely stays in bounds of the scene
  4. Calling actionMoveUp to move the ship up by 30 points
  5. Calling actionMoveDown when touch location is below ship current location

Scrolling Background

Add these static vector math methods and constants right after obstacleCategory

static const float BG_VELOCITY = 100.0; //Velocity with which our background is going to move

static inline CGPoint CGPointAdd(const CGPoint a, const CGPoint b)

{
    return CGPointMake(a.x + b.x, a.y + b.y);
}

static inline CGPoint CGPointMultiplyScalar(const CGPoint a, const CGFloat b)
{
    return CGPointMake(a.x * b, a.y * b);
}

To make endlessly scrolling background, make two background images instead of one and lay them side-by-side. Then, as you scroll both images from right to left, once one of the images goes off-screen, you simply put it back to the right.
Add this method in MyScene.m
-(void)initalizingScrollingBackground
{
    for (int i = 0; i < 2; i++) {
        SKSpriteNode *bg = [SKSpriteNode spriteNodeWithImageNamed:@"bg"];
        bg.position = CGPointMake(i * bg.size.width, 0);
        bg.anchorPoint = CGPointZero;
        bg.name = @"bg";
        [self addChild:bg];
    }

}
Call this method right after setting background color to white.
        self.backgroundColor = [SKColor whiteColor];
        [self initalizingScrollingBackground];
Next we will add method in MyScene.m to move the background
- (void)moveBg
{
    [self enumerateChildNodesWithName:@"bg" usingBlock: ^(SKNode *node, BOOL *stop)
     {
         SKSpriteNode * bg = (SKSpriteNode *) node;
         CGPoint bgVelocity = CGPointMake(-BG_VELOCITY, 0);
         CGPoint amtToMove = CGPointMultiplyScalar(bgVelocity,_dt);
         bg.position = CGPointAdd(bg.position, amtToMove);

         //Checks if bg node is completely scrolled of the screen, if yes then put it at the end of the other node
         if (bg.position.x <= -bg.size.width)
         {
             bg.position = CGPointMake(bg.position.x + bg.size.width*2,
                                       bg.position.y);
         }
     }];
}
This finds any child with the “bg” name and moves it to the left according to the velocity
Finally to scroll the background with every frame we are going add update method in MyScene.m
 -(void)update:(CFTimeInterval)currentTime {

    if (_lastUpdateTime)
    {
        _dt = currentTime - _lastUpdateTime;
    }
    else
    {
        _dt = 0;
    }
    _lastUpdateTime = currentTime;

    [self moveBg];
}

Run the project and you should have a scrolling background.
iOS Simulator Screen shot Oct 28, 2013 10.51.25 PM