Friday, 29 July 2011

Just beware of...

..your textures not being included in your tmxtiledmap whenever you are using cocos2d. I was using the java and it had the option to included all tilesets in the tmx file, however this does not work and you need to have the .png of your tileset in the xcode resources with the map itself. 

Also, if your tileset files weren't part of the same root folder as the map when you were making it, it won't know where to look for the right file. To correct this, whenever you import the tmx map into xcode resources, click on it and it should show xml code and edit the 4th line to read;

  <image source="yourspritesheet" width="NUMBER" height="NUMBER"/>.  

You would get the error:
exec_arithmetic error on rect.origin.x = (gid % max_x) * (tileSize_.width + spacing_) + margin_;

Thursday, 28 July 2011

How to scroll and zoom in/out of a map using motions

So for my first cocos2d game I'm making a tiled map based game. I wanted the player to be able to zoom in and out freely, and to drag and move the level around the camera if it was too big to fit all on the screen.

There was little google help on this, and after days of torture I've got the code working for it, which I hope will help someone.

The code is fully commented and hopefully people can read through it and understand it. The player can pinch-zoom and scroll across the map, as long as the map isn't smaller than the actual screen (otherwise the map would show black spacing).

Everything is just maths to do with the scaling of the original size of the layer and the scaled layer. However when the layer is fully scaled out, there is a slight off set, which I think is just down to fliddly bits in xcode and it can't be helped. To solve this, I did an offset which should fix the offset and make the level have correct boundries: if (bottomLeft.x + mapWidth*mapScale +differenceX/2 < 440+ (1-mapScale)*33) {

where "+ (1-mapScale)*33)" is the fiddly bit.
http://twitter.com/#!/ant_run


//
//
//  Created by petemyster on 21/07/2011.
//  Copyright petemyster. All rights reserved.
//

// When you import this file, you import all the cocos2d classes
#import "cocos2d.h"

@interface easyLevel1 : CCLayer
{
    //map
    CCTMXTiledMap * testMap;
    CCTMXLayer * testLayer;
    CCLayer * mapLayer;
    //ui
    CCLayer * UILayer;
    CCSprite* UIBackground;
    
    //declarations for return to main menu
    CCMenu * returnMenu;
    CCMenuItemImage * returnMenuImage;
    
}
+(CCScene *) scene;
-(void) goBackToMain;
-(void) update:(ccTime) dt;

@property (nonatomic, retain) CCSprite * testSprite;

//map
@property (nonatomic, retain) CCTMXTiledMap * testMap;
@property (nonatomic, retain) CCTMXLayer * testLayer;
@property (nonatomic, retain) CCLayer * mapLayer;
@property int mapWidth;
@property int mapHeight;
@property float mapScale;
@property float distance;
@property float lastGoodDistance;
//UI
@property (nonatomic, retain) CCLayer * UILayer;
@property (nonatomic, retain) CCSprite* UIBackground;

//properties for the return to main menu screen
@property (nonatomic, retain) CCMenu * returnMenu;
@property (nonatomic, retain) CCMenuItemImage * returnMenuImage;

@end


