Monday, 15 August 2011

How to make a local leaderboard

If your not up for implementing Game Center into your app, you can easily just create a simple local score system using plists.

Create a plist like this
The way I have my levels formated is, there are 3 "cups"; easy, medium and hard. Each cup has a certain amount of levels in them, and then each level will have its scores.

To read/write to the plist, I created a class which handles everything for us. Here is the code
@implementation scoreAndSaveClass

NSString* pathOfPlist;

+(void) getPathOfScoreList{
    
    NSError *error;
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); 
    NSString *documentsDirectory = [paths objectAtIndex:0]; 
    pathOfPlist = [documentsDirectory stringByAppendingPathComponent:@"scoresFile.plist"]; 
    
    NSFileManager *fileManager = [NSFileManager defaultManager];
    
    if (![fileManager fileExistsAtPath: pathOfPlist]) 
    {
        NSString * bundle = [[NSBundle mainBundle] pathForResource: @"scoresFile" ofType:@"plist"];
        
        [fileManager copyItemAtPath:bundle toPath: pathOfPlist error:&error]; 
    }
}

+(void) setLevelScore:(float) inScore andTheDifficulty:(NSString*) difficulty andTheLevel:(int) level{
     //get path of plist
    [self getPathOfScoreList];
    
    NSMutableDictionary * indexOfPlist = [[[NSMutableDictionary alloc] initWithContentsOfFile:pathOfPlist] autorelease];

    //gets dictionary of levels from this difficulty
    NSMutableDictionary * indexOfLevels = [indexOfPlist objectForKey:difficulty];
    
    //gets the array of scores for this level
    NSMutableArray *arrayOfLevelScores = [indexOfLevels objectForKey:[NSString stringWithFormat:@"Level%i", level]];

    //adds the users newest score to the array
    [arrayOfLevelScores addObject:[NSNumber numberWithFloat:inScore]];
    
    //updates the cup's dictionary to be the array with the latest values
    [indexOfLevels setValue:arrayOfLevelScores forKey:difficulty];

    //saves changes
    [indexOfPlist writeToFile: pathOfPlist atomically:YES];

    
}

