From Flash to iPad: The iPad Part

Now it’s time to bring the same game to the iPad, and once again I will use Cocos2D as a framework to manage OpenGL. If I have time later I may post a version of the same game but without Cocos2D; using instead classes from the now defunct CrashLanding sample project from Apple.

I’ll assume you know how to create a Cocos2D project, I called mine Outlaw, and I will also assume you are using XCode 4.

It might also help you to do my other tutorials on Objective-C and Cocos2D before reading this one.

Right from the start we need to make some changes to the project. I set the target to iPad only and I set it to be fixed with a Landscape orientation. You do this by configuring the Target in XCode (by clicking on the first icon in your project tree).

Next comes the one change we need to make to AppDelegate.m. In the event handler applicationDidFinishLaunching, right after the EAGLView object is created (cocos2d calls this object glView) you need to add the following line:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
 
 
 
#import "GameHeaders.h"
 
#import "MovingSprite.h"
 
 
 
@interface Bullet : MovingSprite {
 
 
 
 int _shooter;
 
}
 
 
 
-(id) initWithWorld:(GameStage*)world withShooter:(int)shooter;
 
-(void) startBulletAtPosition:(CGPoint) position withDirection:(int) direction;
 
 
 
@end

This will allow, as the name sugests, for the application to handle multiple touches. And that’s it for the AppDelegate. If you want to remove the frame rate display you can make the change here too:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
 
 
 
#import "Bullet.h"
 
#import "Cactus.h"
 
 
 
 
 
@implementation Bullet
 
 
 
-(id) initWithWorld:(GameStage*)world withShooter:(int)shooter {
 
 
 
    self = [super initWithPosition:CGPointZero andWorld:world];
 
 
 
    if (self) {
 
 
 
        _shooter = shooter;
 
        _speed = 12;
 
        _vx = _vy = 0;
 
        if (_shooter == LEFT_PLAYER) {
 
            _vx = _speed + 2;
 
        } else {
 
            _vx = - (_speed + 2); 
 
        }
 
        _active = NO;
 
 
 
        CCSpriteFrame *frame = [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:@"bullet.png"];
 
 
 
        _skin = [[CCSprite spriteWithSpriteFrame:frame] retain];  
 
        [_skin setAnchorPoint:CGPointMake(0.0f, 1.0f)];
 
 
 
        _width = frame.rect.size.width;
 
        _height = frame.rect.size.height;
 
 
 
        _skin.visible = NO;
 
 
 
        [_world.stage addChild:_skin];
 
    }
 
 
 
    return self;
 
}
 
 
 
-(void) startBulletAtPosition:(CGPoint) position withDirection:(int) direction {
 
    _skin.position = position;
 
    _nextX = _x = position.x;
 
    _nextY = _y = position.y;
 
 
 
    if  (direction == DIRECTION_UP) {
 
        _vy = _speed;
 
    } else if (direction == DIRECTION_STRAIGHT) {
 
        _vy = 0;
 
    } else if  (direction == DIRECTION_DOWN) {
 
        _vy = -_speed;
 
    }
 
    _skin.visible = YES;    
 
    _active = YES;
 
 
 
}
 
 
 
 
 
-(void) update:(ccTime)dt {
 
 
 
    [super update:dt];
 
 
 
 
 
	//if touching vertical walls, remove it
 
   if (_nextX < WALL_THICKNESS) {
 
		_nextX = WALL_THICKNESS;
 
		[self destroy];
 
		return;
 
	}
 
 
 
	if ([self next_right] > SCREEN_WIDTH - WALL_THICKNESS) {
 
		_nextX = SCREEN_WIDTH - WALL_THICKNESS - _width;
 
		[self destroy];
 
		return;
 
	}
 
 
 
	//if touching cactus, remove it
 
    if ([_world.cactus collidesWith:[self bounds]]) {
 
        [self destroy];
 
         return;
 
     }
 
 
 
 
 
    //if touching top wall, ricochet
 
	if (_nextY > COURT_HEIGHT - WALL_THICKNESS) {
 
		_nextY = COURT_HEIGHT - WALL_THICKNESS;
 
		_vy *= -1;
 
	}
 
 
 
	//if touching bottom wall, ricochet
 
	if ([self next_bottom] < WALL_THICKNESS) {
 
		_nextY = WALL_THICKNESS + _height;
 
		_vy *= -1;
 
	}
 
 
 
}
 
 
 
