Vielen Dank, dass du meinen Artikel geöffnet hast. Es würde mir viel bedeuten, wenn du ihn zu Ende lesen würdest und mir Rückmeldung gibst. Es geht um Chatbots, Conversational Commerce, die Facebook Messenger Platform und ein Produkt, das ich entwickelt habe.
Read MoreLatest Posts
How to build a game like Flappy Bird with Xcode and SpriteKit
Update Feb. 27th: Fixed a small glitch in the tutorial that DylanYasen pointed out, thanks! (No code change.)
Update Apr. 9th: Sorry, I currently don’t have time to update the tutorial. In the meantime, take a look what reader Daniel Bocksteger built with the learnings from this tutorial: Coin Catcher.
Graphic assets
Since the app was already removed from the stores when I started working on the clone, I had to rely on screenshots and videos of the original game to reproduce the individual sprites. I used The Gimp to produce the bird (two simple frames for animation), the pipes (upper and lower version), the distant background and the ground.
Note that the created files are a little too small for an iPhone display. Therefore, in the course of this tutorial, you’ll see that I simply doubled the size by scaling every sprite by a factor of 2. In order to maintain the 8 bit retro look I changed the filtering mode of the textures from SKTextureFilteringLinear
(standard) to SKTextureFilteringNearest
.
You can download the created files with the final project.
Xcode project setup
iOS 7 comes with a great new framework called Sprite Kit. It hides lots of the iOS UI development details game programmers are not necessarily familiar with (outlets anyone?) and builds upon a basic game infrastructure they are are more used to (e.g. rendering loop and scene graph). For all iOS developers, focussing on the game mechanics becomes a breeze.
When Xcode starts, simply select “Create a new Xcode project” from the splash screen.
From the available project templates, select “SpriteKit Game”.
Now enter some project details. You can change them later if you need to.
Select a place to store the project files. I usually create a new subfolder in my Documents folder.
Finally, the project is created and we’re ready for development.
The bird
The template project comes with some sample code and assets that need to be removed. Delete the Spaceship.png graphic from the project and add the Bird1.png and Bird2.png files instead. Also remove the existing code in the initWithSize
and touchesBegan
methods of MyScene.m.
In initWithSize
, create the bird sprite and add it to the scene. To access the bird easily from other class methods, we declare it as an ivar.
#import "MyScene.h" @interface MyScene () { SKSpriteNode* _bird; } @end @implementation MyScene -(id)initWithSize:(CGSize)size { if (self = [super initWithSize:size]) { /* Setup your scene here */ SKTexture* birdTexture1 = [SKTexture textureWithImageNamed:@"Bird1"]; birdTexture1.filteringMode = SKTextureFilteringNearest; _bird = [SKSpriteNode spriteNodeWithTexture:birdTexture1]; [_bird setScale:2.0]; _bird.position = CGPointMake(self.frame.size.width / 4, CGRectGetMidY(self.frame)); [self addChild:_bird]; } return self; } -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { /* Called when a touch begins */ } -(void)update:(CFTimeInterval)currentTime { /* Called before each frame is rendered */ } @end
One important thing to remember is that the SpriteKit’s scene coordinate system has it’s origin at the bottom left point and the positive y- and x-axes extend to the top and right of the screen. A sprite’s default anchor point is always the center. Using the code above, the bird is placed on the vertical center and end of the horizontal first quarter of the screen.
Hooray, we’re nearly there! It won’t get much harder than that, believe me.
Animating the bird
Animating is pretty easy thanks to SpriteKit Actions. We simply create a sequence of texture-changes and loop them endlessly with a little delay in order to produce the illusion of flapping wings. I believe that the original game used some more frames, but we’ll go with two for the sake of simplicity.
We’ll have to load the second bird sprite, too, produce an SKAction
to animate them, and attach this animation to the bird.
SKTexture* birdTexture1 = [SKTexture textureWithImageNamed:@"Bird1"]; birdTexture1.filteringMode = SKTextureFilteringNearest; SKTexture* birdTexture2 = [SKTexture textureWithImageNamed:@"Bird2"]; birdTexture2.filteringMode = SKTextureFilteringNearest; SKAction* flap = [SKAction repeatActionForever:[SKAction animateWithTextures:@[birdTexture1, birdTexture2] timePerFrame:0.2]]; _bird = [SKSpriteNode spriteNodeWithTexture:birdTexture1]; [_bird setScale:2.0]; _bird.position = CGPointMake(self.frame.size.width / 4, CGRectGetMidY(self.frame)); [_bird runAction:flap];
This results in a simple animation like this:
Adding ground and background
Most of the screen background is a bright blue sky. Let’s fill the screen accordingly. We store the sky color in an ivar for later…
@interface MyScene () { SKSpriteNode* _bird; SKColor* _skyColor; } @end @implementation MyScene -(id)initWithSize:(CGSize)size { if (self = [super initWithSize:size]) { /* Setup your scene here */ _skyColor = [SKColor colorWithRed:113.0/255.0 green:197.0/255.0 blue:207.0/255.0 alpha:1.0]; [self setBackgroundColor:_skyColor];
The background and ground sprites are repeating patterns. After adding Skyline.png and Ground.png to the project, we setup the textures first and then fill the full screen width with the sprites.
// Create ground SKTexture* groundTexture = [SKTexture textureWithImageNamed:@"Ground"]; groundTexture.filteringMode = SKTextureFilteringNearest; for( int i = 0; i < 2 + self.frame.size.width / ( groundTexture.size.width * 2 ); ++i ) { SKSpriteNode* sprite = [SKSpriteNode spriteNodeWithTexture:groundTexture]; [sprite setScale:2.0]; sprite.position = CGPointMake(i * sprite.size.width, sprite.size.height / 2); [self addChild:sprite]; } // Create skyline SKTexture* skylineTexture = [SKTexture textureWithImageNamed:@"Skyline"]; skylineTexture.filteringMode = SKTextureFilteringNearest; for( int i = 0; i < 2 + self.frame.size.width / ( skylineTexture.size.width * 2 ); ++i ) { SKSpriteNode* sprite = [SKSpriteNode spriteNodeWithTexture:skylineTexture]; [sprite setScale:2.0]; sprite.zPosition = -20; sprite.position = CGPointMake(i * sprite.size.width, sprite.size.height / 2 + groundTexture.size.height * 2); [self addChild:sprite]; }
This leads to the following static scene.
You may have noticed that I created one more tile than necessary. This is required for the animation: We can create the illusion of movement by simply moving each sprite to the left for a full width and then resetting it to the original position. When the tiles wander to the left, a gap opens at the right edge of the screen. We use the extra tile to fill that gap.
// Create ground SKTexture* groundTexture = [SKTexture textureWithImageNamed:@"Ground"]; groundTexture.filteringMode = SKTextureFilteringNearest; SKAction* moveGroundSprite = [SKAction moveByX:-groundTexture.size.width*2 y:0 duration:0.02 * groundTexture.size.width*2]; SKAction* resetGroundSprite = [SKAction moveByX:groundTexture.size.width*2 y:0 duration:0]; SKAction* moveGroundSpritesForever = [SKAction repeatActionForever:[SKAction sequence:@[moveGroundSprite, resetGroundSprite]]]; for( int i = 0; i < 2 + self.frame.size.width / ( groundTexture.size.width * 2 ); ++i ) { // Create the sprite SKSpriteNode* sprite = [SKSpriteNode spriteNodeWithTexture:groundTexture]; [sprite setScale:2.0]; sprite.position = CGPointMake(i * sprite.size.width, sprite.size.height / 2); [sprite runAction:moveGroundSpritesForever]; [self addChild:sprite]; } // Create skyline SKTexture* skylineTexture = [SKTexture textureWithImageNamed:@"Skyline"]; skylineTexture.filteringMode = SKTextureFilteringNearest; SKAction* moveSkylineSprite = [SKAction moveByX:-skylineTexture.size.width*2 y:0 duration:0.1 * skylineTexture.size.width*2]; SKAction* resetSkylineSprite = [SKAction moveByX:skylineTexture.size.width*2 y:0 duration:0]; SKAction* moveSkylineSpritesForever = [SKAction repeatActionForever:[SKAction sequence:@[moveSkylineSprite, resetSkylineSprite]]]; for( int i = 0; i < 2 + self.frame.size.width / ( skylineTexture.size.width * 2 ); ++i ) { SKSpriteNode* sprite = [SKSpriteNode spriteNodeWithTexture:skylineTexture]; [sprite setScale:2.0]; sprite.zPosition = -20; sprite.position = CGPointMake(i * sprite.size.width, sprite.size.height / 2 + groundTexture.size.height * 2); [sprite runAction:moveSkylineSpritesForever]; [self addChild:sprite]; }
Two layers of the scene are scrolling with different pace (speed scale factors 0.1 and 0.02 above), leading to an illusion of depth. This effect has been used in 2D video games for ages and is called parallax scrolling.
Let’s add some physics
Until now, everything in the scene is just fake. The bird doesn’t even move. But we can easily enable the SpriteKit physics engine in order to add some realistic movement to the scene.
SpriteKit allows to attach a physics body to every scene node. The physics body has certain properties like mass, density, friction,… and it can interact with other physics-enabled entities in the scene. Once a physics body is created, the owner node is controlled by physics.
Let’s start with the bird. We will represent it’s physical shape by a circle.
_bird = [SKSpriteNode spriteNodeWithTexture:birdTexture1]; [_bird setScale:2.0]; _bird.position = CGPointMake(self.frame.size.width / 4, CGRectGetMidY(self.frame)); [_bird runAction:flap]; _bird.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius:_bird.size.height / 2]; _bird.physicsBody.dynamic = YES; _bird.physicsBody.allowsRotation = NO; [self addChild:_bird];
When we start our clone now, we’ll see the bird falling straight down – and out of the screen. In order to avoid that, we will have to create a physics body for the ground as well. We could attach it to each of the ground tiles, but remember that they’re continuously moving left and then resetting their position. This could lead to some strange effects once the bird collides with such a ground tile. So instead, we can simply create an empty node with a rectangular physics body which spans the full ground.
Add this below the creation of the ground sprites:
// Create ground physics container SKNode* dummy = [SKNode node]; dummy.position = CGPointMake(0, groundTexture.size.height); dummy.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:CGSizeMake(self.frame.size.width, groundTexture.size.height * 2)]; dummy.physicsBody.dynamic = NO; [self addChild:dummy];
Note that the bird has property dynamic
set to YES
, while the ground (and all other “static” entities) have dynamic
set to NO
. This is because that only the bird is affected by interactions with the physic world. The remaining entities are under our control and won’t change their position or rotation e.g. due to a collision.
With these changes in place, you’ll see our little bird falling to the ground.
The bird is falling a little too fast for my taste. We have multiple options to control this behavior, let’s try the easiest and most obvious one first: we’ll change the world’s gravity!
At the beginning of the scene setup, simply add:
self.physicsWorld.gravity = CGVectorMake( 0.0, -5.0 );
This will reduce the gravity from an authentic -9.8m/s^2 to only -5m/s^2, making the bird falling slower.
In order to let the bird raise on a click/touch, we will apply a small directed impulse to the bird’s physics body. You wouldn’t believe how easy this is with SpriteKit. Just add one line to the touchesBegan method:
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { /* Called when a touch begins */ [_bird.physicsBody applyImpulse:CGVectorMake(0, 8)]; }
The result should look like that:
Blog reader Sascha suggests to reset the bird’s velocity before the new impulse is applied. This avoids that the impulses accumulate per touch/click. Blog reader Nico suggests to reduce the impulse a little in that case, so the touchesBegan
method now looks like that:
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { /* Called when a touch begins */ _bird.physicsBody.velocity = CGVectorMake(0, 0); [_bird.physicsBody applyImpulse:CGVectorMake(0, 4)]; }
I strongly encourage you to experiment with these values.
While we’re at it, we can also change the bird’s rotation depending on it’s velocity. If it’s flying upward the beak should point to the top right; if downward, to the bottom right. This can easily happen on a per-frame basis in the update:currentTime
method, which was unused up to now.
CGFloat clamp(CGFloat min, CGFloat max, CGFloat value) { if( value > max ) { return max; } else if( value < min ) { return min; } else { return value; } } -(void)update:(CFTimeInterval)currentTime { /* Called before each frame is rendered */ _bird.zRotation = clamp( -1, 0.5, _bird.physicsBody.velocity.dy * ( _bird.physicsBody.velocity.dy < 0 ? 0.003 : 0.001 ) ); }
As you can see, I use the bird’s physics body velocity
property which is maintained by the physics simulation. I added a little helper function clamp
which avoids too large values in either direction. Note that the minimum and maximum values (-1, 0.5) are radians.
Pipes
The last game entity are the pipes which are moving in to the scene from the right and need to be avoided. As before, the pipes are textured SKSpriteNode
s, they will be animated using SKAction
s and they’ll get a physics body attached for collision.
Add the two pipe graphics Pipe1.png and Pipe2.png to the project. From a graphical perspective, the pipes are a special entity. While the ground and skyline are always attached to the bottom edge of the screen and the bird always has the same dimensions, the top pipe has to extend from the top edge and the bottom pipe has to extend from the bottom edge. While this could be solved programmatically with the benefit of saving some texture space, I decided to use very high textures instead and not worry about display size anymore.
While pipes consist of two SKSpriteNode
s, I wrap both nodes into one empty SKNode
as a parent. This has the advantage that it is sufficient to position the parent node in the world, while the two childs are positioned relatively to their parent node, so I only need to worry about their vertical position.
The distance between the upper and lower pipe is arbitrary, I chose 100 points.
@implementation MyScene static NSInteger const kVerticalPipeGap = 100; -(id)initWithSize:(CGSize)size {
A pair of pipes is created outside of the right screen edge. The lower pipe’s position is chosen randomly somewhere in the lower third of the screen. The upper pipe is added accordingly, considering the defined gap size. The zPosition
for the pipes is chosen so that they always are rendered behind the ground.
// Create pipes SKTexture* _pipeTexture1 = [SKTexture textureWithImageNamed:@"Pipe1"]; _pipeTexture1.filteringMode = SKTextureFilteringNearest; SKTexture* _pipeTexture2 = [SKTexture textureWithImageNamed:@"Pipe2"]; _pipeTexture2.filteringMode = SKTextureFilteringNearest; SKNode* pipePair = [SKNode node]; pipePair.position = CGPointMake( self.frame.size.width + _pipeTexture1.size.width * 2, 0 ); pipePair.zPosition = -10; CGFloat y = arc4random() % (NSInteger)( self.frame.size.height / 3 ); SKSpriteNode* pipe1 = [SKSpriteNode spriteNodeWithTexture:_pipeTexture1]; [pipe1 setScale:2]; pipe1.position = CGPointMake( 0, y ); pipe1.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:pipe1.size]; pipe1.physicsBody.dynamic = NO; [pipePair addChild:pipe1]; SKSpriteNode* pipe2 = [SKSpriteNode spriteNodeWithTexture:_pipeTexture2]; [pipe2 setScale:2]; pipe2.position = CGPointMake( 0, y + pipe1.size.height + kVerticalPipeGap ); pipe2.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:pipe2.size]; pipe2.physicsBody.dynamic = NO; [pipePair addChild:pipe2]; SKAction* movePipes = [SKAction repeatActionForever:[SKAction moveByX:-1 y:0 duration:0.02]]; [pipePair runAction:movePipes]; [self addChild:pipePair];
The bird collides with the moving pipe and is pushed out of the screen.
We want these pipes to appear regularly. We will use SKAction
s again, but this time, in a little different way. Instead of moving the pipes and resetting their position, we will spawn new pipes regularly. We will move the spawn logic into a separate method. For that purpose, we need to store textures and the SKAction
to move the pipes in new ivars:
@interface MyScene () { SKSpriteNode* _bird; SKColor* _skyColor; SKTexture* _pipeTexture1; SKTexture* _pipeTexture2; SKAction* _moveAndRemovePipes; } @end
The _moveAndRemovePipes
action is created right after the pipe texture loading like so:
CGFloat distanceToMove = self.frame.size.width + 2 * _pipeTexture1.size.width; SKAction* movePipes = [SKAction moveByX:-distanceToMove y:0 duration:0.01 * distanceToMove]; SKAction* removePipes = [SKAction removeFromParent]; _movePipesAndRemove = [SKAction sequence:@[movePipes, removePipes]];
This will move the pipes by a full screen width plus two times their texture width to make sure that the pipes are out of sight. Then, they are removed from the scene.
The spawn method is responsible for creating new pipes:
-(void)spawnPipes { SKNode* pipePair = [SKNode node]; pipePair.position = CGPointMake( self.frame.size.width + _pipeTexture1.size.width, 0 ); pipePair.zPosition = -10; CGFloat y = arc4random() % (NSInteger)( self.frame.size.height / 3 ); SKSpriteNode* pipe1 = [SKSpriteNode spriteNodeWithTexture:_pipeTexture1]; [pipe1 setScale:2]; pipe1.position = CGPointMake( 0, y ); pipe1.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:pipe1.size]; pipe1.physicsBody.dynamic = NO; [pipePair addChild:pipe1]; SKSpriteNode* pipe2 = [SKSpriteNode spriteNodeWithTexture:_pipeTexture2]; [pipe2 setScale:2]; pipe2.position = CGPointMake( 0, y + pipe1.size.height + kVerticalPipeGap ); pipe2.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:pipe2.size]; pipe2.physicsBody.dynamic = NO; [pipePair addChild:pipe2]; [pipePair runAction:_movePipesAndRemove]; [self addChild:pipePair]; }
Finally, we need to ensure that the spawnPipes
method is called regularly to produce new pipes and attach the animation to it. This happens with an SKAction
which is directly attached to the SKScene
.
SKAction* spawn = [SKAction performSelector:@selector(spawnPipes) onTarget:self]; SKAction* delay = [SKAction waitForDuration:2.0]; SKAction* spawnThenDelay = [SKAction sequence:@[spawn, delay]]; SKAction* spawnThenDelayForever = [SKAction repeatActionForever:spawnThenDelay]; [self runAction:spawnThenDelayForever];
This action calls the spawnPipes
method, then pauses for two seconds, then repeats.
Now we’ve got beautiful repeating pipes.
Collision detection and response
The game is over when the bird hits a pipe, so detecting a hit is our next challenge. SpriteKit provides a dedicated delegate for collision detection, SKPhysicsContactDelegate
, which allows to respond whenever two physics entities are in contact. We will implement the delegate in our scene and set the physics world delegate to the scene.
Moreover, we need to setup some categories for the different types of entities in the scene. Right now, all entities collide with each other and we would receive notifications for all these contacts because of the default setup: all relevant properties – discussed below – have value 0. We will use the categories to configure the contact testing in more detail.
@interface MyScene () <SKPhysicsContactDelegate> { SKSpriteNode* _bird; SKColor* _skyColor; SKTexture* _pipeTexture1; SKTexture* _pipeTexture2; SKAction* _movePipesAndRemove; } @end @implementation MyScene static const uint32_t birdCategory = 1 << 0; static const uint32_t worldCategory = 1 << 1; static const uint32_t pipeCategory = 1 << 2; static NSInteger const kVerticalPipeGap = 100; -(id)initWithSize:(CGSize)size { if (self = [super initWithSize:size]) { /* Setup your scene here */ self.physicsWorld.gravity = CGVectorMake( 0.0, -5.0 ); self.physicsWorld.contactDelegate = self;
We can use the categories to configure which entities can collide with each other and for which types of collisions we want to get notified. This is useful for two reasons: 1. the physics engine doesn’t need to test for collisions between e.g. the pipes and the ground (which saves some performance) and 2. we don’t require notifications for all possible types of collisions.
SpriteKit provides three physics body properties for that purpose: categoryBitMask
says to which categories an entity belongs, collisionBitMask
says which categories can collide with the entity and contactTestBitMask
says which contacts lead to a notification. Note the separation of collision (actual interaction with the physics world) and contact (sole collision testing without any reaction in the physics world)! The latter two fields allow to produce three different scenarios: 1. contacts between entities are ignored, 2. contacts are notified but nothing happens in the physics simulation, 3. contacts are notified and applied to the physics world.
We will setup the bird as follows:
_bird.physicsBody.categoryBitMask = birdCategory; _bird.physicsBody.collisionBitMask = worldCategory | pipeCategory; _bird.physicsBody.contactTestBitMask = worldCategory | pipeCategory;
This means that the bird can collide with entities of the worldCategory
and pipeCategory
and that we would get notified for both of them. (You may wonder why we separate the categories for world and pipes – we could use one category for both of them. I’ll clarify later in this tutorial.)
The definition for the ground entity is a little easier. We only need to assign the category but do not request any collision notification/reaction.
dummy.physicsBody.categoryBitMask = worldCategory;
pipe1.physicsBody.categoryBitMask = pipeCategory; pipe1.physicsBody.contactTestBitMask = birdCategory;
pipe2.physicsBody.categoryBitMask = pipeCategory; pipe2.physicsBody.contactTestBitMask = birdCategory;
For the player nothing has changed: the bird still collides with the world. But the internals are now configured in a more sensible way.
Now we need to implement the contact notification method as required by the delegate. In order to visualize the collision, we will flash the background a little. That’s why we stored the sky color at the beginning of the tutorial!
- (void)didBeginContact:(SKPhysicsContact *)contact { // Flash background if contact is detected [self removeActionForKey:@"flash"]; [self runAction:[SKAction sequence:@[[SKAction repeatAction:[SKAction sequence:@[[SKAction runBlock:^{ self.backgroundColor = [SKColor redColor]; }], [SKAction waitForDuration:0.05], [SKAction runBlock:^{ self.backgroundColor = _skyColor; }], [SKAction waitForDuration:0.05]]] count:4]]] withKey:@"flash"]; }
We’re using a symbolic identifier for the SKAction
here for the first time. That way, we can remove any previously created SKAction
with the same name.
When you start the game now, you’ll see the background flashing red in case of a collision.
Animation stop
In order to stop the world movement and disable player control of the bird in case of a collision, we can make use of the speed
property of the SKNode
s. Setting the speed to 0 means that all running SKAction
s are paused. In order to avoid setting the speed
property on every individual moving entity in the scene, we create a dummy parent node called _moving
which holds all moving entities: the pipes, the background, the ground.
Once again we use an ivar to make it accessible from all methods.
@interface MyScene () <SKPhysicsContactDelegate> { SKSpriteNode* _bird; SKColor* _skyColor; SKTexture* _pipeTexture1; SKTexture* _pipeTexture2; SKAction* _movePipesAndRemove; SKNode* _moving; } @end
The entity is created at the beginning and added to the scene. All moving entities are added to this parent node.
_skyColor = [SKColor colorWithRed:113.0/255.0 green:197.0/255.0 blue:207.0/255.0 alpha:1.0]; [self setBackgroundColor:_skyColor]; _moving = [SKNode node]; [self addChild:_moving]; SKTexture* birdTexture1 = [SKTexture textureWithImageNamed:@"Bird1"]; birdTexture1.filteringMode = SKTextureFilteringNearest; ... // Create ground ... for( int i = 0; i < 2 + self.frame.size.width / ( groundTexture.size.width * 2 ); ++i ) { ... [_moving addChild:sprite]; } // Create skyline ... for( int i = 0; i < 2 + self.frame.size.width / ( skylineTexture.size.width * 2 ); ++i ) { ... [_moving addChild:sprite]; }
This affects also the pipe creation:
-(void)spawnPipes { ... [pipePair runAction:_movePipesAndRemove]; [_moving addChild:pipePair]; }
Now that we have all moving parts except the bird under one parent node. In order to stop animations, we can set the speed
of _moving
to 0 in case of contact. The parent’s speed is applied to all child nodes. We also want to happen this exactly once. We’re not interested in any further contacts once the animation has halted, so we wrap the whole method in a corresponding if
.
- (void)didBeginContact:(SKPhysicsContact *)contact { if( _moving.speed > 0 ) { _moving.speed = 0; // Flash background if contact is detected ... }
You might have noticed that we did not add the bird to the _moving
node. This is because we still want it to move and animate. We just need to retain player control from the bird by allowing touches only with _moving.speed
> 0.
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { /* Called when a touch begins */ if( _moving.speed > 0 ) { _bird.physicsBody.velocity = CGVectorMake(0, 0); [_bird.physicsBody applyImpulse:CGVectorMake(0, 4)]; } }
Restart
Before we can restart the game, we need some way to reset the entire scene: the bird has to be moved to its initial position, its physics properties have to be reset, existing pipes have to be removed and the animation has to be restarted. Most importantly, this shouldn’t start immediately after a collision but after a little delay and an explicit user click/touch.
Let’s keep track when the user is allowed to restart the game in a new ivar _canRestart
.
@interface MyScene () <SKPhysicsContactDelegate> { SKSpriteNode* _bird; SKColor* _skyColor; SKTexture* _pipeTexture1; SKTexture* _pipeTexture2; SKAction* _movePipesAndRemove; SKNode* _moving; BOOL _canRestart; } @end
We will initialize this flag to NO
because it’s not relevant during a running game.
-(id)initWithSize:(CGSize)size { if (self = [super initWithSize:size]) { /* Setup your scene here */ _canRestart = NO; self.physicsWorld.gravity = CGVectorMake( 0.0, -5.0 );
But we’ll change the flag to YES
after a collision and after the background-flash-animation has finished (that should be enough game-over-drama for the sake of this tutorial). For that purpose, we can use another kind of SKAction
, one that supports block execution. We simply append it to the flash-animation.
- (void)didBeginContact:(SKPhysicsContact *)contact { _moving.speed = 0; // Flash background if contact is detected [self removeActionForKey:@"flash"]; [self runAction:[SKAction sequence:@[[SKAction repeatAction:[SKAction sequence:@[[SKAction runBlock:^{ self.backgroundColor = [SKColor redColor]; }], [SKAction waitForDuration:0.05], [SKAction runBlock:^{ self.backgroundColor = _skyColor; }], [SKAction waitForDuration:0.05]]] count:4], [SKAction runBlock:^{ _canRestart = YES; }]]] withKey:@"flash"]; }
Now, if the game finished due to a collision and the user is allowed to restart, we can simply make use of the flag in the touchesBegan
method and reset the scene after click/touch.
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { /* Called when a touch begins */ if( _moving.speed > 0 ) { _bird.physicsBody.velocity = CGVectorMake(0, 0); [_bird.physicsBody applyImpulse:CGVectorMake(0, 4)]; } else if( _canRestart ) { [self resetScene]; } }
But wait, where’s that resetScene
method coming from? Right, we still need to implement it.
How would we do that? We can easily reset the bird, restart the animation and reset the _canRestart
flag, but how can we reset the pipes? Right now the existing pipes are direct children of the _moving
container and we have no way to easily distinguish them from other nodes like the ground or skyline.
In order to easily access the pipes and remove them from the scene, we will restructure the scene hierarchy one more time: We will introduce another artificial parent node for all pipes, add this node to the existing _moving
node and add all pipes to this new parent node.
The change is pretty straightforward. Once again a new ivar…
@interface MyScene () <SKPhysicsContactDelegate> { SKSpriteNode* _bird; SKColor* _skyColor; SKTexture* _pipeTexture1; SKTexture* _pipeTexture2; SKAction* _movePipesAndRemove; SKNode* _moving; SKNode* _pipes; BOOL _canRestart; } @end
…and a new SKNode
as the parent for the pipes…
_moving = [SKNode node]; [self addChild:_moving]; _pipes = [SKNode node]; [_moving addChild:_pipes]; SKTexture* birdTexture1 = [SKTexture textureWithImageNamed:@"Bird1"]; birdTexture1.filteringMode = SKTextureFilteringNearest;
…and all created pipes added to this node instead of the _moving
node.
[pipePair runAction:_movePipesAndRemove]; [_pipes addChild:pipePair]; }
Now we have everything we need to implement the resetScene
method:
-(void)resetScene { // Move bird to original position and reset velocity _bird.position = CGPointMake(self.frame.size.width / 4, CGRectGetMidY(self.frame)); _bird.physicsBody.velocity = CGVectorMake( 0, 0 ); // Remove all existing pipes [_pipes removeAllChildren]; // Reset _canRestart _canRestart = NO; // Restart animation _moving.speed = 1; }
A little more drama
Right now, a crashed bird would simply fall as far as the physics world permits. If it crashes right in a pipe gap, it would possibly land directly on the lower pipe. I thought a little more drama was due! I wanted to ensure that the bird falls down to the ground and I wanted it to look a little more spectacular. So I changed the bird’s collision bitmask after collision so that it only would collide with the ground, and I added a little rotation animation:
- (void)didBeginContact:(SKPhysicsContact *)contact { _moving.speed = 0; _bird.physicsBody.collisionBitMask = worldCategory; [_bird runAction:[SKAction rotateByAngle:M_PI * _bird.position.y * 0.01 duration:_bird.position.y * 0.003] completion:^{ _bird.speed = 0; }];
The angle and duration calculations are attempts to adjust the amount of rotation to the bird’s altitude. If the bird collides at a very low position, it will hit the ground sooner than if it collides at a high position, so the angle and durations are smaller if the bird flew lower. Right after the animation we change the bird’s speed
to 0, too, to halt the wing flap animation. To avoid overriding the animation, we need to restrict the velocity-dependent rotation to when the game is still running:
-(void)update:(CFTimeInterval)currentTime { /* Called before each frame is rendered */ if( _moving.speed > 0 ) { _bird.zRotation = clamp( -1, 0.5, _bird.physicsBody.velocity.dy * ( _bird.physicsBody.velocity.dy < 0 ? 0.003 : 0.001 ) ); } }
Now we need to reset the collisionBitMask
, speed
and zRotation
properties in the resetScene
method as well:
-(void)resetScene { // Reset bird properties _bird.position = CGPointMake(self.frame.size.width / 4, CGRectGetMidY(self.frame)); _bird.physicsBody.velocity = CGVectorMake( 0, 0 ); _bird.physicsBody.collisionBitMask = worldCategory | pipeCategory; _bird.speed = 1.0; _bird.zRotation = 0.0;
An idea to make the crash even more spectacular would be to add a little feather particle system. But maybe that’s something for another tutorial.
Score counting
The first thing to do for score counting is to actually display the score. SpriteKit provides SKLabelNode
s, which are meant to display text in the scene. iOS 7 comes with plenty of fonts to choose from, I decided to go for “MarkerFelt-Wide”. Eventually we should switch to a more fitting bitmap font, but this should do for now.
To keep track of the score and the label, we’ll first add two new ivars.
@interface MyScene () <SKPhysicsContactDelegate> { SKSpriteNode* _bird; SKColor* _skyColor; SKTexture* _pipeTexture1; SKTexture* _pipeTexture2; SKAction* _movePipesAndRemove; SKNode* _moving; SKNode* _pipes; BOOL _canRestart; SKLabelNode* _scoreLabelNode; NSInteger _score; } @end
Now we’ll initialize the ivars and add the label to the scene. Add this at the end of the /* Setup your scene here */
block.
// Initialize label and create a label which holds the score _score = 0; _scoreLabelNode = [SKLabelNode labelNodeWithFontNamed:@"MarkerFelt-Wide"]; _scoreLabelNode.position = CGPointMake( CGRectGetMidX( self.frame ), 3 * self.frame.size.height / 4 ); _scoreLabelNode.zPosition = 100; _scoreLabelNode.text = [NSString stringWithFormat:@"%d", _score]; [self addChild:_scoreLabelNode];
The score should increment whenever the bird has passed a pair of pipes. The easy way, as suggested in the comments, would be to simply increment the score when the pipes have left the screen and are removed from the scene graph. However, this would also mean a little delay until the score is counted. Instead, we will create an SKNode
right “after” (meaning: to the right of) the pipes with a physics body with contact detection enabled.
Add this to -(void)spawnPipes
:
[pipePair addChild:pipe2]; SKNode* contactNode = [SKNode node]; contactNode.position = CGPointMake( pipe1.size.width + _bird.size.width / 2, CGRectGetMidY( self.frame ) ); contactNode.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:CGSizeMake( pipe2.size.width, self.frame.size.height )]; contactNode.physicsBody.dynamic = NO; [pipePair addChild:contactNode]; [pipePair runAction:_movePipesAndRemove];
If you run the game now, you’ll see that the bird crashes shortly after passing a pair of pipes. This happens because the contact of the bird’s physics body with the contactNode’s physics body is detected and - (void)didBeginContact:(SKPhysicsContact*)
is called, but we currently do not distinguish whether the bird is in contact with the world or something else. We can make use of the collision categories again and add a dedicated one for the score area.
@implementation MyScene static const uint32_t birdCategory = 1 << 0; static const uint32_t worldCategory = 1 << 1; static const uint32_t pipeCategory = 1 << 2; static const uint32_t scoreCategory = 1 << 3; static NSInteger const kVerticalPipeGap = 100;
We assign this category to the score node that we created right before.
SKNode* contactNode = [SKNode node]; contactNode.position = CGPointMake( pipe1.size.width + _bird.size.width / 2, CGRectGetMidY( self.frame ) ); contactNode.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:CGSizeMake(pipe2.size.width, self.frame.size.height)]; contactNode.physicsBody.dynamic = NO; contactNode.physicsBody.categoryBitMask = scoreCategory; contactNode.physicsBody.contactTestBitMask = birdCategory; [pipePair addChild:contactNode];
Now we can handle the different types of contacts properly by investigating the SKPhysicsContact
object passed to the didBeginContact
method. It has two properties representing the two physics bodies in contact, bodyA
and bodyB
. If either of these two physics bodies is of category scoreCategory
, we can increase the score. In all other cases, the bird collides with the world.
- (void)didBeginContact:(SKPhysicsContact *)contact { if( _moving.speed > 0 ) { if( ( contact.bodyA.categoryBitMask & scoreCategory ) == scoreCategory || ( contact.bodyB.categoryBitMask & scoreCategory ) == scoreCategory ) { // Bird has contact with score entity _score++; _scoreLabelNode.text = [NSString stringWithFormat:@"%d", _score]; } else { // Bird has collided with world ... } } }
The only thing left is to reset the score to 0 in the reset method.
-(void)resetScene { ... // Reset score _score = 0; _scoreLabelNode.text = [NSString stringWithFormat:@"%d", _score]; }
The result should look like that:
The score update is a little boring. You can easily add some visual feedback by using some SKAction
s to scale the score for a short moment.
- (void)didBeginContact:(SKPhysicsContact *)contact { if( _moving.speed > 0 ) { if( ( contact.bodyA.categoryBitMask & scoreCategory ) == scoreCategory || ( contact.bodyB.categoryBitMask & scoreCategory ) == scoreCategory ) { // Bird has contact with score entity _score++; _scoreLabelNode.text = [NSString stringWithFormat:@"%d", _score]; // Add a little visual feedback for the score increment [_scoreLabelNode runAction:[SKAction sequence:@[[SKAction scaleTo:1.5 duration:0.1], [SKAction scaleTo:1.0 duration:0.1]]]]; } else { ...
The action scales the score text up to 150% of its size of 1/10th of a second and then back to the original 100% in another 1/10th second.
Many roads lead to Rome
I need to emphasize that there are dozens of other possible ways to implement the game mechanics.
For example, the world movement and score counting could easily be done in the update
method. The movement could be implemented depending on the passed time since last frame and the counting could be implemented depending on the pipe’s horizontal position. Even collision detection is so trivial that you don’t need the physics engine for that. On the other hand, look how easy it was to implement the bird’s behavior!
You would also normally not load all sprites into separate files but use texture atlasses instead (i.e. as many sprites as possible on one texture) because texture switches are an expensive operation on the graphics card.
From a software engineering perspective there is also plenty of improvement potential, like moving the score update functionality into a dedicated method, or even a subclass of SKLabelNode
with dedicated increment/reset methods.
Conclusion
During the course of this tutorial, you’ve learned how to…
- create a SpriteKit project with Xcode
- load textures into
SKTexture
- create sprites with
SKSpriteNode
- create text with
SKLabelNode
- use different animation speeds to create the illusion of depth
- use the
SKSpriteNode
‘szPosition
property to control rendering order - attach and configure physics bodies to nodes
- configure the physics world
- use
SKAction
to animate sprites, move the game world and control game states - control game flow using the
SKNode.speed
property - handle touches
- detect and distinguish contacts between entities
- structure the scene graph in a way that makes sense for the game world
If you think that something needs to be covered in more detail, feel free to leave a comment or contact me on Twitter (@digitalbreed). I will try to update the tutorial or respond to you directly.
If you liked this tutorial and you have some Bitcoins to spare, I would appreciate a tip at 1QByEN4aeEN1KKqikBFUGdgeg1ZCX6c1nr.
Download
I have pushed the tutorial code to GitHub.
LeWeb 2010 startups revisited
- Votizen – acquired by causes.com (open positions 11/2012: 2)
- Simple (former BankSimple) – online and hiring (2012: 7, 2013: 7)
- Blekko – online, no jobs offers found (open positions 11/2012: 3)
- Hipmunk – online and hiring (2012: 1, 2013: 5)
- Optimizely – online and hiring (2012: 9, 2013: 21)
- 1000 Memories – aquired by ancestry.com
- Elation EMR – online and hiring (2012: 3, 2013: 5)
- CueUp (former Greplin) – acquired by Apple, now defunct (2012: 4.5)
- SavingStar (SaveWave) – online and hiring (2012: 1, 2013: 3)
- Topguest – acquired by Switchfly
- Beautylish – online and hiring (2012: 1, 2013: 3)
- GroupMe – online and hiring (2012: 3, 2013: 2)
Out of the ten finalists, four companies were acquired and the other six are still in business. I think that’s pretty impressive.
Tomcat 7 and Nginx on Ubuntu 10.12
Read More
No signature of method addTo* is applicable
No signature of method: package.Parent.addToChildren() is applicable for argument types: (package.Child) values: [package.Child : null]
Possible solutions: getChildren(). Stacktrace follows:
groovy.lang.MissingMethodException: No signature of method: package.Parent.addToChildren() is applicable for argument types: (package.Child) values: [package.Child : null]
I searched for the problem, read dozens of different hints regarding proper general ORM one-to-many setups, GORM-specific hasMany/belongsTo relationships, cascading, saving before adding, etc. All were different problems leading to the same exception.
My problem, however, was far more trivial. Here’s the code that made me want to tear my hair out.
class Parent { static hasMany = { children: Child } }
Do you spot the mistake?
I used curly braces for the hasMany
relationship. Groovy doesn’t complain at all, as it’s valid syntax. But to make GORM work the expected way, it should be square brackets:
class Parent { static hasMany = [ children: Child ] }
I’ve lost two hours of my life to this one.
Enabling Tomcat NIO Connector with Grails 2.0
grails.tomcat.nio=true
into your Config.groovy
file. I believed that, too, and it drove me nuts to see that the org.apache.coyote.http11.Http11NioProtocol
class wasn’t even instantiated.I had to review the commit logs to figure out that the property should actually be in the
BuildConfig.groovy
file.Now I finally got the log messages that I expected to see…
| Loading Grails 2.0.0
| Configuring classpath.
| Environment set to development.....
| Packaging Grails application.....
| Running Grails application
| Enabling Tomcat NIO connector
http11.Http11NioProtocol Initializing ProtocolHandler ["http-nio-8080"]
| Server running. Browse to http://localhost:8080/...
MongoDB: can only have 1 index plugin / bad index key pattern
com.mongodb.MongoException: can only have 1 index plugin / bad index key pattern
at com.mongodb.MongoException.parse(MongoException.java:82)
at com.mongodb.DBApiLayer$MyCollection.__find(DBApiLayer.java:312)
at com.mongodb.DBCursor._check(DBCursor.java:369)
at com.mongodb.DBCursor._hasNext(DBCursor.java:498)
at com.mongodb.DBCursor.hasNext(DBCursor.java:523)
This exception drove me nuts. I checked the index definition in Java, which looked fine, compared it to the index result in Mongo shell (db.collectionName.getIndexes()
), which looked fine too, made several desperate attempts to drop and recreate the index, re-index the collection, etc.
Then I cut down the DB query and recognized that removing my sort()
call made a difference. Eventually it turned out that the sort order was specified as a String
instead of an Integer
, see below:
BasicDBObject query = ... // WRONG: // BasicDBObject sort = new BasicDBObject("column1", "1").append("column2", "-1"); // RIGHT: BasicDBObject sort = new BasicDBObject("column1", 1).append("column2", -1); mongoDB.getCollection("collectionName").find(query).sort(sort);
Using Grails without a database
- Remove Grails’ Hibernate plugin.
grails uninstall-plugin hibernate
- Delete the datasource configuration file
conf/DataSource.groovy
- Explicitly declare services as non-transactional. The default is
true
and your class methods would be proxied, but without the Hibernate plugin there is no transaction manager and the deployment will fail.class SomeService { static transactional = false // ... }
- Use command objects instead of domain objects, particularly if you need validation.
9 out of 10 startups fail
I was interested in whether there’s any truth in this startup-scene meme, so I took some time to walk through 13 startups who presented 30 second pitches at Web 2.0 summit 2010, a little more than one year ago.
Here’s the outcome – all startups in the order of appearance with a number of open permanent positions at the time of this writing.
- Votizen – online and hiring (2)
- BankSimple – online and hiring (6)
- Blekko – online and hiring (3)
- Hipmunk – online
- Optimizely – online and hiring (2)
- 1000 Memories – online and hiring (2)
- Elation EMR – online and hiring (2)
- Greplin – online and hiring (4)
- SavingStar (SaveWave) – online and hiring (1)
- Topguest – online
- Beautylish – online
- GroupMe – online and hiring (1)
I’ve just put a reminder in my calendar to revisit this list in one year. Let’s see how it’s looking like then.
Deprecation of ConfigurationHolder in Grails 2
ConfigurationHolder
is now deprecated. The new way to access the configuration is through dependency injection of the grailsApplication
bean.
Before 2.0:
import org.codehaus.groovy.grails.commons.ConfigurationHolder as CH ... def value = CH.config.my.configEntry
With 2.0:
class MyService/MyController { def grailsApplication def method { def value = grailsApplication.config.my.configEntry } }