+(NSArray*) getSortedScore:(NSString*) difficulty andTheLevel:(int) level{
    //get path of plist
    [self getPathOfScoreList];
    
    NSMutableDictionary * indexOfPlist = [[[NSMutableDictionary alloc] initWithContentsOfFile:pathOfPlist] autorelease];

    //gets the array of score depends on what cup is selected
    NSMutableDictionary * indexOfLevels = [indexOfPlist objectForKey:difficulty];
    
    //gets the array of score depends on what level is selected
    NSMutableArray *arrayOfLevelScores = [indexOfLevels objectForKey:[NSString stringWithFormat:@"Level%i", level]];
    
    //this sorted the array in ascending order
    NSSortDescriptor * temp= [[NSSortDescriptor alloc] initWithKey:@"doubleValue" ascending:NO];
    [arrayOfLevelScores sortUsingDescriptors:[NSArray arrayWithObject:temp]];
        
    return arrayOfLevelScores;
    
@end

 
To read/write to the plist, you call the required function. At the start of each function it calls the "get path" function which will set the string to be the location of the plist. Because each plist is structured as dictionary, we use dictionaries to browse through the plist.

Whenever you call the function to read/write a score you must pass a string into the function, which you will set to be either "Easy", "Medium" or "Hard" (at least it is for me, depends on how you structured your plist).

To display the latest score I created just a simple loop that loops the top 10 score, or if there is less than 10 scores, it lists all the scores it can.

int maxNumOfScores = MIN([scoreArray count], 15);
        
        for (int i = 0; i <maxNumOfScores; i++) {
         
            CCLabelTTF * scoreLabel = [CCLabelTTF labelWithString:[NSString stringWithFormat:@"Score %i", [[scoreArray objectAtIndex:i] intValue]] fontName:@"Marker Felt" fontSize:20];
            [self addChild:scoreLabel];
            scoreLabel.position = ccp(240, (280 - i*20));
            
        }

Wednesday, 10 August 2011

Updated, how to scroll and pinch-zoom a tiled map

So, silly me, I didn't realise the original code I posted earlier in this blog doesn't work for big maps. This is because in big maps, the layer slides down to the bottom left, which would ruin the boundaries.

 So instead I came up with a method where you get the top right and the bottom left corners of the tiled map, finds their co-ordinates, then converts it to world space.

From here, the boundaries are drawn up where in the code below, I have taken into account a 40x320 UI bar running up the right side of the screen. My code is tried and tested on tiled maps of varying size, hope it will help someone.

- (void)ccTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event

{
    
    
    if (hasPickedButton == NO) {
        
        // "allObjects" returns an NSArray of all the objects in the set
        NSArray *touchArray = [touches allObjects];
        
        
        
        // Only run the following code if there is more than one touch, wanting to resize
        if ([touchArray count] > 1)
        {
            
            
            // We're going to track the first two touches (i.e. first two fingers)
            // Create "UITouch" objects representing each touch
            UITouch *fingerOne = [touchArray objectAtIndex:0];
            UITouch *fingerTwo = [touchArray objectAtIndex:1];
            
            // Convert each UITouch object to a CGPoint, which has x/y coordinates we can actually use
            CGPoint pointOne = [fingerOne locationInView:[fingerOne view]];
            CGPoint pointTwo = [fingerTwo locationInView:[fingerTwo view]];
            
            CGPoint pointOnePrev = [fingerOne previousLocationInView:[fingerOne view]];
            CGPoint pointTwoPrev = [fingerTwo previousLocationInView:[fingerTwo view]];
            
            // The touch points are always in "portrait" coordinates
            // You will need to convert them if in landscape (which we are)
            pointOne = [[CCDirector sharedDirector] convertToGL:pointOne];
            pointTwo = [[CCDirector sharedDirector] convertToGL:pointTwo];
            
            pointOnePrev = [[CCDirector sharedDirector] convertToGL:pointOnePrev];
            pointTwoPrev = [[CCDirector sharedDirector] convertToGL:pointTwoPrev];
            
            CGPoint point1Map = [mapLayer convertToNodeSpace:pointOne];
            CGPoint point2Map = [mapLayer convertToNodeSpace:pointTwo];
            CGPoint midPoint = ccpMidpoint(point1Map, point2Map);
            
            mapLayer.anchorPoint = ccp(midPoint.x/mapWidth, midPoint.y/mapWidth);
            
            
            //this statement finds the difference in the pinches that the player is doing now, and the pinch the player was doing
            
            //if they were pinching in, subtract the distance of the pinch from the distance of the previous pinch
            
            if ((sqrt(pow(pointOnePrev.x - pointTwoPrev.x, 2.0) + pow(pointOne.y - pointTwo.y, 2.0))) > sqrt(pow(pointOne.x - pointTwo.x, 2.0) + pow(pointOne.y - pointTwo.y, 2.0))) {
                distance -= sqrt(pow(pointOne.x - pointTwo.x, 2.0) + pow(pointOne.y - pointTwo.y, 2.0))/100;
                distance = fabsf(distance);
            }
            //otherwise they are pinching out and so add on more to the distance
            else  if ((sqrt(pow(pointOnePrev.x - pointTwoPrev.x, 2.0) + pow(pointOne.y - pointTwo.y, 2.0))) < sqrt(pow(pointOne.x - pointTwo.x, 2.0) + pow(pointOne.y - pointTwo.y, 2.0))) {
                distance += sqrt(pow(pointOne.x - pointTwo.x, 2.0) + pow(pointOne.y - pointTwo.y, 2.0))/100;
                distance = fabsf(distance);
            }
            
            // Get the distance between the touch points
            
            // Scale the distance based on the overall width of the screen (multiplied by a constant, just for effect)
            mapScale = distance / [CCDirector sharedDirector].winSize.width * 3;
            
            
            //we don't want the player to be able to zoom out to the point where the map is smaller than the screen
            float minFactor = MAX(440.0/mapWidth, 320.0/mapHeight);
            
            
            if (mapScale <= minFactor) {
                //set this here, so if the player keeps zooming out out out, even though it doesn't change the scale factor, the distance is always decremented. this line keeps it to the last distance that had any effect, stops jumping camera
                distance = lastGoodDistance;
            }
            //we don't want him zooming in larger than the original image
            
            if (mapScale>1.0) {
                distance = lastGoodDistance;
            }
            
            if ((mapScale< 1.0) && (mapScale > minFactor)){
                
                //set the last distance which had any affect to be the current one
                
                lastGoodDistance= distance;

                //allow the zoom to take place, but don't permentatly apply it just yet, we want to make sure the final scaled map is within the bounds of the view before moving it (otherwise it appears shakey)
                float oldscale = mapLayer.scale;
                [mapLayer setScale:mapScale];
                          //get the co-ordinates of the BL and TR tiles for this scale in wordspace, then put scale back to normal for now
                
                CGSize mapTiles = [levelMap mapSize];
                float y = mapTiles.height;
                float x = mapTiles.width;
                
                CCSprite * grid = [mapBgLayer tileAt:ccp(0, y-1)];
                CGPoint bottomLeftGrid = grid.position;
                bottomLeftGrid = [levelMap convertToWorldSpace:bottomLeftGrid];
                
                grid = [mapBgLayer tileAt:ccp(x-1, 0)];
                CGPoint topRightGrid = grid.position;
                
                topRightGrid = [levelMap convertToWorldSpace:topRightGrid];
                
                          //restore the old mapscale until we sort out the position
                mapLayer.scale = oldscale;
                
                //check to see if the layer is drifting off the screen while the zoom is taking place
                
                CGPoint difference = ccp(0, 0);
                
                float degreeOfAwkard = (1- mapScale)*10;
                
                float awkardOffsetX = degreeOfAwkard *3.51;
                float awkardOffsetY = degreeOfAwkard *3.5;
                
                if (bottomLeftGrid.x > 0) {
                    
                    float offBy = 0 - bottomLeftGrid.x;
                    difference.x += offBy;
                    
                }
                
                if (bottomLeftGrid.y > 0) {
                    float offBy = 0 - bottomLeftGrid.y;
                    difference.y += offBy;
                    
                }
                
                if (topRightGrid.x < 400 + awkardOffsetX) {
                    
                    float offBy = 400 - topRightGrid.x + awkardOffsetX;
                    difference.x += offBy;
                }
                
                if (topRightGrid.y < 280 + awkardOffsetY) {
                    float offBy = 280 - topRightGrid.y + awkardOffsetY;
                    difference.y += offBy;
                    
                }
                
                
                mapLayer.position = ccpAdd(mapLayer.position, difference);
                //      [mapLayer runAction:[CCMoveBy actionWithDuration:0.05 position:difference]];
                mapLayer.scale = mapScale;
                
                
            }
            
        }  
        
        
        if ([touchArray count] ==1){
            
            CGSize mapTiles = [levelMap mapSize];
            float y = mapTiles.height;
            float x = mapTiles.width;
            
            CCSprite * grid = [mapBgLayer tileAt:ccp(0, y-1)];
            CGPoint bottomLeftGrid = grid.position;
            bottomLeftGrid = [levelMap convertToWorldSpace:bottomLeftGrid];
            
            grid = [mapBgLayer tileAt:ccp(x-1, 0)];
            CGPoint topRightGrid = grid.position;
            
            topRightGrid = [levelMap convertToWorldSpace:topRightGrid];
            
            
            UITouch * fingerOne = [touchArray objectAtIndex:0];
            
            CGPoint newTouchLocation = [fingerOne locationInView:[fingerOne view]];
            newTouchLocation = [[CCDirector sharedDirector] convertToGL:newTouchLocation];
            
            CGPoint oldTouchLocation = [fingerOne previousLocationInView:fingerOne.view];
            oldTouchLocation = [[CCDirector sharedDirector] convertToGL:oldTouchLocation];
            
            //get the difference in the finger touches when the player was dragging
            CGPoint difference = ccpSub(newTouchLocation, oldTouchLocation);
            
            //adds this on to the layers current position, effectively moving it
            CGPoint bottomLeft = ccpAdd(mapLayer.position, difference);
            
            //check to see if the map edges of the map are showing in the screen, if so bringing them back on the view so no black space can be seen
            
            bottomLeft = ccpAdd(mapLayer.position, difference);
            
            bottomLeftGrid = ccpAdd(bottomLeftGrid, difference);
            
            
            topRightGrid = ccpAdd(topRightGrid, difference);
            //don't ask why, but the boundary changes by about 3pxls per 0.1 scale
            float degreeOfAwkard = (1- mapScale)*10;
            
            float awkardOffsetX = degreeOfAwkard *3.51;
            float awkardOffsetY = degreeOfAwkard *3.5;
            
            
            if (bottomLeftGrid.x > 0) {
                //finds how much the map is off the boundary
                float offBy = 0 - bottomLeftGrid.x;
                difference.x += offBy;
                
            }
            
            if (bottomLeftGrid.y > 0) {
                float offBy = 0 - bottomLeftGrid.y;
                difference.y += offBy;
                
            }
            
            if (topRightGrid.x < 400 + awkardOffsetX) {
                
                float offBy = 400 - topRightGrid.x + awkardOffsetX;
                difference.x += offBy;
            }
            
            if (topRightGrid.y < 280 + awkardOffsetY) {
                float offBy = 280 - topRightGrid.y + awkardOffsetY;
                difference.y += offBy;
                
            }
            //adjusts the map so it is within the boundary just
            mapLayer.position = ccpAdd(mapLayer.position, difference);
            
        }
        
    }
}

Tuesday, 9 August 2011

How to easily keep track of save games

This is just a simple method on how to create local save files for a level system. For the app I'm making, there are several levels and the player only unlocks the next level if the previous one is complete. To do this I'm just using plists.

Create plists with this sort of structure. There will be 1 plist for saves.




Now create a class which will be used to manage this data.

//
//  scoreAndSaveClass.h
//  Ant Run
//
//  Created by Peter Lockhart on 05/08/2011.
//  Copyright 2011 __MyCompanyName__. All rights reserved.
//

#import <Foundation/Foundation.h>


@interface scoreAndSaveClass : NSObject {

}
+(void) getPathOfSaveList;
+(BOOL) checkCompletedLevels:(int) thisIndex andTheDifficulty:(NSString*) difficulty;
+(void) setLevelCompleted:(int) thisIndex andTheDifficulty:(NSString*) difficulty;


@end

//
//  scoreAndSaveClass.m
//  Ant Run
//
//  Created by Peter Lockhart on 05/08/2011.
//  Copyright 2011 __MyCompanyName__. All rights reserved.
//

#import "scoreAndSaveClass.h"


@implementation scoreAndSaveClass

NSString* pathOfPlist;

+(void) getPathOfSaveList{
    
    NSError *error;
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); 
    NSString *documentsDirectory = [paths objectAtIndex:0]; 
    pathOfPlist = [documentsDirectory stringByAppendingPathComponent:@"saveFiles.plist"]; 
    
    NSFileManager *fileManager = [NSFileManager defaultManager];
    
    if (![fileManager fileExistsAtPath: pathOfPlist]) 
    {
        NSString * bundle = [[NSBundle mainBundle] pathForResource: @"saveFiles" ofType:@"plist"];
        
        [fileManager copyItemAtPath:bundle toPath: pathOfPlist error:&error]; 
    }
}