-(void) destroy {
 
	_active = NO;
 
    _skin.visible = NO;
 
}
 
 
 
@end

The actual game is created in the line:

1
2
3
4
 
// Run the intro Scene
 
[[CCDirector sharedDirector] runWithScene: [Outlaw scene]];

So now, let me show you the Outlaw class. Its interface looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 
#import "cocos2d.h"
 
#import "GameStage.h"
 
 
 
@interface Outlaw : CCLayer
 
{
 
    BOOL _paused;
 
    GameStage *_world;
 
}
 
+(CCScene *) scene;
 
 
 
@end

The implementation looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
 
 
 
#import "Outlaw.h"
 
 
 
@implementation Outlaw
 
 
 
+(CCScene *) scene
 
{
 
	// 'scene' is an autorelease object.
 
	CCScene *scene = [CCScene node];
 
 
 
	// 'layer' is an autorelease object.
 
	Outlaw *layer = [Outlaw 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])) {
 
        _paused = NO;
 
 
 
        CCSpriteBatchNode * _stage;
 
        [GameVars setDeviceType];
 
 
 
        if ([GameVars isIPad]) {
 
            [[CCSpriteFrameCache sharedSpriteFrameCache] addSpriteFramesWithFile:
 
             @"game_sheet.plist"];
 
            _stage = [CCSpriteBatchNode 
 
                      batchNodeWithFile:@"game_sheet.png"];
 
 
 
 
 
            ccColor4B c = {45,50,184,255};
 
            CCLayerColor *bg = [[[CCLayerColor alloc] initWithColor:c width:SCREEN_WIDTH height:SCREEN_HEIGHT] retain];
 
            [self addChild:bg];
 
            [bg release];
 
 
 
        } else {
 
            //NSLog(@"USING IPHONE SPRITES!");
 
            /*
 
             logic for iPhone implementation...
 
             */
 
        }
 
 
 
        [self addChild:_stage];
 
        _world = [[[GameStage alloc] initWithStage:_stage] retain]; 
 
 
 
        //add scheduled update
 
        //[self schedule:@selector(update:)];
 
        [self scheduleUpdate];
 
 
 
        //add input handling
 
        self.isTouchEnabled = YES;
 
    }
 
	return self;
 
}
 
 
 
-(void) update:(ccTime) dt {
 
    if (_paused) return;
 
    [_world update:20*dt];
 
}
 
 
 
-(void) ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
 
    [_world processTouchEnd:touches];    
 
}
 
 
 
- (void) dealloc
 
{
 
	[super dealloc];
 
}
 
@end

First, notice that the main game layer, the Outlaw class, is the equivalent here to the Document Class.

The majority of the logic is handled in the init method.

Here I create the SpriteBatchNode layer that is associated with one source image (and one source image only) in this case the game’s only sprite sheet.

If you did my previous tutorial on Cocos2D you know that a SpriteBatchNode layer can only contain textures from one source PNG file. So in a game you will tipically create a sprite batch node for menus, a sprite batch node for the main game, and if the game requires a particle system you will also create a layer for that (particles must exist in their own separate layer in Cocos2D). If your game uses a lot of graphics and you can’t shove them all inside one PNG file 2048 x 2048 pixels big (and even that will work only in some devices…) you might end up using more than one sprite batch node for your game play screen. But you should be careful with how many layers you create.

