Sunday, March 3, 2013

Cocos2D Tutorial: How to Make a Universal Game

Hey guys.

I actually have tonnes of ideas to make tutorials on, but damnit I'm too busy making apps and games
right now. I have 2 apps in the queue and 1 game. My third game is finally using cocos2d. Yeah I know, I am late in the cocos2d bandwagon, but hey, better late than never. And sometimes, it is better
to be late as well ......... ;)


Anyway, here's what we're going to make:



So, today I want to tutor you awesome people (ie programmers, because programmers are awesome people) how to set up your cocos2d project as a UNIVERSAL APP/GAME! As normal, I write tutorials based on what I research/want to do myself. I have been googling for soooo long time to find how to set up universal cocos2d project (ie, include iPhone, iPhone Ret, iPad & iPad Retina), but I just couldn't find any.

So as noobs in cocos2d, I found some pieces of codes in the internet, and play LEGO (ie put them together) and modify it here and there and voila it works.

So first of all, we need to consider about the assets (ie graphics) size. How are we going to make the graphics for universal game that runs and looks good in all iPhone, iPhone Ret, iPad and iPad Retina?
The easiest way to do this, is to crop your "play area" in the confines of iPhone screen ratio.

So in a nutshell, background graphics will be (in pixels):
1. iPhone - 384 x 512.
2. iPhone Retina AND iPad - 768 x 1024
3. iPad Retina - 1536 x 2048

As you can see, the background graphics for iPhone will exceed its screen size, but it's ok, because
we limit the "play area" only in 320x480. Thus the "playable area" for all devices should be set to:
1. iPhone - 320 x 480
2. iPhone Retina AND iPad - 640 x 960
3. iPad Retina - 1280 x 1920

So the area that are non playable must be "padded" with any graphics to make it look nice in iPads.

For this tutorial purpose, we will be creating a platformer with an animated character in the middle.
Just a simple scene of a game. Take a look of the graphics design below. here I just create a background following to the rule we set up above. Keep in mind of the padded areas because those areas will ONLY be visible in iPads, but NOT visible in iPhones. I make the padded area obvious in this example so you can see what I mean. In the real project, use your creativity to make it not so obvious. :D