+(BOOL) checkCompletedLevels:(int) thisIndex andTheDifficulty:(NSString*) difficulty{
    //set the string  of plathofPlist to be that of the level plist
    [self getPathOfSaveList];
    
    //this is the root of the plist
    NSMutableDictionary * indexOfPlist = [[[NSMutableDictionary alloc] initWithContentsOfFile:pathOfPlist] autorelease];
    
    //get the array under the level category (defined by the string "difficulty", which is set when the method is called)
    NSMutableArray *arrayOfLevelSaves = [indexOfPlist objectForKey:difficulty];
    //get the boolean for the level that needs checked
    BOOL test = [[arrayOfLevelSaves objectAtIndex:thisIndex] boolValue];
    
    //check if level is completed
    if (test == YES) {
        return YES;
    }
    return NO;
}

+(void) setLevelCompleted:(int) thisIndex andTheDifficulty:(NSString*) difficulty{
    [self getPathOfSaveList];
    
    NSMutableDictionary * indexOfPlist = [[[NSMutableDictionary alloc] initWithContentsOfFile:pathOfPlist] autorelease];
    
    NSMutableArray *arrayOfLevelSaves = [indexOfPlist objectForKey:difficulty];
    
    //create a temp boolean to replace the original value of the array of saves
    BOOL done = YES;
    //make the level specified be completed
    [arrayOfLevelSaves replaceObjectAtIndex:thisIndex withObject:[NSNumber numberWithBool:done]];

    //update the dictionary in the plist so that its array of save files is the newest version
    [indexOfPlist setValue:arrayOfLevelSaves forKey:difficulty];
    
    //write the plist to memory
    [indexOfPlist writeToFile: pathOfPlist atomically:YES];
    
}
@end

Don't be put off by it, its really not too bad.

I've declared a string at the top of the .m file, which will hold the path of the save file. This will change depending on if you are accessing the save file plist or the scores plist (which will be in a later tutorial). The getPathOfSaveList function will not get called by you, so don't worry about remembering to call it (saves headache when we implement local scores later).

The code in the function is pretty self explanatory. It searches in the iphones memory to find the specified file and if it doesn't exists then it creates the file (but it should always exist because we created the plist in xcode in the project file).


 

Friday, 5 August 2011

How to sort an array in ascending/descending order, easily!

So many people create their own functions to loop through an array and comparing the values to each other and sorting it that way, which takes far too much effort for me! No one seems to know about this on google, cause I searched for ages trying to find an easy way to do this. Simply do;

NSMutableArray *arrayOfLevelSaves = //your array full of numbers;
    
NSSortDescriptor * temp= [[NSSortDescriptor alloc] initWithKey:@"doubleValue" ascending:NO];
[arrayOfLevelSaves sortUsingDescriptors:[NSArray arrayWithObject:temp]];