Sparrow-Framework. Now we are going to refine the game and actually use the OpenFeint, that we setup at the beginning.
Some of the refinements we’ll be
making.
- Adding scoring;
- Adding a pause menu;
- Improving the AI slightly;
- Add some settings
First before we begin a few notes about iPad compatibility. Although I’ve just been mentioning the AppDelegate_iPhone files, rather then both _iPhone and _iPad files, we have been using the window’s size rather then hard coding values. So you should just be able to copy and paste the code from bellow applicationDidFinishLaunching, with only 2 modifications.
- The OpenFeint initialisation code on the iPad needs to be done after [window makeKeyAndVisible];
- The game needs to initialised with “window.frame.size.width” and “window.frame.size.height” rather then ’320′ and ’480′;
First we are going to create a few new classes.
- Prefs : NSObject based singleton;
This will handle our game saves and any game settings using NSUserDefaults.
Replace the contents of the .h file we this, an NSUserDefault object, 3 properties (all ints) and 1 new method.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | @interface Prefs : NSObject { NSUserDefaults *defaults; } @property (nonatomic, assign) int currentGameMax; @property (nonatomic, assign) int currentPlayerScore; @property (nonatomic, assign) int currentAIScore; + (Prefs*) sharedInstance; -(void)activate; @end |
In the .m file we want to replace everything from #import “Prefs.h” to (and including) the method ‘sharedInstance’ with 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 | #import "Prefs.h" // --- private interface --------------------------------------------------------------------------- @interface Prefs () -(void)checkDefaults; @end // --- private interface --------------------------------------------------------------------------- static Prefs *_instance; @implementation Prefs @synthesize currentGameMax, currentPlayerScore, currentAIScore; + (Prefs*)sharedInstance{ @synchronized(self) { if (_instance == nil) { _instance = [[super allocWithZone:NULL] init]; [_instance activate]; [_instance checkDefaults]; _instance.currentGameMax = [[NSUserDefaults standardUserDefaults] integerForKey:CURRENT_GAME_MAX]; _instance.currentPlayerScore = 0; //The Game class sets this not the prefs class. _instance.currentAIScore = 0; //Again it can't be left nil but we won't set it here. } } return _instance; } -(int)currentGameMax{ return [defaults integerForKey:CURRENT_GAME_MAX]; } -(void)setCurrentGameMax:(int)i{ [defaults setInteger:i forKey:CURRENT_GAME_MAX]; [defaults synchronize]; } -(void)activate{ defaults = [NSUserDefaults standardUserDefaults]; } -(void)checkDefaults{ if (![defaults boolForKey:DEFAULTS_SET]) { self.currentGameMax = 5; } } |
- SettingsBackgroundView : UIView subclass;
This is just another gradient view (yes i now realise i should have made a single GradientView class that i can just reuse but I’ve only learnt that lesson now).
This time the class is a black gradient similar to the custom button we made before.
Again put #import “Common.h” at the top of .m and replace the drawRect method with this, which is just another gradient.
1 2 3 4 5 6 7 8 9 10 | - (void)drawRect:(CGRect)rect { // Drawing code CGContextRef context = UIGraphicsGetCurrentContext(); CGColorRef whiteColor = [UIColor colorWithRed:60.0/255.0 green:60.0/255.0 blue:60.0/255.0 alpha:0.85].CGColor; CGColorRef lightGrayColor = [UIColor colorWithRed:0.0/255.0 green:0.0/255.0 blue:0.0/255.0 alpha:0.85].CGColor; CGRect paperRect = self.bounds; drawLinearGradient(context, paperRect, whiteColor, lightGrayColor); } |
- PauseScreen : SPSprite subclass;
PauseScreen.h
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 #import "CoolWhiteView.h" #import "Sparrow.h" @protocol PauseScreenDelegate @required -(void)unPause; -(void)restart; -(void)quit; @end @interface PauseScreen : SPSprite { id delegate; SPSprite *innerSprite; int actualWidth; int actualHeight; } @property (nonatomic, assign) id delegate; -(id)initWithHeight:(int)h andWidth:(int)w; -(void)showAnimated:(BOOL)yesOrNo; @end |
Now this class will handle the entire pause screen for us so we don’t clutter the Game.m file with setting it up.
At the top of the file we create a custom protocol (List of methods a class will have if it implements that protocol) for our pause screens delegate.
For the pause screen i wanted some nice graphics like we have in the reset of the app, for that however we are mixing Sparrow and UIKit, sort of.
(This is actually the same way you can take screen shots of normal apps.)
PauseScreen.m
The PauseScreen.m file is too big to add here but as the project is on github you can view the file here.
In the init method we are creating the buttons, text and also noting what height and width we are meant to have (SPSprite’s width and height properties just give you its bounds and aren’t fixed).
Then we have 3 methods that return a UIImage for the graphics, and 4 very small methods to handle the buttons.
Now that the pause screen class is setup and we have the extra classes we need we’ll drive back into the Game class and make some refinements.
Going Back Over What We’ve Done
In the Game.h file we need to:
- #import the “Prefs.h” and “PauseScreen.h” files;
- On the @interface line add <PauseScreenDelegate> after “SPSprite” but before {;
Inside the @interface{/*….*/} add these 3 more sparrow objects and 2 int’s.
1 2 3 4 5 6 | SPSprite *infoSprite; SPTextField *p1ScoreFeild; SPTextField *p2ScoreFeild; /*.........*/ int p1Score; int p2Score; |
They are: a sprite to hold them all in; 2 SPTextFields to display the players scores; and then the int’s the players scores are stored in.
In the Game.m file we need to make lots of changes.
- In the private interface delete -(void)pause; and put -(void)pauseAnimated:(BOOL)yesOrNo; in the .h file under the @property line;
- Change kAISpeed to ((4.5/320.0)*self.width) instead of 3.5;
- In the init method after [line release]; add
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 | /************ TopBar Section : START ***********/ infoSprite = [[SPSprite alloc] init]; //Create the sprite SPQuad *infoBackground = [SPQuad quadWithWidth:self.width height:25]; infoBackground.color = 0x000000; //Give it a Black background. [infoSprite addChild:infoBackground]; //Add the background to the sprite. /***Create the text fields.***/ p1ScoreFeild = [[SPTextField alloc] initWithText:@"Player1: 0"]; p1ScoreFeild.fontName = @"Marker Felt"; p1ScoreFeild.fontSize = 15; p1ScoreFeild.vAlign = SPVAlignTop; p1ScoreFeild.hAlign = SPHAlignLeft; p1ScoreFeild.width = self.width*0.25; p1ScoreFeild.height = 20; p1ScoreFeild.x = ((5.0/320.0)*self.width); p1ScoreFeild.y = 5; p1ScoreFeild.color = 0xFFFFFF; p1ScoreFeild.alpha = 0.8; [infoSprite addChild:p1ScoreFeild]; p2ScoreFeild = [[SPTextField alloc] initWithText:@"AI: 0"]; p2ScoreFeild.fontName = @"Marker Felt"; p2ScoreFeild.fontSize = 15; p2ScoreFeild.vAlign = SPVAlignTop; p2ScoreFeild.hAlign = SPHAlignLeft; p2ScoreFeild.width = self.width*0.25; p2ScoreFeild.height = 20; p2ScoreFeild.x = ((5.0/320.0)*self.width)+(p1ScoreFeild.x+p1ScoreFeild.width); p2ScoreFeild.y = 5; p2ScoreFeild.color = 0xFFFFFF; p2ScoreFeild.alpha = 0.8; [infoSprite addChild:p2ScoreFeild]; /***Finish creating the text fields.***/ SPSprite *pauseButton = [[SPSprite alloc] init]; //Create a sprite to hold the 2 pause button parts. SPQuad *pause1 = [SPQuad quadWithWidth:5 height:15]; //Half a pause button. [pauseButton addChild:pause1]; SPQuad *pause2 = [SPQuad quadWithWidth:5 height:15]; //The other half. pause2.x = pause1.width+5; [pauseButton addChild:pause2]; pauseButton.x = self.width-pauseButton.width-10; //Position the pause button. pauseButton.y = 5; [infoSprite addChild:pauseButton]; //Add the pause screen to the topbar's sprite. [pauseButton release]; //Release it. [self addChild:infoSprite]; //And the topbar to the screen. SPQuad *buttonArea = [SPQuad quadWithWidth:50 height:50]; //Make a bigger hit area for the pause button then the actual pause button itself. buttonArea.alpha = 0.0; //Make it invisible as we only want it as a hit catcher. buttonArea.x = self.width-buttonArea.width; [buttonArea addEventListener:@selector(pausePressed:) atObject:self forType:SP_EVENT_TYPE_TOUCH]; //Add the touch event to this not the actual pause button. [self addChild:buttonArea]; /************ TopBar Section : END ***********/ //.........later on in the init....... //Increase the second paddles height by the infoSprite's height. paddle2.y = (self.height*0.025)+infoSprite.height; //.........later on, at the bottom... //Default the players scores the 0. p1Score = 0; p2Score = 0; |
Here we are just setting up the top bar that displays the scores, and setting the players scores to 0 (read through the comments for a run through of what we are doing there).
Improving the gameplay
Now we are going to try making the game better to play (and give you a change of even winning).
To do that we will change a the ball and the AI.
Go into the movementController: method and under the existing code add this little code snippet.
1 2 3 4 5 6 7 | static int frameCount = 0; static double totalTime = 0; totalTime += event.passedTime; if (++frameCount % 60 == 0){ ballXSpeed += ballXSpeed*0.02; ballYSpeed += ballYSpeed*0.02; } |
Once a second this will slightly increase the balls speed so that it’ll keep gaining speed the longer the game goes on.
(this little block of code was actually taken from Sparrow’s website and was for calculating frames per second originally)
The next controller method down is the collisionController: and although this is fine as it is we need to make a few changes for the InfoBar we added.
1 2 3 4 | //In the second 'if else' block replace. else if (ball.y <= 0 && ballYSpeed < 0) //.........with...... else if (ball.y <= infoSprite.height && ballYSpeed < 0) |
This now makes the edge of the info sprite the top wall rather then the edge of the screen.
Also in that same ‘if else’ block we need to now do more then just change the balls direction. When the ball hits the end wall that player lost and the other player gains a point so that block should now look like.
1 2 3 4 5 6 7 8 9 10 11 12 13 | //Check the ball's top & bottom if ((ball.y+ball.height) >= self.height && ballYSpeed >= 0) { ballYSpeed = -ballYSpeed; p2Score++; //Increase the score. p2ScoreFeild.text = [NSString stringWithFormat:@"AI: %d",p2Score]; //Update the text. [self gameOver]; //Call game over which will check itself if it really is game over. } else if (ball.y <= infoSprite.height && ballYSpeed < 0){ ballYSpeed = -ballYSpeed; p1Score++; //Increase the score. p1ScoreFeild.text = [NSString stringWithFormat:@"Player1: %d",p1Score]; //Update the text. [self gameOver]; //Call game over which will check itself if it really is game over. } |
Whenever it hits the end the other player gets a point, the on screen score is updated and the gameover method is called (which checks itself if it really is).
Now the AIController:
There are lots of changes to this one in an attempt to make the game more playable.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | -(void)AIController:(SPEnterFrameEvent*)event{ if (!gamePaused) { static BOOL tweenStarted = NO; if ([ball.bounds intersectsRectangle:player2Side]) { //NSLog(@"AI should be moving"); if (((ball.x+ball.width)*0.5) < ((paddle2.x+paddle2.width)*0.5) && (paddle2.x) > 0) { paddle2.x += (-(kAISpeed) * (((arc4random() % 8)*0.1) + 0.7)); } else if (((ball.x+ball.width)*0.5) > ((paddle2.x+paddle2.width)*0.5) && (paddle2.x+paddle2.width) < self.width) { paddle2.x += (kAISpeed * (((arc4random() % 8)*0.1) + 0.7)); } tweenStarted = NO; } else { if (!tweenStarted) { tweenStarted = YES; SPTween *moveToMiddle = [SPTween tweenWithTarget:paddle2 time:0.75]; [moveToMiddle animateProperty:@"x" targetValue:((self.width*0.5)-(paddle2.width*0.5))]; [self.juggler addObject:moveToMiddle]; } } } } |
To start of the changes we have a static boolean set to no. (static means if i run this method once and set that BOOL to YES then when i call this method again it will still be YES rather then having disappeared with the end of the method)
Then we are moving the paddles by a random amount between 70% and 150% of its normal speed.
If the ball isn’t on the AI’s side of the screen and the tween hasn’t already started we create a tween to quickly move the paddle back into the center of the screen.
Pausing & Restarting The Game
We added a touch event listener to the the area near the pause button so now we need to actually handle that being pressed.
1 2 3 4 5 6 | -(void)pausePressed:(SPTouchEvent*)event{ SPTouch *touch = [[event touchesWithTarget:self] anyObject]; if (touch.phase == SPTouchPhaseEnded) { [self pauseAnimated:YES]; } } |
Now if the button is pressed the game will pauseAnimated: will be called. The reason for the animated bit is the same reason it was moved into the .h file. So that other classes (the appDelegate in our case) can pause the game (when it is put into the background or the device is locked).
1 2 3 4 5 6 7 8 9 | -(void)pauseAnimated:(BOOL)yesOrNo{ gamePaused = YES; PauseScreen *pauseScreen = [[PauseScreen alloc] initWithHeight:self.height andWidth:self.width]; pauseScreen.delegate = self; [self addChild:pauseScreen]; [pauseScreen showAnimated:yesOrNo]; [pauseScreen release]; } |
This one is incredibly simple (yet again). All we do is set the boolean ‘gamePaused’ to YES and everything stops. The we let the pauseScreen animate into view.
The PauseScreenDelegate Protocol
Now we have the pause screen displaying and communicating with the game class through it’s delegate. However we now need to create the 3 methods a pauseScreen’s delegate is required to have. ‘unPause’, ‘restart’ and ‘Quit’
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 | -(void)unPause{ gamePaused = NO; } -(void)restart{ //Reset Score p1Score = 0; p2Score = 0; //Reset Score Text p1ScoreFeild.text = [NSString stringWithFormat:@"Player 1: %d",p1Score]; p2ScoreFeild.text = [NSString stringWithFormat:@"AI: %d",p2Score]; //Reset Ball Speed ballYSpeed = -((5.0/480.0)*self.height); ballXSpeed = -((5.0/320.0)*self.width); //Reset ball position ball.x = (self.width/2-ball.width/2); ball.y = (self.height/2-ball.height/2); //Position the paddles paddle1.x = (self.width*0.5)-(paddle1.width*0.5); paddle2.x = (self.width*0.5)-(paddle2.width*0.5); //Unpause Game gamePaused = NO; } -(void)quit{ [delegate removeGameView]; } |
Lastly all we need to do is create the GameOver method and update the dealloc method.
This gets called any time the ball bounces off the top or bottom walls so we need to actually check the current score against what the Prefs class tells us the game is meant to go up to.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | -(void)gameOver{ if (p1Score >= [Prefs sharedInstance].currentGameMax || p2Score >= [Prefs sharedInstance].currentGameMax) { gamePaused = YES; [self restart]; } } - (void)dealloc { NSLog(@"Game Released"); [ball release]; [paddle1 release]; [paddle2 release]; [guideX1 release]; [guideY1 release]; [infoSprite release]; [player1Side release]; [player2Side release]; [p1ScoreFeild release]; [p2ScoreFeild release]; [super dealloc]; } |
And very lastly in the appDelgate add [game pauseAnimated:NO]; just above [sparrowView stop]; in the applicationWillResignActive: and applicationDidEnterBackground: methods.
Now everything is almost done:
- We have the game playing up to a specified amount of points;
- As the game goes on the ball goes faster and faster;
- The AI is now at least beatable if still not very good;
- You can pause and un-pause the game.
Coming In Part 5
- The Credits, About and Settings Pages;
- Adding a smoother ending then just having the game simply restart at the end;
- Implementing some basic OpenFient achievements.
Hope you are enjoying the tutorial so far.