On to the coding, you need this DeviceSettings.h header. I found this code somewhere (can't remember) and I modded it a bit. This header was originally designed for iPhone, iPhone Retina and then using the iPhone Retina graphics in iPad. This is brilliant because it reduces the amount of assets you need to pack up (and amount of assets you need to make/draw) in a project.

Note that this file is only used to set the location properly (ie adjust location in iPad/iPhone).
The function ADJUST_CCP and ADJUST_XY is particularly useful when positioning/moving sprites. But it is not shown in this example.


//  Created by Emir Fithri Samsuddin on 1/31/13.
//
//

#import <UIKit/UIKit.h>

#import <UIKit/UIDevice.h>

/*  DETERMINE THE DEVICE USED  */
#ifdef UI_USER_INTERFACE_IDIOM//()
#define IS_IPAD() (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)
#else
#define IS_IPAD() (NO)
#endif



/*  NORMAL DETAILS */
#define kScreenHeight       480
#define kScreenWidth        320

/* OFFSETS TO ACCOMMODATE IPAD */
#define kXoffsetiPad        64
#define kYoffsetiPad        32



/* SD/HD Spritesheet plist */
#define SD_PLIST                @".plist"
#define HD_PLIST                @"-hd.plist"

#define SD_HD_PLIST(__filename__)   \
(IS_IPAD() == YES ?     \
[__filename__ stringByReplacingOccurrencesOfString:SD_PLIST withString:HD_PLIST] :   \
__filename__)


#define ADJUST_CCP(__p__)       \
(IS_IPAD() == YES ?             \
ccp( ( __p__.x * 2 ) + kXoffsetiPad, ( __p__.y * 2 ) + kYoffsetiPad ) : \
__p__)

 
#define ADJUST_XY(__x__, __y__)     \
(IS_IPAD() == YES ?                     \
ccp( ( __x__ * 2 ) + kXoffsetiPad, ( __y__ * 2 ) + kYoffsetiPad ) : \
ccp(__x__, __y__))


#define ADJUST_X(__x__)         \
(IS_IPAD() == YES ?             \
( __x__ * 2 ) + kXoffsetiPad :      \
__x__)

#define ADJUST_Y(__y__)         \
(IS_IPAD() == YES ?             \
( __y__ * 2 ) + kYoffsetiPad :      \
__y__)

#define HD_PIXELS(__pixels__)       \
(IS_IPAD() == YES ?             \
( __pixels__ * 2 ) :                \
__pixels__)

#define HD_TEXT(__size__)   \
(IS_IPAD() == YES ?         \
( __size__ * 1.5 ) :            \
__size__)

Just copy the whole thing and save it as DeviceSettings.h and drag and drop it into your project.

Next.... here's some meddling needed to be done to the cocos2d library.
Under your project, goto libs-> cocos2d -> ccConfig.h

Scroll down to the section where you see a string @"-hd" and add these:


#ifndef CC_RETINA_DISPLAY_FILENAME_SUFFIX
#define CC_RETINA_DISPLAY_FILENAME_SUFFIX @"-hd"
#define CC_IPAD_DISPLAY_FILENAME_SUFFIX @"-ipad"  // -- ADD
#define CC_RETIPAD_DISPLAY_FILENAME_SUFFIX @"-ipadhd" // -- ADD
#endif

Now save it. Next, go to cocos2d->libs->support->CCFileUtils.m

Replace the class method getDoubleResolutionImage with this one:


+(NSString*) getDoubleResolutionImage:(NSString*)path
{
#if CC_IS_RETINA_DISPLAY_SUPPORTED

 if( CC_CONTENT_SCALE_FACTOR() == 2 )
 {
 
        // if iPAD RETINA
        
        if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
            
            
            
            NSString *pathWithoutExtension = [path stringByDeletingPathExtension];
            NSString *name = [pathWithoutExtension lastPathComponent];
            
            // check if path already has the suffix.
            if( [name rangeOfString:CC_RETIPAD_DISPLAY_FILENAME_SUFFIX].location != NSNotFound ) {
                
                CCLOG(@"cocos2d: WARNING Filename(%@) already has the suffix %@. Using it.", name, CC_RETIPAD_DISPLAY_FILENAME_SUFFIX);
                return path;
            }
            
            
            NSString *extension = [path pathExtension];
            
            if( [extension isEqualToString:@"ccz"] || [extension isEqualToString:@"gz"] )
            {
                // All ccz / gz files should be in the format filename.xxx.ccz
                // so we need to pull off the .xxx part of the extension as well
                extension = [NSString stringWithFormat:@"%@.%@", [pathWithoutExtension pathExtension], extension];
                pathWithoutExtension = [pathWithoutExtension stringByDeletingPathExtension];
            }
            
            
            NSString *retinaName = [pathWithoutExtension stringByAppendingString:CC_RETIPAD_DISPLAY_FILENAME_SUFFIX];
            retinaName = [retinaName stringByAppendingPathExtension:extension];
            
            if( [__localFileManager fileExistsAtPath:retinaName] )
                return retinaName;
            
            
        } else {
        
        
            NSString *pathWithoutExtension = [path stringByDeletingPathExtension];
            NSString *name = [pathWithoutExtension lastPathComponent];
            
            // check if path already has the suffix.
            if( [name rangeOfString:CC_RETINA_DISPLAY_FILENAME_SUFFIX].location != NSNotFound ) {
                
                CCLOG(@"cocos2d: WARNING Filename(%@) already has the suffix %@. Using it.", name, CC_RETINA_DISPLAY_FILENAME_SUFFIX);
                return path;
            }
            
            
            NSString *extension = [path pathExtension];
            
            if( [extension isEqualToString:@"ccz"] || [extension isEqualToString:@"gz"] )
            {
                // All ccz / gz files should be in the format filename.xxx.ccz
                // so we need to pull off the .xxx part of the extension as well
                extension = [NSString stringWithFormat:@"%@.%@", [pathWithoutExtension pathExtension], extension];
                pathWithoutExtension = [pathWithoutExtension stringByDeletingPathExtension];
            }
            
            
            NSString *retinaName = [pathWithoutExtension stringByAppendingString:CC_RETINA_DISPLAY_FILENAME_SUFFIX];
            retinaName = [retinaName stringByAppendingPathExtension:extension];
            
            if( [__localFileManager fileExistsAtPath:retinaName] )
                return retinaName;
            
            CCLOG(@"cocos2d: CCFileUtils: Warning HD file not found: %@", [retinaName lastPathComponent] );
        }
        
 } else if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
        
        NSString *pathWithoutExtension = [path stringByDeletingPathExtension];
  NSString *name = [pathWithoutExtension lastPathComponent];
  
  // check if path already has the suffix.
  if( [name rangeOfString:CC_IPAD_DISPLAY_FILENAME_SUFFIX].location != NSNotFound ) {
            
   CCLOG(@"cocos2d: WARNING Filename(%@) already has the suffix %@. Using it.", name, CC_IPAD_DISPLAY_FILENAME_SUFFIX);
   return path;
  }
        
  
  NSString *extension = [path pathExtension];
  
  if( [extension isEqualToString:@"ccz"] || [extension isEqualToString:@"gz"] )
  {
   // All ccz / gz files should be in the format filename.xxx.ccz
   // so we need to pull off the .xxx part of the extension as well
   extension = [NSString stringWithFormat:@"%@.%@", [pathWithoutExtension pathExtension], extension];
   pathWithoutExtension = [pathWithoutExtension stringByDeletingPathExtension];
  }
  
        
        NSString *retinaName = [pathWithoutExtension stringByAppendingString:CC_IPAD_DISPLAY_FILENAME_SUFFIX];
  retinaName = [retinaName stringByAppendingPathExtension:extension];
        
  if( [__localFileManager fileExistsAtPath:retinaName] )
   return retinaName;

        
    }
 