// Import the interfaces
#import "easyLevel1.h"
#import "easyLevelSelect.h"
@implementation easyLevel1
@synthesize testMap,mapLayer, testLayer;
@synthesize  UILayer, UIBackground;
@synthesize returnMenu, returnMenuImage;
@synthesize mapWidth, mapHeight, mapScale, distance, lastGoodDistance;
+(CCScene *) scene;
{
    // 'scene' is an autorelease object.
    CCScene *scene = [CCScene node];
    
    // 'layer' is an autorelease object.
    easyLevel1 *layer = [easyLevel1 node];
    
    // add layer as a child to scene
    [scene addChild: layer];
    
    // return the scene
    return scene;
}
// on "init" you need to initialize your instance
-(id) init
{
    // always call "super" init
    // Apple recommends to re-assign "self" with the "super" return value
    if( (self=[super init])) {
        
        self.isTouchEnabled = YES;
        
        //declare the map and add to a layer
        mapLayer = [CCLayer node];
        testMap = [CCTMXTiledMap tiledMapWithTMXFile:@"testMap.tmx"];
        testLayer = [testMap layerNamed:@"Test Layer 1"];
        [mapLayer addChild:testMap];
        [self addChild:mapLayer];
        
        //get map size in pixels
        mapHeight = testMap.contentSize.height;
        mapWidth = testMap.contentSize.width;
        //set up defaults
        mapLayer.position= ccp(0, 0);
        //this value works well for the calculation later, trial and error really
        distance = 150;
        lastGoodDistance = 150;
        mapScale = 1;
        
        //set up a layer for the UI
        UILayer = [CCLayer node];
        UIBackground = [CCSprite spriteWithFile:@"UIbackgroundImage.png"];
        NSLog(@"%@", UIBackground);
        UIBackground.position= ccp(460, 160);
        [UILayer addChild:UIBackground];
        
        //set up labels for return menu
        returnMenuImage = [CCMenuItemImage itemFromNormalImage:@"backNavigationArrow.png" selectedImage:@"backNavigationArrowPressed.png" target:self selector:@selector(goBackToMain)];
        returnMenu = [CCMenu menuWithItems:returnMenuImage, nil];
        [self addChild:UILayer];
        [UILayer addChild:returnMenu];
        [returnMenu alignItemsVerticallyWithPadding:1];
        [returnMenu setPosition:ccp(460, 300)];
        
    }
    return self;
}
-(void) goBackToMain{
    
    [[CCDirector sharedDirector] replaceScene:[CCTransitionFade transitionWithDuration:0.5 scene:[easyLevelSelect scene:gameMode]]];
    
}
- (void)ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
}
// Override the "ccTouchesMoved:withEvent:" method to add your own logic
- (void)ccTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    //finds the difference in the original map and the scaled map
    int differenceX = mapWidth - (mapWidth*mapScale);
    int differenceY = mapHeight - (mapHeight*mapScale);    
    
    // This method is passed an NSSet of touches called (of course) "touches"
    // "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];
        
        //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) {
            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){
            mapScale = 1.0;
            distance = lastGoodDistance;
            
        }    
        
        //set the last distance which had any affect to be the current one
        lastGoodDistance= distance;
        //allow the zoom to take place
        mapLayer.scale=mapScale;
        
        CGPoint bottomLeft = mapLayer.position;
        
        //check to see if the layer is drifting off the screen while the zoom is taking place
        
        if (bottomLeft.x - differenceX/2 > 0 - (mapWidth * (1-mapScale))) {
            bottomLeft.x = differenceX/2- (mapWidth * (1-mapScale));
        }
        
        if (bottomLeft.y - differenceY/2 > 0 -(mapHeight * (1-mapScale))) {
            bottomLeft.y = differenceY/2-(mapHeight * (1-mapScale));
            
        }
        
        if (bottomLeft.x + mapWidth*mapScale +differenceX/2 < 440) {
            bottomLeft.x = -differenceX/2 - mapWidth*mapScale + 440;
            
        }
        if (bottomLeft.y + mapHeight*mapScale +differenceY/2 < 320) {
            bottomLeft.y =  -differenceY/2 - mapHeight*mapScale + 320;
            
        }
        //set the position of the layer
        mapLayer.position = bottomLeft;
        
    }
    
    if ([touchArray count] ==1){
        
        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 newPosition = ccpAdd(mapLayer.position, difference);
        
        CGPoint bottomLeft = newPosition;
        
        //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
        if (bottomLeft.x - differenceX/2 > 0 - (mapWidth * (1-mapScale)) + (1-mapScale)*33) {
            bottomLeft.x = differenceX/2- (mapWidth * (1-mapScale))+(1-mapScale)*33;
        }
        
        if (bottomLeft.y - differenceY/2 > 0 -(mapHeight * (1-mapScale))) {
            bottomLeft.y = differenceY/2-(mapHeight * (1-mapScale));
            
        }
        
        if (bottomLeft.x + mapWidth*mapScale +differenceX/2 < 440+ (1-mapScale)*33) {
            bottomLeft.x = -differenceX/2 - mapWidth*mapScale + 440 + (1-mapScale)*33;
            
        }
        if (bottomLeft.y + mapHeight*mapScale +differenceY/2 < 320) {
            bottomLeft.y =  -differenceY/2 - mapHeight*mapScale + 320;
            
        }
        
        mapLayer.position = bottomLeft;
    }
}
- (void)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
}
// on "dealloc" you need to release all your retained objects
- (void) dealloc
{
    // in case you have something to dealloc, do it in this method
    // in this particular example nothing needs to be released.
    // cocos2d will automatically release all the children (Label)
    
    // don't forget to call "super dealloc"
    
    [super dealloc];
}
@end

