Why are some of the images in this post went missing?
Today we are going to continue our work to build the famous Flappy Bird like game, Fatty Bird app for App Store.
This app use the iOS7 new feature, SpriteKit as the framework for making 2D games that comes built-in to iOS 7. It has sprite support, cool special effects like videos, filters, and masking, an integrated physics library, and a lot more to offer.
This is a first part of Day 2 of the 3 Days of the series. Check out the previous post here.
Hour 1 : Create New Project, Import Image, Setup Menu Scene
Open XCode, create a new project and choose SpriteKit as your project template.
Click “Next”. Key in your product name and make sure the Devices is set to “Universal”.
Pick a folder to store and click “Create”. Now select the top level of your project and select the “General” tab. Our game only support portrait orientation on both device, so check the Device Orientation to “Portrait” and uncheck the rest of the orientation for both iPhone and iPad.
Next, go to the App Icon section below and click on the right arrow. Drag your app icon you created yesterday to the respective slot.
Next, you are going to copy your artwork you created yesterday to the project folder. Simply drag the the folder “TextureAtlases” and drop it into the project. Make sure the “Copy items into destination group’s folder (if needed)” is checked.
We are going to have two scene for the game.
- Menu Scene : Flying bird animation with the start and rate button
- Game Scene : Scene to capture tap and move the bird around, and displaying Game Over when the bird hit anything
Create a BaseScene class that serve as the common base class of the above two scenes.
#import <SpriteKit/SpriteKit.h> #import "ViewController.h" #define IS_WIDESCREEN ( fabs( ( double )[ [ UIScreen mainScreen ] bounds ].size.height - ( double )568 ) < DBL_EPSILON ) @interface BaseScene : SKScene @property (strong, nonatomic) ViewController *viewController; - (SKTextureAtlas *)textureAtlasNamed:(NSString *)fileName share:(BOOL)share; @end
#import "BaseScene.h" @implementation BaseScene -(id)initWithSize:(CGSize)size { if (self = [super initWithSize:size]) { /* Setup your scene here */ // Add background SKTextureAtlas *imageAtlas = [self textureAtlasNamed:@"image" share:FALSE]; SKSpriteNode *background = [SKSpriteNode spriteNodeWithTexture: [imageAtlas textureNamed:@"background"]]; background.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame)); background.zPosition = 0; [self addChild:background]; CGFloat landThickness = 10; imageAtlas = [self textureAtlasNamed:@"image" share:TRUE]; // land SKSpriteNode *land1 = [self buildLand:imageAtlas]; land1.position = CGPointMake(0, landThickness); [self addChild:land1]; SKSpriteNode *land2 = [self buildLand:imageAtlas]; land2.position = CGPointMake(land1.size.width, landThickness); [self addChild:land2]; SKSpriteNode *land3 = [self buildLand:imageAtlas]; land3.position = CGPointMake(land1.size.width*2, landThickness); [self addChild:land3]; } return self; } -(SKSpriteNode*)buildLand:(SKTextureAtlas*) imageAtlas { SKSpriteNode *land = [SKSpriteNode spriteNodeWithTexture:[imageAtlas textureNamed:@"land"]]; land.anchorPoint = CGPointMake(0.0, 0.5); land.zPosition = 10; land.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:land.frame.size]; land.physicsBody.categoryBitMask = obstacleCategory; land.physicsBody.dynamic = false; land.physicsBody.collisionBitMask = playerCategory; land.name = @"land"; return land; } - (SKTextureAtlas *)textureAtlasNamed:(NSString *)fileName share:(BOOL)share { if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone) { if (IS_WIDESCREEN && !share) { // iPhone Retina 4-inch fileName = [NSString stringWithFormat:@"%@-568", fileName]; } else { // iPhone Retina 3.5-inch fileName = fileName; } } else { fileName = [NSString stringWithFormat:@"%@-ipad", fileName]; } SKTextureAtlas *textureAtlas = [SKTextureAtlas atlasNamed:fileName]; return textureAtlas; } @end
We’ve implemented the method (SKTextureAtlas *)textureAtlasNamed:(NSString *)fileName share:(BOOL)share that will handle the loading of the texture atlas image base on device and retina. The “share” parameter is there to handle if we would like to load the background for iPhone 4inch.
In (id)initWithSize:(CGSize)size , we did the following:
- Load the background image “background” and add it into the scene
- Load the land image “land” and create 3 land sprite node, arrange it one right after another
The reason why we have 3 land sprite node is to handle the moving animation of the land that will swapping around, which we are going to do in next hour.
Next, we create a new MainMenuScene class, the first scene we are going to see when we load our game.
#import <SpriteKit/SpriteKit.h> #import "BaseScene.h" @interface MainMenuScene : BaseScene @end
#import "MainMenuScene.h" @implementation MainMenuScene -(id)initWithSize:(CGSize)size { if (self = [super initWithSize:size]) { /* Setup your scene here */ } return self; } @end
We leave the setup of the menu scene to later. Before you can run the game in your device or simulator, you have one final changes need to be done in ViewController.m.
/* - (void)viewDidLoad { [super viewDidLoad]; // Configure the view. SKView * skView = (SKView *)self.view; skView.showsFPS = YES; skView.showsNodeCount = YES; // Create and configure the scene. SKScene * scene = [MyScene sceneWithSize:skView.bounds.size]; scene.scaleMode = SKSceneScaleModeAspectFill; // Present the scene. [skView presentScene:scene]; }*/ - (void)viewWillLayoutSubviews { [super viewWillLayoutSubviews]; // Configure the view. SKView * skView = (SKView *)self.view; if (!skView.scene) { // Create and configure the scene. MainMenuScene * scene = [MainMenuScene sceneWithSize:skView.bounds.size]; scene.viewController = self; scene.scaleMode = SKSceneScaleModeAspectFill; // Present the scene. [skView presentScene:scene]; } }
We did the following:
- Comment or remove the viewDidLoad method. It’s a known problem to have the scene initialized and load at this method when the app switched to horizontal orientation. Although our game is not supporting horizontal orientation, it’s always safe to do this to prevent future problem.
- Create method viewWillLayoutSubviews and move the scene initialization code to here. We will use MainMenuScene as the initial scene
Go ahead and run the the app on your device. You should able to see your first scene with background and land loaded. That’s it for this hour.
Hour 2 : Animate Land, Game Logo, Bird animation
We are going to animate our land indefinitely, both on menu and game scene. Add the following in BaseScene class.
// Define the speed for the movement of the background static const float BG_VELOCITY = 120.0; // static arithmetic method to calculate coordinate static inline CGPoint CGPointAdd(const CGPoint a, const CGPoint b) { return CGPointMake(a.x + b.x, a.y + b.y); } static inline CGPoint CGPointMultiplyScalar(const CGPoint a, const CGFloat b) { return CGPointMake(a.x * b, a.y * b); } // Move the background - (void) moveBg; // Get the time difference since last update - (double) getDt;
@implementation BaseScene { double _dt; CFTimeInterval _lastUpdateTime; } -(void)update:(CFTimeInterval)currentTime { if (_lastUpdateTime) { _dt = currentTime - _lastUpdateTime; } else { _dt = 0; } _lastUpdateTime = currentTime; [self moveBg]; } -(double)getDt { return _dt; } - (void)moveBg { [self enumerateChildNodesWithName:@"land" usingBlock: ^(SKNode *node, BOOL *stop) { float velocity = -BG_VELOCITY; SKNode * bg = (SKNode *) node; CGPoint bgVelocity = CGPointMake(velocity, 0); CGPoint amtToMove = CGPointMultiplyScalar(bgVelocity,_dt); bg.position = CGPointAdd(bg.position, amtToMove); //Checks if bg node is completely scrolled of the screen, if yes then put it at the end of the other node if (bg.position.x <= -bg.frame.size.width) { //bg.position = CGPointMake(bg.position.x + bg.size.width + 115, bg.position.y); bg.position = CGPointMake(bg.position.x + 3*(bg.frame.size.width), bg.position.y); } }]; }
Here’s what we did:
- Define the BG_VELOCITY as the constant to control the speed of the land animation. We will be using this to control the speed of the obstacle movement as well.
- The update is an internal method that will be invoked every time the application going to redrawn. We use this method to deduce the time elapse since last update
- in moveBG method, we use the time elapse since last drawn and calculate the new position of the block to move. If the first block is completely going off the left screen, we move it to the end of the third block, to create the indefinite movement effect of the land
Run your app on your device now. You should be able to see the land is moving. Adjust the BG_VELOCITY to have different speed.
Next, we are going to add the game logo in the menu scene, in the middle of the screen, about 380pt above from the origin (x=0 and y=0 point is on bottom left corner).
380pt is perfect on 3.5 inch display, but will appear too low on 4 inch and iPad display, which do not proportional according to our design plan on Day 1. We need a method to convert the point effectively. Create a new class GameUtil and add the following.
#import <Foundation/Foundation.h> @interface GameUtil : NSObject +(CGPoint)convertPoint:(CGPoint)point; + (CGPoint)convertPointByRatioOnly:(CGPoint)point; @end
#import "GameUtil.h" @implementation GameUtil + (CGPoint)convertPoint:(CGPoint)point { // portrait if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { return CGPointMake(64 + point.x*2, 32 + point.y*2); } else { if (IS_WIDESCREEN) { return CGPointMake(point.x, point.y + 44); } else { return point; } } } + (CGPoint)convertPointByRatioOnly:(CGPoint)point { // portrait if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { return CGPointMake(point.x*2, point.y*2); } else { return point; } } @end
Here’s what convertPoint method will do:
- iPhone 5 : will take the point (in 3.5 inch display) and add 44 point to push the Y-coordinate to relative center of the screen. (3.5 inch height = 480 pt, 4 inch height = 568 pt, diff = 88, so divide it to half to put it on top and bottom of the screen
- iPad : double the value of x and y (refer to Day 1 for explanation) and add x offset and y offset to push the point to the middle of the screen.
The convertPointByRatioOnly method will just multiply the point by 2 for iPad. We use this method to convert point when offset of the screen doesn’t matter. This is only useful when we are adding child into another node, not a scene. The point we use is always relative to the parent we added to.
To add the game logo in the menu scene, add the following in the MainMenuScene initWithSize method.
SKTextureAtlas *imageAtlas = [self textureAtlasNamed:@"image" share:TRUE]; // logo SKSpriteNode *fattybird = [SKSpriteNode spriteNodeWithTexture:[imageAtlas textureNamed:@"fattybird"]]; fattybird.position = [GameUtil convertPoint:CGPointMake(MID_POINT_X, 380)]; fattybird.zPosition = 1; [self addChild:fattybird];
To put it in the middle, we use the 320/2 = 160 pt. Put it in the BaseScene.h as constant to reuse it later.
static const float MID_POINT_X = 160;
Next, we are going add the bird element. Create a new class Bird and add the following:
#import <SpriteKit/SpriteKit.h> @interface Bird : SKSpriteNode @property (nonatomic, retain) NSMutableArray *flyFrames; +(id)initWithTextureAtlas:(SKTextureAtlas*)atlas; -(void)setupPhysics; -(void)flyForever; -(void)flyOnce; @end
#import "Bird.h" #import "GameUtil.h" #import "BaseScene.h" @implementation Bird +(id)initWithTextureAtlas:(SKTextureAtlas *)atlas { NSMutableArray *tempFrames = [NSMutableArray array]; for (int i=1; i <= 3; i++) { NSString *textureName = [NSString stringWithFormat:@"bird%d", i]; SKTexture *temp = [atlas textureNamed:textureName]; [tempFrames addObject:temp]; } // add the second frame to last [tempFrames addObject:[tempFrames objectAtIndex:1]]; SKTexture *temp = tempFrames[0]; Bird *bird = [Bird spriteNodeWithTexture:temp]; bird.flyFrames = tempFrames; bird.position = CGPointMake(CGRectGetMidX(bird.frame), CGRectGetMidY(bird.frame)); return bird; } -(void)setupPhysics { } -(void)flyForever { [self removeActionForKey:@"flyingbird"]; [self runAction:[SKAction repeatActionForever: [SKAction animateWithTextures:self.flyFrames timePerFrame:0.1f resize:NO restore:YES]] withKey:@"flyingbird"]; SKAction *up = [SKAction moveByX:0 y:10 duration:0.2f]; SKAction *down = [SKAction moveByX:0 y:-10 duration:0.2f]; [self runAction:[SKAction repeatActionForever:[SKAction sequence:@[up, down]]] withKey:@"floatingbird"]; } -(void)flyOnce { [self removeActionForKey:@"flyingbird"]; [self removeActionForKey:@"floatingbird"]; self.texture = self.flyFrames[0]; [self runAction:[SKAction animateWithTextures:self.flyFrames timePerFrame:0.1f resize:NO restore:YES] withKey:@"flyingbird"]; }
This is what we did:
- Setup texture frame whenever we init the object. It store the 3 bird frames (bird1.png, bird2.png, bird3.png) into array to enable the animation later. I put in the second frame (bird2.png) at the end of the array so it will have a nice animation loop that will reset back to first frame.
- The flyForever will animate the bird to flying indefinitely. This animation will be use in menu scene, and the beginning of the game scene before the game start.
- The flyOnce will animate the bird to flap it’s wing once only.
I’ll leave the setupPhysics method to implement later, which will only be used in game scene.
Now, add the following code in MainMenuScene.m class.
// bird bird = [Bird initWithTextureAtlas:imageAtlas]; bird.position = [GameUtil convertPoint:CGPointMake(MID_POINT_X, 300)]; [bird flyForever]; [self addChild:bird];
Go ahead and run the game in your device. You should be seeing the game logo and the bird flying indefinitely.
Hour 3 : Menu Button and Action
To add button in the menu scene, add the following in MainMenuScene.m class.
// start button SKSpriteNode *startButton = [SKSpriteNode spriteNodeWithTexture:[imageAtlas textureNamed:@"startbutton"]]; startButton.position = [GameUtil convertPoint:CGPointMake(MID_POINT_X, 200)]; startButton.zPosition = 1; startButton.name = @"startButton"; [self addChild:startButton]; // rate button SKSpriteNode *rateButton = [SKSpriteNode spriteNodeWithTexture:[imageAtlas textureNamed:@"ratebutton"]]; rateButton.position = [GameUtil convertPoint:CGPointMake(MID_POINT_X, 150)]; rateButton.zPosition = 1; rateButton.name = @"rateButton"; [self addChild:rateButton];
Nothing new here, except that the name we specify for each button are going to use to detect which node we touch in the screen in below code.
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *touch = [touches anyObject]; CGPoint location = [touch locationInNode:self]; SKNode *node = [self nodeAtPoint:location]; if ([node.name isEqualToString:@"startButton"]) { // change to game scene SKTransition *trans = [SKTransition fadeWithDuration:1.0]; MyScene * scene = [MyScene sceneWithSize:self.scene.view.frame.size]; scene.viewController = self.viewController; scene.scaleMode = SKSceneScaleModeAspectFill; [self.scene.view presentScene:scene transition:trans]; } if ([node.name isEqualToString:@"rateButton"]) { } }
The touchesBegan method will be called whenever we touch the screen, and then we added the code to check which node that is touch and action to be taken.
For start button, we need to transition the current scene to the game scene, so we initiate the scene just like we did in the ViewController.
We leave the rate button implementation to later. Now run your game on your phone. You should be able to touch the button and present you to the game scene. Of course it’s empty right now, and we are going to add some detail in the next hour.
Hour 4 : Game Scene, Instruction, Bird Action
Make the following changes on MyScene.
#import <SpriteKit/SpriteKit.h> #import "BaseScene.h" @interface MyScene : BaseScene @end
#import "MyScene.h" #import "MainMenuScene.h" #import "GameUtil.h" #import "Bird.h" @implementation MyScene { SKTextureAtlas *imageAtlas; SKSpriteNode *instructionNode; Bird *player; SKAction *actionMoveUp; BOOL gameover; int totalScore; } @end
We make MyScene subclass of the BaseScene we created in the previous hour, so that we can reuse the background loading and land animation code.
Next, we initialize the game world by doing the following.
-(id)initWithSize:(CGSize)size { if (self = [super initWithSize:size]) { imageAtlas = [self textureAtlasNamed:@"image" share:TRUE]; self.physicsWorld.gravity = CGVectorMake(0, -5); // default (0.0,-9.8) self.physicsWorld.speed = 1.3; // default 1.0 self.physicsWorld.contactDelegate = self; self.scaleMode = SKSceneScaleModeAspectFill; self.physicsBody = [SKPhysicsBody bodyWithEdgeLoopFromRect:self.frame]; self.physicsBody.categoryBitMask = boundaryCategory; self.physicsBody.restitution = 0; gameover = false; totalScore = 0; } return self; }
This is what we did in the code.
- Set the gravity of the game to be 5G (normal is 9.8G). This will make the falling of the bird from air slightly slower than normally would. You can adjust this to suit your game
- Set the game speed to 1.3 times. This value make the jump of the bird faster than normally would, which make the game a bit harder. You can play around the value
- We let the code know that we will handle any contact event in this class
- We define the screen edge on the device as the boundary of the game, to make sure the bird will not flying off the screen
- We assign the bit mask category to a value. Bit Mask Category basically let us group number of physics body together, and later we can define how each of this category will contact, collide with each other
- We set the restitution or bouncy of the boundary to 0, which means any object hit on the edge will not be bounce back
- Set the gameover flag to false. This flag will indicate whether the game is still on or should end
- Init the totalScore to 0. This will keep track of the current score of the game
We’re going to define 4 category bit mask like below in the BaseScene.h
static const uint32_t playerCategory = 0x1 << 0; static const uint32_t boundaryCategory = 0x1 << 1; static const uint32_t obstacleCategory = 0x1 << 2; static const uint32_t pointCategory = 0x1 << 3;
Next, we add the bird and use the flyForever animate method to animate it indefinitely until the first tap. Put the below code in the init method.
// setup bird player = [Bird initWithTextureAtlas:imageAtlas]; [player setupPhysics]; player.name = @"bird"; [player flyForever]; [self addChild:player];
We need to setup the physics of the bird. Add the following in the Bird.m.
-(void)setupPhysics { self.position = [GameUtil convertPoint:CGPointMake(100, 250)]; self.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:self.frame.size]; self.physicsBody.categoryBitMask = playerCategory; self.physicsBody.dynamic = NO; self.physicsBody.mass = 1; self.physicsBody.restitution = 0; self.physicsBody.allowsRotation = NO; self.physicsBody.contactTestBitMask = obstacleCategory; self.physicsBody.collisionBitMask = obstacleCategory | boundaryCategory; }
This is what we did in the code:
- Define the physic body of the bird to be a rectangle of the entire frame, which is the width and height of the bird image itself
- Set the dynamic to NO, which means that the bird will not fall due to gravity, and will not collide to anything. This is the initial state of the bird, but we will make it dynamic again whenever the game start
- Set the contactTestBitMask to obstacleCategory, which will trigger the -(void)didBeginContact:(SKPhysicsContact *)contact method that we are going to implement later. We will handle our logic there
- Set the collsionBitMask to obstacleCategory and boundaryCategory. This will not allow the bird to pass through the obstacle and the screen boundary
Next, we define the action of the bird, which is the short jump when we tap on the screen. Add the following in the MyScene init method.
CGFloat impulse = 300; // setup bird fly action actionMoveUp = [SKAction runBlock:^{ player.physicsBody.resting = YES; [player.physicsBody applyImpulse:CGVectorMake(0, impulse)]; [player flyOnce]; }];
We set the impulse to 300, but you can play around with the value. We also set the bird to rest whenever we tap, this is because we need the bird velocity to be neutral whenever the impulse is apply to make the jump distance consistent.
Before we are done in this hour, add the instruction image in the scene.
// instruction NSMutableArray *tempFrames = [NSMutableArray array]; for (int i=1; i <= 2; i++) { NSString *textureName = [NSString stringWithFormat:@"instruction%d", i]; SKTexture *temp = [imageAtlas textureNamed:textureName]; [tempFrames addObject:temp]; } SKTexture *temp = tempFrames[0]; instructionNode = [SKSpriteNode spriteNodeWithTexture:temp]; instructionNode.position = [GameUtil convertPoint:CGPointMake(180, 240)]; [self addChild:instructionNode]; [instructionNode runAction:[SKAction repeatActionForever: [SKAction animateWithTextures:tempFrames timePerFrame:0.3f resize:NO restore:NO]]];
The code will loop the 2 instruction image indefinitely, and will dismiss when the first tap happened later.
Go ahead and run the game in your device. By right the game scene will display the bird flying infinitely, with the flashing instruction that won’t go away no matter what you do. We will fix it in the next hour.
I’ll continue the rest of the part on next post.
Let me know your thought about this tutorial.
The post Create Flappy Bird Game in 3 Days – Day 2, Part 1 appeared first on Yoke Harn.