#endif // CC_IS_RETINA_DISPLAY_SUPPORTED
 
 return path;
}

Basically I added codes how cocos2d uses the images according to what device.
So we are done "meddling" with the library.

Next lets setup the project as "Universal" (obviously!)

Then we need to enable Retina. Goto AppDelegate.m and add this in applicationDidFinishLaunching anywhere before running the HelloworldScene:
[director enableRetinaDisplay:YES];



Then, lets add code to add the background sprite. Put this in the init method of your HelloWorldLayer.m

       CGSize theScreenSize = [[CCDirector sharedDirector] winSize];

        CCSprite *bg = [CCSprite spriteWithFile:@"tutBg.png"];
        bg.position = ccp(theScreenSize.width/2.0, theScreenSize.height/2.0); // place at center.
        [self addChild:bg z:0];


And when you run the app in iPhone Sim and iPad sims it will look like this:


It works! Wahey! :P

Now I will show you an example of how to load spritesheets accordingly. I mean, what good is an empty scene right?

ANIMATED CHARACTER

Do you like Mortal Kombat? I do. In particular, Scorpion. So lets add him to our scene! Obviously
I'm gonna Google search for animated gif of Scorpion giving an uppercut, extract the frames and make a spritesheets out of them.

I have Zwoptex for spritesheet making (Bought it for $15 for indie license). So from the animated gifs, I created 4 spritesheets. I won't be covering how to make a spritesheet here. But if you make a game, spritesheets are a must! There are other apps to make spritesheet too, Zwoptex is one of them, there are also Texture Packer, and some others.

1. scorpion.png
2. scorpion-hd.png
3. scorpion-ipad.png
4. scorpion-ipadhd.png

Remember that 2 and 3 are the exact same spritesheets! I just create 3 and duplicate the hd or ipad.
(Note: The sprite will look not so good in iPad Retina because I upscaled it the original one by 200%).

Just for fun, lets make the character do an uppercut, everytime we tap the screen. Also, perhaps have
a swoosh sound play as well.

First of all, we need to add the spritesheets (and their corresponding .plist files) into the project.
Then we load them up using this code (put this in init method):

[[CCSpriteFrameCache sharedSpriteFrameCache] addSpriteFramesWithFile:@"scorpion.plist"];
        CCSpriteBatchNode *pieceSpriteSheet = [CCSpriteBatchNode batchNodeWithFile:@"scorpion.png"];
        [self addChild:pieceSpriteSheet];

Note that we don't need to bother anymore with the name extensions since we already set it up nicely in CCFileUtils.m earlier. Marvelous!

Now lets add Scorpion!

// create the sprite from the sprites inside the spritesheet
       scorpion = [CCSprite spriteWithSpriteFrameName:@"SC1.png"];
        scorpion.position = ccp(theScreenSize.width/2.0, theScreenSize.height/3.5);
        [self addChild:scorpion z:1];

Note that we use "spriteWithSpriteFrameName" because we want to load an image from the spritesheet and not directly from the resource files (in which case we used "spriteWithFile"). Also do declare CCSprite *scorpion in the header.

So next, we enable touches.
Pretty easy, inside your HelloWorldLayer.m init method add this code:


 [[CCTouchDispatcher sharedDispatcher] addTargetedDelegate:self priority:0 swallowsTouches:YES]; // allows touches

Then add a delegate method to capture touches and add animation to the scorpion sprite and play the animation once.


- (BOOL)ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event {
    // if u need touch location u can use this, but in this example we're not going to bother with location
    // CGPoint touchLocation = [self convertTouchToNodeSpace:touch];
    
    NSMutableArray *scorpionArray = [NSMutableArray array];
    
    // load all the sprites into the array
    for (int i=1; i<=32; i++) {
        [scorpionArray addObject: [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:[NSString stringWithFormat:@"SC%d.png",i]]];
    }
    
    CCAnimation *scorpionAnim = [CCAnimation
                                 animationWithFrames:scorpionArray delay:0.05f];
    CCAction *scorpionAction = [CCRepeat actionWithAction:
                                [CCAnimate actionWithAnimation:scorpionAnim restoreOriginalFrame:NO] times:1];
    [self.scorpion  runAction:scorpionAction];

    
    return TRUE;
}

Also, add a sound. Sounds in cocos2d are simple to use. First import #import "SimpleAudioEngine.h" and just call [[SimpleAudioEngine sharedEngine] playEffect:@"uppercut.wav"]; where uppercut.wav is the soundfile that you added to your project. So go ahead and add this line just above "return TRUE;" line in the method above.

And.. we're DONE!


Stay tune for more cocos2d tutorial. I have another one in mind that will surely be useful to a lot of us.