Sunday, 24 July 2011

How to easily (in comparison!) set up a looping menu for level select screens

So, I have tried a lot of times to find good material about this. There are so many threads asking for help about this, and a lot of them have solutions but when you go to put it together or open their projects its all so outdated and filled with warnings and errors.

 They are also really complicated, so someone who's new like me doesn't understand it at all and so it sort of becomes a black box. So I thought I'd come up with my own solution. All the code is fully commented and explained.

//
// test
//
// Created by petemyster on 21/07/2011.
// Copyright petemyster. All rights reserved.
//


// Import the interfaces
#import "easyLevelSelect.h"
#import "difficultySelectLayer.h"

// easyLevelSelect implementation
@implementation easyLevelSelect

@synthesize beginTouchPoint, movingTouchPoint, originPoint;
@synthesize arrayOfLevels, numOfLevels, differencePoint, retardationNum, spacingBetweenLevels;

+(CCScene *) scene
{
// 'scene' is an autorelease object.
CCScene *scene = [CCScene node];
// 'layer' is an autorelease object.
easyLevelSelect *layer = [easyLevelSelect node];
// add layer as a child to scene
[scene addChild: layer];
// return the scene
return scene;
}

// on "init" you need to initialize your instance
-(id) init
{
if( (self=[super init])) {
self.isTouchEnabled = YES;
//add the images for the looping menu
//set the number of levels
numOfLevels = 7;
//the space between the images (image size + spacing you want)
spacingBetweenLevels = 150;
//allocate memory for the mutable array, do initwithcapacity because just init has problems sometimes
arrayOfLevels = [[NSMutableArray alloc] initWithCapacity:1];
for(int i = 0; i < numOfLevels; i++)
{
//loop through all the levels, creating a sprite and the specified image
//uses stringwithformat, the %d is replaced with the number of the loop +1, because my image numbers start at number 1
CCSprite * item = [CCSprite spriteWithFile:[NSString stringWithFormat:@"testPicPreview-%d.png", i+1] ];
//set the position of the sprite. mine start off on the far right of the screen (320) then are spaced according to my spacing and the number of sprites that have appeared before it
item.position = ccp(320 +((i-1)*spacingBetweenLevels), 150);
//set a tag for the sprite so we can identify it later
item.tag = i+1;
//add the sprite to the view
[self addChild:item];
//add sprite to array so we can cycle through all sprites later
[arrayOfLevels addObject:item];
}
//this is the slowing amount for when scrolling through the menu, see later
retardationNum = 1.0;
//this constantly checks for movement and moves the menu
[self schedule:@selector(accelerateLabel) interval:0.05];

}
return self;
}

-(void) accelerateLabel{
//if is it moving ridicously slowly, stop it
if (fabs(differencePoint.x) < 1) {
differencePoint.x = 0;
}
//if the menu is moving
if (differencePoint.x !=0) {
//if its moving ridiclously fast, slow it down
if (fabs(differencePoint.x) > 30) {
differencePoint.x = differencePoint.x * 0.75;
}

//if it is moving to the right, slow it down
if (differencePoint.x >0) differencePoint.x -=retardationNum;
//if its moving to the left, slow it down
if (differencePoint.x <0) differencePoint.x +=retardationNum;
//move the menu by the "strength" of the swipe
[self moveMenuItem:differencePoint.x];
}
//reset variables
else
{
differencePoint = ccp(0, 0);
}
}

-(void) callMe:(CCSprite*) sender{
//takes the tag of the sprite that was selected, and checks for an appropiate action
switch (sender.tag) {
case 1:NSLog(@"lol");
break;
case 2: NSLog(@"lol 2");
break;
case 3:[[CCDirector sharedDirector] replaceScene:[CCTransitionFade transitionWithDuration:0.5 scene:[difficultySelectLayer scene]]];
break;
//etc etc etc
}
}

