Do you like smartphone games?
I like the following type of game.
Today, Let’s create a breakout clone with Sprite Kit.
Here is a goal.
Sprite Kit is a framework by Apple to create 2D games for iOS and Mac OS X.
Here is the main elements with Sprite Kit.
# | Name | Description |
---|---|---|
1 | SKView | Subclass of UIView. Rendering Sprite Kit. |
2 | SKScene | Game scene like title, setting, main, etc. |
3 | SKTransition | Animation between scenes. Run by presentScene:transition: of SKView. |
4 | NodeCount, DrawCount, FPS | Information for developers. Configure by showsDrawCount , showsNodeCount , showsFPS of SKView。 |
5 | SKSpriteNode | Node with texture image (SKTexture) or colorized rect。 |
6 | SKLabelNode | Node with single line text. |
7 | SKShapeNode | Node with CGPath shapes. |
8 | SKEmitterNode | Node with particle. |
9 | SKAction | Animation for nodes. Run with runAction: of SKNode. |
SKScene and all nodes are subclass of SKNode.
SpriteKit Game is with Storyboards. This time we don’t use storyboards. So we use Empty Application. SpriteKit.frameWork will link automatically by Xcode 5.
self.view
.@import SpriteKit;
- (void)loadView {
SKView *skView = [[SKView alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
self.view = skView;
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
SKView *skView = (SKView *)self.view;
skView.showsDrawCount = YES;
skView.showsNodeCount = YES;
skView.showsFPS = YES;
SKScene *scene = [SKScene sceneWithSize:self.view.bounds.size];
[skView presentScene:scene];
}
- (BOOL)prefersStatusBarHidden {
return YES;
}
@end
rootViewController
.#import "SJViewController.h"
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
// Override point for customization after application launch.
SJViewController *viewController = SJViewController.new;
_window.rootViewController = viewController;
[self.window makeKeyAndVisible];
return YES;
}
Show scene with black background and information.
Now, Sprite Kit is ready!
Create a first scene with single line text.
- (id)initWithSize:(CGSize)size {
self = [super initWithSize:size];
if (self) {
SKLabelNode *titleLabel = [SKLabelNode labelNodeWithFontNamed:@"HelveticaNeue"];
titleLabel.text = @"BREAKOUT!";
titleLabel.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame));
titleLabel.fontSize = 50.0f;
[self addChild:titleLabel];
}
return self;
}
import "SJTitleScene.h"
- (void)viewDidLoad {
/* abbr. */
//SKScene *scene = [SKScene sceneWithSize:self.view.bounds.size];
SKScene *scene = [SJTitleScene sceneWithSize:self.view.bounds.size];
/* abbr. */
}
Show scene with BREAKOUT! at center.
Write settings with JSON.
{
"block" : {
"margin" : 16.0,
"width" : 34.0,
"height" : 16.0,
"rows" : 5,
"max_life" : 5
},
}
initialize
.spriteNodeWithColor
. If you use images, you can use spriteNodeWithImageNamed
or spriteNodeWithTexture
.- (id)initWithSize:(CGSize)size {
self = [super initWithSize:size];
if (self) {
[self addBlocks];
}
return self;
}
static NSDictionary *config = nil;
+ (void)initialize {
NSString *path = [[NSBundle mainBundle] pathForResource:@"config" ofType:@"json"];
NSData *data = [NSData dataWithContentsOfFile:path];
if (!config) {
config = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil];
}
}
# pragma mark - Block
- (void)addBlocks {
int rows = [config[@"block"][@"rows"] intValue];
CGFloat margin = [config[@"block"][@"margin"] floatValue];
CGFloat width = [config[@"block"][@"width"] floatValue];
CGFloat height = [config[@"block"][@"height"] floatValue];
int cols = floor(CGRectGetWidth(self.frame) - margin) / (width + margin);
CGFloat y = CGRectGetHeight(self.frame) - margin - height / 2;
for (int i = 0; i < rows; i++) {
CGFloat x = margin + width / 2;
for (int j = 0; j < cols; j++) {
SKNode *block = [self newBlock];
block.position = CGPointMake(x, y);
x += width + margin;
}
y -= height + margin;
}
}
- (SKNode *)newBlock {
CGFloat width = [config[@"block"][@"width"] floatValue];
CGFloat height = [config[@"block"][@"height"] floatValue];
int maxLife = [config[@"block"][@"max_life"] floatValue];
SKSpriteNode *block = [SKSpriteNode spriteNodeWithColor:[SKColor cyanColor] size:CGSizeMake(width, height)];
block.name = @"block";
int life = (arc4random() % maxLife) + 1;
block.userData = @{ @"life" : @(life) }.mutableCopy;
[self updateBlockAlpha:block];
[self addChild:block];
return block;
}
- (void)updateBlockAlpha:(SKNode *)block {
int life = [block.userData[@"life"] intValue];
block.alpha = life * 0.2f;
}
#import "SJPlayScene.h"
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
SKScene *scene = [SJPlayScene sceneWithSize:self.size];
SKTransition *transition = [SKTransition pushWithDirection:SKTransitionDirectionUp duration:1.0f];
[self.view presentScene:scene transition:transition];
}
Show blocks!
Show paddle, ball and move paddle by tap.
"paddle" : {
"width" : 70.0,
"height" : 14.0,
"y" : 40.0,
},
"ball" : {
"radius" : 6.0,
},
Paddle or ball are searched by name
with childNodeWithName:
.
- (id)initWithSize:(CGSize)size {
/* abbr. */
[self addPaddle];
/* abbr. */
}
# pragma mark - Paddle
- (void)addPaddle {
CGFloat width = [config[@"paddle"][@"width"] floatValue];
CGFloat height = [config[@"paddle"][@"height"] floatValue];
CGFloat y = [config[@"paddle"][@"y"] floatValue];
SKSpriteNode *paddle = [SKSpriteNode spriteNodeWithColor:[SKColor brownColor] size:CGSizeMake(width, height)];
paddle.name = @"paddle";
paddle.position = CGPointMake(CGRectGetMidX(self.frame), y);
[self addChild:paddle];
}
- (SKNode *)paddleNode {
return [self childNodeWithName:@"paddle"];
}
# pragma mark - Ball
- (void)addBall {
CGFloat radius = [config[@"ball"][@"radius"] floatValue];
SKShapeNode *ball = [SKShapeNode node];
ball.name = @"ball";
ball.position = CGPointMake(CGRectGetMidX([self paddleNode].frame), CGRectGetMaxY([self paddleNode].frame) + radius);
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddArc(path, NULL, 0, 0, radius, 0, M_PI * 2, YES);
ball.path = path;
ball.fillColor = [SKColor yellowColor];
ball.strokeColor = [SKColor clearColor];
CGPathRelease(path);
[self addChild:ball];
}
- (SKNode *)ballNode {
return [self childNodeWithName:@"ball"];
}
# pragma mark - Touch
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
if (![self ballNode]) {
[self addBall];
return;
}
UITouch *touch = [touches anyObject];
CGPoint locaiton = [touch locationInNode:self];
CGFloat speed = [config[@"paddle"][@"speed"] floatValue];
CGFloat x = locaiton.x;
CGFloat diff = abs(x - [self paddleNode].position.x);
CGFloat duration = speed * diff;
SKAction *move = [SKAction moveToX:x duration:duration];
[[self paddleNode] runAction:move];
}
Show scene with paddle.
Sprite Kit has build in physics engine. If set ‘physicsBody’ to node, Sprite kit will simulate physics automatically.
It’s easy to use for collision detection.
speed
, velocity
. "paddle" : {
"width" : 70.0,
"height" : 14.0,
"y" : 40.0,
"speed" : 0.005
},
"ball" : {
"radius" : 6.0,
"velocity" : {
"x" : 50.0,
"y" : 120.0
}
},
categoryBitMask
, contactTestBitMask
and physicsWorld.contactDelegate
for contact delegate.didBeginContact:
. You must order nodes of contact in delegate method.static const uint32_t blockCategory = 0x1 << 0;
static const uint32_t ballCategory = 0x1 << 1;
@interface SJPlayScene () <SKPhysicsContactDelegate>
@end
- (id)initWithSize:(CGSize)size {
/* abbr. */
self.physicsBody = [SKPhysicsBody bodyWithEdgeLoopFromRect:self.frame];
self.physicsWorld.contactDelegate = self;
/* abbr. */
}
# pragma mark - Block
- (SKNode *)newBlock {
/* abbr. */
block.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:block.size];
block.physicsBody.dynamic = NO;
block.physicsBody.categoryBitMask = blockCategory;
/* abbr. */
}
- (void)decreaseBlockLife:(SKNode *)block {
int life = [block.userData[@"life"] intValue] - 1;
block.userData[@"life"] = @(life);
[self updateBlockAlpha:block];
}
# pragma mark - Paddle
- (void)addPaddle {
/* abbr. */
paddle.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:paddle.size];
paddle.physicsBody.dynamic = NO;
/* abbr. */
}
# pragma mark - Ball
- (void)addBall {
/* abbr. */
CGFloat velocityX = [config[@"ball"][@"velocity"][@"x"] floatValue];
CGFloat velocityY = [config[@"ball"][@"velocity"][@"y"] floatValue];
/* abbr. */
ball.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius:radius];
ball.physicsBody.affectedByGravity = NO;
ball.physicsBody.velocity = CGVectorMake(velocityX, velocityY);
ball.physicsBody.restitution = 1.0f;
ball.physicsBody.linearDamping = 0;
ball.physicsBody.friction = 0;
ball.physicsBody.usesPreciseCollisionDetection = YES;
ball.physicsBody.categoryBitMask = ballCategory;
ball.physicsBody.contactTestBitMask = blockCategory;
/* abbr. */
}
# pragma mark - SKPhysicsContactDelegate
- (void)didBeginContact:(SKPhysicsContact *)contact {
SKPhysicsBody *firstBody, *secondBody;
if (contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask) {
firstBody = contact.bodyA;
secondBody = contact.bodyB;
} else {
firstBody = contact.bodyB;
secondBody = contact.bodyA;
}
if (firstBody.categoryBitMask & blockCategory) {
if (secondBody.categoryBitMask & ballCategory) {
[self decreaseBlockLife:firstBody.node];
}
}
}
OK, it’s a gmame.
Draw border with PhysicsDebugger.
pod 'PhysicsDebugger', git: 'https://github.com/ymc-thzi/PhysicsDebugger.git'
#import "YMCPhysicsDebugger.h"
- (id)initWithSize:(CGSize)size {
self = [super initWithSize:size];
if (self) {
[YMCPhysicsDebugger init];
/* Create scene contens */
[self drawPhysicsBodies];
}
}
All nodes are collide by default.
If collisionTestBitMask
of ball is without block, It will pass through blocks.
Sprite Kit has built in particle node (SKEmitterNode) and editor (Particle Emitter Editor).
spark.sks
Here is aparticle file.
# pragma mark - Block
- (void)decreaseBlockLife:(SKNode *)block {
/* abbr. */
if (life < 1) {
[self removeNodeWithSpark:block];
}
/* abbr. */
}
# pragma mark - Utilities
- (void)removeNodeWithSpark:(SKNode *)node {
NSString *sparkPath = [[NSBundle mainBundle] pathForResource:@"spark" ofType:@"sks"];
SKEmitterNode *spark = [NSKeyedUnarchiver unarchiveObjectWithFile:sparkPath];
spark.position = node.position;
spark.xScale = spark.yScale = 0.3f;
[self addChild:spark];
SKAction *fadeOut = [SKAction fadeOutWithDuration:0.3f];
SKAction *remove = [SKAction removeFromParent];
SKAction *sequence = [SKAction sequence:@[fadeOut, remove]];
[spark runAction:sequence];
[node removeFromParent];
}
Explosion!
Some adjustment for play a game.
"label" : {
"margin" : 5.0,
"font_size" : 14.0
},
"max_life" : 5
blockNodes
is 0.If not set zPosition
, labels will be not foreground.
@property (nonatomic) int life;
@property (nonatomic) int stage;
- (id)initWithSize:(CGSize)size life:(int)life stage:(int)stage;
#import "SJGameOverScene.h"
- (id)initWithSize:(CGSize)size life:(int)life stage:(int)stage {
self = [super initWithSize:size];
if (self) {
self.life = life;
self.stage = stage;
[self addBlocks];
[self addPaddle];
[self addStageLabel];
[self addLifeLabel];
[self updateLifeLabel];
self.physicsBody = [SKPhysicsBody bodyWithEdgeLoopFromRect:self.frame];
self.physicsWorld.contactDelegate = self;
}
return self;
}
- (id)initWithSize:(CGSize)size {
return [self initWithSize:size life:[config[@"max_life"] intValue] stage:1];
}
# pragma mark - Block
- (void)decreaseBlockLife:(SKNode *)block {
/* abbr. */
if ([self blockNodes].count < 1) {
[self nextLevel];
}
/* abbr. */
}
- (NSArray *)blockNodes {
NSMutableArray *nodes = @[].mutableCopy;
[self enumerateChildNodesWithName:@"block" usingBlock:^(SKNode *node, BOOL *stop) {
[nodes addObject:node];
}];
return nodes;
}
# pragma mark - Ball
- (void)addBall {
/* abbr. */
ball.physicsBody.velocity = CGVectorMake(velocityX + self.stage, velocityY + self.stage);
/* abbr. */
}
# pragma mark - Label
- (void)addStageLabel {
CGFloat margin = [config[@"label"][@"margin"] floatValue];
CGFloat fontSize = [config[@"label"][@"font_size"] floatValue];
SKLabelNode *label = [SKLabelNode labelNodeWithFontNamed:@"HelveticaNeue-Bold"];
label.text = [NSString stringWithFormat:@"STAGE %d", _stage];
label.verticalAlignmentMode = SKLabelVerticalAlignmentModeTop;
label.horizontalAlignmentMode = SKLabelHorizontalAlignmentModeRight;
label.position = CGPointMake(CGRectGetMaxX(self.frame) - margin, CGRectGetMaxY(self.frame) - margin);
label.fontSize = fontSize;
label.zPosition = 1.0f;
[self addChild:label];
}
- (void)addLifeLabel {
CGFloat margin = [config[@"label"][@"margin"] floatValue];
CGFloat fontSize = [config[@"label"][@"font_size"] floatValue];
SKLabelNode *label = [SKLabelNode labelNodeWithFontNamed:@"HiraKakuProN-W3"];
label.verticalAlignmentMode = SKLabelVerticalAlignmentModeTop;
label.horizontalAlignmentMode = SKLabelHorizontalAlignmentModeLeft;
label.position = CGPointMake(margin, CGRectGetMaxY(self.frame) - margin);
label.fontSize = fontSize;
label.zPosition = 1.0f;
label.color = [SKColor magentaColor];
label.colorBlendFactor = 1.0f;
label.name = @"lifeLabel";
[self addChild:label];
}
- (void)updateLifeLabel {
NSMutableString *s = @"".mutableCopy;
for (int i = 0; i < _life; i++) {
[s appendString:@"♥"];
}
[self lifeLabel].text = s;
}
- (SKLabelNode *)lifeLabel {
return (SKLabelNode *)[self childNodeWithName:@"lifeLabel"];
}
# pragma mark - Callbacks
- (void)update:(NSTimeInterval)currentTime {
if((int)currentTime % 5 == 0) {
CGVector velocity = [self ballNode].physicsBody.velocity;
velocity.dx *= 1.001f;
velocity.dy *= 1.001f;
[self ballNode].physicsBody.velocity = velocity;
}
}
- (void)didEvaluateActions {
CGFloat width = [config[@"paddle"][@"width"] floatValue];
CGPoint paddlePosition = [self paddleNode].position;
if (paddlePosition.x < width / 2) {
paddlePosition.x = width / 2;
} else if (paddlePosition.x > CGRectGetWidth(self.frame) - width / 2) {
paddlePosition.x = CGRectGetWidth(self.frame) - width / 2;
}
[self paddleNode].position = paddlePosition;
}
- (void)didSimulatePhysics {
if ([self ballNode] && [self ballNode].position.y < [config[@"ball"][@"radius"] floatValue] * 2) {
[self removeNodeWithSpark:[self ballNode]];
_life--;
[self updateLifeLabel];
if (_life < 1) {
[self gameOver];
}
}
}
# pragma mark - Utilities
- (void)gameOver {
SKScene *scene = [SJGameOverScene sceneWithSize:self.size];
SKTransition *transition = [SKTransition pushWithDirection:SKTransitionDirectionDown duration:1.0f];
[self.view presentScene:scene transition:transition];
}
- (void)nextLevel {
SJPlayScene *scene = [[SJPlayScene alloc] initWithSize:self.size life:self.life stage:self.stage + 1];
SKTransition *transition = [SKTransition doorwayWithDuration:1.0f];
[self.view presentScene:scene transition:transition];
}
Here is game loop processing with Sprite Kit.
You can override update:
, didEvaluateActions
and didSimulatePhysics
.
This time, use for the following.
update:
.didEvaluateActions
.didSimulatePhysics
.It is close to the same TitleScene.
#import "SJPlayScene.h"
- (id)initWithSize:(CGSize)size {
self = [super initWithSize:size];
if (self) {
SKLabelNode *titleLabel = [SKLabelNode labelNodeWithFontNamed:@"HelveticaNeue"];
titleLabel.text = @"GAVE OVER...";
titleLabel.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame));
titleLabel.fontSize = 40.0f;
[self addChild:titleLabel];
}
return self;
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
SKScene *scene = [SJPlayScene sceneWithSize:self.size];
SKTransition *transition = [SKTransition pushWithDirection:SKTransitionDirectionUp duration:1.0f];
[self.view presentScene:scene transition:transition];
}
Show GAME OVER… if life is zero.
It’s all done.
Source code is available on tnantoka/hello-spritekit.Licensed under the terms of the MIT license.Feel free to use.
Please give me a star!