The reason for all this is that in OpenGL you bind a texture to the OpenGL’s equivalent of a brush. Before you paint anything, you have to first bind the texture you will use on an OpenGl view (canvas) and then paint with it. If you require a different texture (a different file) you must drop the previously bound texture and bind the new one instead before you paint with it. These series of binding actions is what may cause trouble to the processor and should be the focus of your optimization efforts.

In this game I use only one sprite sheet, the same used for the Flash version. I didn’t create menus, or instruction screens, so I use a very small number of graphics. But in a more complex game I could have:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 
_bg = [CCSpriteBatchNode batchNodeWithFile:@"bg_tile.png"];
 
_stage = [CCSpriteBatchNode batchNodeWithFile:@"game_sheet.png"];
 
_menu = [[CCSpriteBatchNode batchNodeWithFile:@"menu_sheet.png"] retain];
 
CCLayer *_particleLayer = [[CCLayer node] retain];
 
 
 
[self addChild:_bg];
 
[self addChild:_stage];
 
[self addChild:_particleLayer];
 
[self addChild:_menu];

In this case I could only fit the PNG files for the textures in three separate PNGs. One for the game play, one for menus and one for the background.

In the Outlaw class I also added a method to check if the device is an iPad. I added this just as an example, because if you wanted to support the game for the iPhone then you would probably need to create the SpriteBatchNode layers using different PNG files for each device.

So I begin by creating a color filled background and loading the SpriteFrameCache with the information from the Sprite sheet.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 
[CCSpriteFrameCache sharedSpriteFrameCache] addSpriteFramesWithFile:
 
 @"game_sheet.plist"];
 
_stage = [CCSpriteBatchNode batchNodeWithFile:@"game_sheet.png"];
 
 
 
ccColor4B c = {45,50,184,255};
 
CCLayerColor *bg = [[[CCLayerColor alloc] initWithColor:c width:SCREEN_WIDTH height:SCREEN_HEIGHT] retain];
 
[self addChild:bg];
 
[bg release];

Then I create the GameStage object which holds a reference to the “stage”, in this case the SpriteBatchNode layer.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 
[self addChild:_stage];
 
_world = [[[GameStage alloc] initWithStage:_stage] retain]; 
 
 
 
//add scheduled update
 
[self scheduleUpdate];
 
 
 
//add input handling
 
self.isTouchEnabled = YES;

The last thing I do is I start the main loop by registering to an update event and I set this layer to receive touches. This is a major difference to the way I did things in Flash, but it follows the same logic. I need a Node object in Cocos2D to capture touches and register a loop, whereas in Flash I needed a display object to register a loop and capture key events. But here, I managed to remove the notion of a GameController entirely.

The methods that updates the game and receives touches are redirected to the GameStage object (_world):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 
-(void) update:(ccTime) dt {
 
    if (_paused) return;
 
    [_world update:20*dt];
 
}
 
 
 
-(void) ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
 
    [_world processTouchEnd:touches];    
 
}

I don’t use PAUSE logic in this game, but if you require it for your project, then you would need to add logic to handle it in the AppDelegate as well as inside the actual game. For instance, if I wanted to allow pause in this game and so be able to change the boolean value of the variable _paused, I would create a method in Outlaw called pauseGame and in the AppDelegate I would add:

1
2
3
4
5
6
7
8
9
10
11
12
 
- (void)applicationWillResignActive:(UIApplication *)application {
 
    CCScene *runningScene =  [[CCDirector sharedDirector] runningScene];
 
    Outlaw * outlaw = [runningScene.children objectAtIndex:0];
 
    [outlaw pauseGame];
 
    [[CCDirector sharedDirector] pause];
 
}

This allows the game to be paused when the player returns to it after leaving the game momentarily. It is always important to pay attention to the events related to application cycle. Cocos2D does an excellent job for you by using its Director object to handle the changes within the application cycle, but if decide later to build your own framework, so check out all those events in AppDelegate.