-(void) registerWithTouchDispatcher {
[[CCTouchDispatcher sharedDispatcher] addTargetedDelegate:self priority:0 swallowsTouches:YES];
}


-(BOOL) ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event{

//gets the touch location on the screen
beginTouchPoint= [touch locationInView: [touch view]];
beginTouchPoint = [[CCDirector sharedDirector] convertToGL:beginTouchPoint];
beginTouchPoint = [self convertToNodeSpace:beginTouchPoint];
//store the very first starting point, we will need this later as beginTouchPoint is changed throughout
originPoint = beginTouchPoint;
return true;
}

-(void) ccTouchMoved:(UITouch *)touch withEvent:(UIEvent *)event{
//get the point that the player is currently pressing as they move
movingTouchPoint = [self convertTouchToNodeSpace:touch];
//get the difference of positions between the last touch point, and the current move point
differencePoint = ccpSub(beginTouchPoint, movingTouchPoint);
//set the last touch point to equal the current, so the line of code above is always subtracting the latest points next time it is called
beginTouchPoint = movingTouchPoint;
}

-(void) ccTouchEnded:(UITouch *)touch withEvent:(UIEvent *)event{
//get the position of the final touch
CGPoint locationPressed = [touch locationInView: [touch view]];
locationPressed = [[CCDirector sharedDirector] convertToGL:locationPressed];
//subtract the final touch location from the very first touch location
CGPoint difference = ccpSub(originPoint, locationPressed);

//if the distance between the points was very small, the player is wanting to press the button instead of finishing their swipe
if (fabs(difference.x) <15) {
//cycle through our array of sprites
for (int i = 0; i < numOfLevels; i++) {
CCSprite * item = [arrayOfLevels objectAtIndex:i];
// if the location that the player touched is within the sprites picture...
if (CGRectContainsPoint(item.boundingBox, locationPressed)) {
// call the function that will find the specific action for that sprite
[self callMe:item];
}
}
}
}


-(void) moveMenuItem:(float) velocity{
//defining the first sprite in the array and the last sprite of the array
CCSprite * testBoundItemMax = [arrayOfLevels objectAtIndex:numOfLevels-1];
CCSprite * testBoundItemMin = [arrayOfLevels objectAtIndex:0];

//allow movement of the sprites as long as they don't go off screen
if (((velocity>0) && (testBoundItemMax.position.x> 225)) || ((velocity<0) && (testBoundItemMin.position.x< 225))) {

for(int i = 0; i < numOfLevels; i++)
{
//move the sprites one by one
CCSprite * item = [arrayOfLevels objectAtIndex:i];
item.position = ccp(item.position.x + (-velocity), item.position.y);

}
}

}

// on "dealloc" you need to release all your retained objects
- (void) dealloc
{
// in case you have something to dealloc, do it in this method
// in this particular example nothing needs to be released.
// cocos2d will automatically release all the children (Label)
// don't forget to call "super dealloc"
//loop menu
[arrayOfLevels release];
[super dealloc];
}
@end


and....


// test
//
// Created by petemyster on 21/07/2011.
// Copyright petemyster. All rights reserved.
//


// When you import this file, you import all the cocos2d classes
#import "cocos2d.h"


@interface easyLevelSelect : CCLayer
{
//set up the looping menu
CGPoint beginTouchPoint;
CGPoint movingTouchPoint;
CGPoint originPoint;
CGPoint differencePoint;
NSMutableArray * arrayOfLevels;

}

// returns a CCScene that contains the HelloWorldLayer as the only child
+(CCScene *) scene;
-(void) callMe:(CCSprite*) sender;
-(void) moveMenuItem:(float) velocity;
-(void) accelerateLabel;


//set up looping menu
@property (nonatomic) CGPoint beginTouchPoint;
@property (nonatomic) CGPoint movingTouchPoint;
@property (nonatomic) CGPoint originPoint;
@property CGPoint differencePoint;
@property (nonatomic, retain) NSMutableArray * arrayOfLevels;
@property int numOfLevels;
@property int spacingBetweenLevels;
@property float retardationNum;


@end