iOS7 Sprite Kit for Game Design for iPhone & iPad

iOS 7 Series – Sprite Kit

Welcome to iOS7 and to start off, I want to kick things off with SpriteKit.  Although it deals with video games, many companies are using iOS apps as a marketing tactic to engage their users in an effort to promote their products.

SpriteKit is the most prominent feature in iOS7 so we’re going to take a quick tour.  Go ahead and create a New Project in XCode5 and select the SpriteKit template (the bottom right icon):

iOS7 SpriteKit Basics by Marcio Valenzuela Santiapps.com
iOS7 SpriteKit Basics by Marcio Valenzuela Santiapps.com

Click next and fill in your project data.  Once you are in the main XCode window notice we have the following files in the Project Navigator:

1)   AppDelegate

2)   Main.Storyboard

3)   ViewController

4)   MyScene

If you look in the AppDelegate you will see nothing new, just a UIWindow property.  The Main.Storyboard is even more eerie.  It’s nothing more than a blank scene.  Then you find the ViewController.h file, which basically imports the SpriteKit framework.  If you check in your Target Linked Libraries you can see SpriteKit in linked.

ViewController.m is where things get interesting.

– (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];

}

In viewDidLoad we create an SKView object and assign our viewcontroller’s view to it.  As an SKView type view, we have a property that allows us to display the fps onscreen and another one for viewing the nodes onscreen for debugging purposes.

We now create a second object of type SKScene and instantiate it with the size of our SKView object to make sure it fits.  Then we set the SKScene object’s scaleMode to AspectFill.  Finally we tell our SKView object to present our SKScene object each with its set properties.

There are a couple of extra methods that tell the app to autorotate and returns the supported orientations.

Now let’s move on to the MyScene object created because this is where the magic happens.

-(id)initWithSize:(CGSize)size {

if (self = [super initWithSize:size]) {

/* Setup your scene here */

self.backgroundColor = [SKColor colorWithRed:0.15 green:0.15 blue:0.3 alpha:1.0];

SKLabelNode *myLabel = [SKLabelNode labelNodeWithFontNamed:@”Chalkduster”];

myLabel.text = @”Hello, World!”;

myLabel.fontSize = 30;

myLabel.position = CGPointMake(CGRectGetMidX(self.frame),

CGRectGetMidY(self.frame));

[self addChild:myLabel];

}

return self;

}

First we create an instance of self with the passed in value for size.  Next we set the background color and create a label in order to position it in the middle of the screen.

The next method reacts to screen touches using the well known touchesBegan method.

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {

/* Called when a touch begins */

for (UITouch *touch in touches) {

CGPoint location = [touch locationInNode:self];

SKSpriteNode *sprite = [SKSpriteNode spriteNodeWithImageNamed:@”Spaceship”];

sprite.position = location;

SKAction *action = [SKAction rotateByAngle:M_PI duration:1];

[sprite runAction:[SKAction repeatActionForever:action]];

[self addChild:sprite];

}

}

Here we loop through any touches and get their location as a CGPoint.  Then we instantiate a sprite from a file named Spaceship and set its position to the touch received.  Finally we create a rotating action and run it on the sprite and add that now rotating sprite to our SKScene.  Voila.

However, there is a very important method left out, the update method.  This method gets called every so often, many times a second actually!  So this is the method used to execute our game logic.

So let’s assume we are the Head iOS Programmer for a big soda company and you met with the Marketing Director about making a game to promote your product.  You decide game will be really simple, such as trying to catch a soda can falling from the sky at the beach.  What you need:

a)    A beach backdrop

b)   A soda can

c)    A potential client

d)   You also need a good marketing strategy to go with the game, otherwise playing the game won’t amount to much.  Such a strategy could be whoever makes the most points in the next month, gets a free 24pack or something.  But you better check with your Marketing Director first J

iOS7 SpriteKit Basics by Marcio Valenzuela Santiapps.com
iOS7 SpriteKit Basics
iOS7 SpriteKit Basics by Marcio Valenzuela Santiapps.com
iOS7 SpriteKit Basics
iOS7 SpriteKit Basics by Marcio Valenzuela Santiapps.com
iOS7 SpriteKit Basics Sprites

So let’s go ahead and google some images:

Ok so let’s plop these on screen to make sure the sizes are proportional.  We are going to be making this project for an iPhone Retina screen so the correct beach size should be the complete screen size, which is 960×640.  To put the beach image on the screen, aside from importing it into your project:

iOS7 SpriteKit Basics by Marcio Valenzuela Santiapps.com
iOS7 SpriteKit Basics

We need to add the code.  So let’s move over to our MyScene initWithSize method because that is only called once and we don’t need to change the background for this game after its been created the first time.  First remove the entire code inside the if block and replace it:

SKSpriteNode *background = [SKSpriteNode spriteNodeWithImageNamed:@”beach.png”];

background.anchorPoint = CGPointZero;

background.position = CGPointZero;

[self addChild:background];

This created a sprite called background using the beach image.  It positions the image and adds it to the scene.  If you run it you’ll get a chopped off image because the simulator is positioned vertically.  This is because we didn’t tell the app to only run in landscape mode.  So head over to your Target Settings and uncheck all orientations except Landscape Left.

iOS7 SpriteKit Basics by Marcio Valenzuela Santiapps.com
iOS7 SpriteKit Basics

FIRST NOTE: Orientation must be defined.

Now run it again and you’ll get a huge image as the background and possibly only part of it being displayed.  This is because you must tell Xcode that the image is to be used in Retina devices.  You do this by renaming the images beach@2x.png.  So go ahead and rename the image in Finder and you’ll have to remove the reference and re-add it as beach@2x.png.  Voila!

iOS7 SpriteKit Basics by Marcio Valenzuela Santiapps.com
iOS7 SpriteKit Basics

Ok so far so good!  Notice we get the node and fps count on the right bottom corner.

NOTE: Retina images are named filename@2x.png

Let’s go ahead and place the cooler on the screen to see if it looks right.  This time remember to rename your images before importing them.  After importing it, let’s add the following code, again in the initWithSize method because we only need to add the cooler once:

// ADD COOLER

SKSpriteNode *cooler = [SKSpriteNode spriteNodeWithImageNamed:@”cooler@2x.png”];

cooler.anchorPoint = CGPointZero;

cooler.position = CGPointZero;

[self addChild:cooler];

Just for your reference, my cooler png is 125×131, almost a square.  I added it and it looks fine:

iOS7 SpriteKit Basics by Marcio Valenzuela Santiapps.com
iOS7 SpriteKit Basics

As you can see, node count went up to 2 and fps has no reason to change so far.  There is something important to note here, and it’s that the cooler was added in the bottom left corner.  If you review the code, we set both the background and the cooler’s anchorPoint to CGPointZero, which is equal to 0,0.  This corresponds to the bottom left of the screen.  Thus when we position the cooler, we are telling it to put the cooler’s bottom left corner (its anchor), in the screen’s bottom left corner (its position).

NOTE: CGPointZero in SpriteKit = (0,0) = Bottom Left Corner

Ok now let’s think about the logic.  Both the background and the cooler need only appear once.  The soda can however might need to appear more than once because once the user catches the first one, we probably want to give him a second one to catch, otherwise it’ll be a short and disappointing game.  This is where the game structure gets interesting.  We won’t simply add the soda can in the initWithSize method but rather outsource its creation to a separate method.  Let’s create a method called createSodaCan:

-(void)createSodaCan{

// ADD CAN

SKSpriteNode *can = [SKSpriteNode spriteNodeWithImageNamed:@”sodacan@2x.png”];

can.anchorPoint = CGPointMake(CGRectGetMidX(can.frame), CGRectGetMidY(can.frame));

can.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame));

[self addChild:can];

}

Here we are setting the can’s anchor point to its center and placing it in the center of the screen using CGPointMake and CGRectGetMidX/Y.  However if you run the app there won’t be a soda can anywhere onscreen because we haven’t called it.  One place to call it would be in a method such as the touchesBegan method, but let’s try something different.  In your initWithSize method, add the following code right after the cooler code:

//Delay to call can method

[NSTimer scheduledTimerWithTimeInterval:2.0

target:self

selector:@selector(createSodaCan)

userInfo:nil

repeats:NO];

This basically calls the createSodaCan method after 2 seconds of initializing the scene.  And two seconds after, there it is:

iOS7 SpriteKit Basics by Marcio Valenzuela Santiapps.com
iOS7 SpriteKit Basics

NOTE: Game logic can be fired off from the init method of your scene or from a user interaction method such as the touchesBegan method.

Great!  Now let’s think of what we want to do.  Im thinking we could do something like:

1)   Center the cooler at the bottom of the screen

2)   Move the soda can up to the top

3)   Have the can move from top to bottom

Ok great, so let’s do the first two, which should be quite simple.  Then we’ll worry about moving the can.  The first step is the cooler, so replace the cooler code with:

// ADD COOLER

SKSpriteNode *cooler = [SKSpriteNode spriteNodeWithImageNamed:@”cooler@2x.png”];

cooler.anchorPoint = CGPointMake(CGRectGetMidX(cooler.frame), CGRectGetMidY(cooler.frame));

cooler.position = CGPointMake(self.frame.size.width*0.5, self.frame.size.height*0.01);

[self addChild:cooler];

This will place the cooler at 50% the width of the screen and about 1% from bottom to top.  Now do something similar with the can except that we want it to be on top so it can drop:

// ADD CAN

SKSpriteNode *can = [SKSpriteNode spriteNodeWithImageNamed:@”sodacan@2x.png”];

can.anchorPoint = CGPointMake(CGRectGetMidX(can.frame), CGRectGetMidY(can.frame));

can.position = CGPointMake(self.frame.size.width/2, self.frame.size.height*0.75);

[self addChild:can];

This places the can halfway across from left to right and ¾ of the way up the screen:

iOS7 SpriteKit Basics by Marcio Valenzuela Santiapps.com
iOS7 SpriteKit Basics

NOTE: We use relative positioning of an object in the view’s frame to avoid offscreen or misplaced images on different sized screens.

Perfect!  Now how do we animate the can so it can fall?  Well there are two ways, one is using physics, which requires us to create a physics-body object and assign it to the soda can and then “turn on” gravity so to speak.  The much simpler way is to animate the can to go from the top of the screen to the bottom of the screen.

To use the latter, we need SKActions much like the rotate action in the template.  So let’s code some and see what happens.  Well I took a look at the way the template created the rotating action and started typing…XCode took care of the rest.  I reasoned that what I wanted was to “move” the sprite and XCode suggested the following:

//Move the can

SKAction *fallingAction = [SKAction moveByX:<#(CGFloat)#> y:<#(CGFloat)#> duration:<#(NSTimeInterval)#>]

So I went with that and ended up with this code for the createSodaCan method:

// ADD CAN

SKSpriteNode *can = [SKSpriteNode spriteNodeWithImageNamed:@”sodacan@2x.png”];

can.anchorPoint = CGPointMake(CGRectGetMidX(can.frame), CGRectGetMidY(can.frame));

can.position = CGPointMake(self.frame.size.width/2, self.frame.size.height*0.75);

//Move the can

SKAction *fallingAction = [SKAction moveByX:0 y:-400 duration:2.0];

[can runAction:fallingAction];

[self addChild:can];

Well that’s not half bad.  I could’ve used another method such as moveTo instead of moveBy.

Ok now let’s simulate how to actually “catch” the can in the cooler.  What we need is something called “collision detection”.  What we want to do is check to see if the rectangle around the cooler is intersecting with the rectangle around the can.  To do this, we have to do so constantly.  That’s where the update method comes in.  This method is called repeatedly before each frame is drawn.  So we need to ask it if those two rectangles are colliding.  That means we need references to both objects.  To do that we create an instance variable for each, so we add them in MyScene.h:

@interface MyScene : SKScene {

SKSpriteNode *cooler;

SKSpriteNode *can;

}

Now modify the code in initWithSize by removing the type declaration in each line where we create the cooler and the can so they look like:

cooler = [SKSpriteNode spriteNodeWithImageNamed:@”cooler@2x.png”];

can = [SKSpriteNode spriteNodeWithImageNamed:@”sodacan@2x.png”];

And now we can access those objects through their pointers in the update method as well.  So add this line to your update method:

//Check for intersection of frames for collision simulation

[self testCollisionOfSprite:can withSprite:cooler];

This means this line will be called every time the update method fires.  You can even add an NSLog into the update method to get an idea of how many times its called.  Now let’s define the testCollisionOfSprite:withSprite: method:

-(void)testCollisionOfSprite:(SKSpriteNode*)sprite1 withSprite:(SKSpriteNode*)sprite2{

CGRect sprite1Rect = [self getSizeOfSprite:sprite1];

CGRect sprite2Rect = [self getSizeOfSprite:sprite2];

if (CGRectIntersectsRect(sprite1Rect, sprite2Rect)) {

NSLog(@”They collided”);

}

}

Nothing special going on here, we are simply creating a rectangle for each of the 2 sprites passed in and checking if those rectangles intersect.  If they do, we log.  Finally the way we get those rectangles around the sprites:

– (CGRect) getSizeOfSprite:(SKSpriteNode*)sprite{

double sx = sprite.position.x;

double sy = sprite.position.y;

return CGRectMake(sx, sy, sprite.frame.size.width, sprite.frame.size.height);

}

We have a game…almost.  Let’s add some sound and make the can disappear.

Import your favorite sound and add this code right after the NSLog for They Collided:

SKAction *play = [SKAction playSoundFileNamed:@”explosion_small.caf” waitForCompletion:YES];

[can runAction:play];

can.alpha = 0;

As you noticed earlier, the collision happens many times because there is a long trajectory form the moment the rectangles begin overlapping until they stop overlapping.  This results in a repetitive sound, which is annoying.  More importantly if becomes a problem if you want to work with scoring points because if you try to +1 a point when the collision occurs, you actually end up with many more points in total!  So what is usually done in these cases is a work around that creates a BOOL flag from the start and sets it to False.  We do this in MyScene.h:

BOOL collisionOccurred;

then in the createSodaCan method we set it to FALSE:

collisionOccurred = FALSE;

Finally we modify the if test like so:

if (CGRectIntersectsRect(sprite1Rect, sprite2Rect) && !collisionOccurred) {

collisionOccurred = TRUE;

NSLog(@”They collided”);

SKAction *play = [SKAction playSoundFileNamed:@”explosion_small.caf” waitForCompletion:YES];

[can runAction:play];

can.alpha = 0;

}

This way we always check to see if collisionOccurred is FALSE, if it is then we set it to TRUE inside the if, do our stuff, thus collisionOccurred will not be FALSE again until we set it back.

It is a cumbersome solution because then you must make sure you re-set your flag to FALSE everytime you create a new can.  It even worse if you don’t really create more cans.  Many times for memory saving reasons, we simply don’t destroy the original can.  If you noticed I didn’t destroy the node, I simply made it invisible.  The reason is that now I can reuse that sprite again by repositioning it on top and making it visible again thus saving processor power.  This is a very common tactic in games where you want to have lots of enemies or coins or what not and you don’t want to have to create thousands of instances of them.

The other more elegant solution to the collision detection repetition issue is the first method we talked about collision detection.  So let’s try to cover that in as simple terms as possible.

Physics & Collision Detection

Physics introduces the notion of physical bodies that will be “attached” to our sprite nodes.  Those bodies can be of type dynamic, static or edges.  A dynamic body reacts to physics, static bodies react to physics except forces and collisions because they are meant not to move yet exist in a physical world.  Edges never move and they are usually used to represent boundaries in a game.

iOS7 SpriteKit Basics by Marcio Valenzuela Santiapps.com
iOS7 SpriteKit Basics

This means that we can try to match a physics body to a sprite node identically, such as:

iOS7 SpriteKit Basics by Marcio Valenzuela Santiapps.com
iOS7 SpriteKit Basics

which results in a very complex physics body to monitor vs simply using a tall rectangle which results in a much easier body to manipulate.  A circle is actually the easiest body to use.

So let’s start off by creating a border around our screen by adding this line at the end of our initWithSize method:

//Create border

self.physicsBody = [SKPhysicsBody bodyWithEdgeLoopFromRect:self.frame];

and in the createSodaCan method add these lines to the end:

can.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius:can.size.width/2];

can.physicsBody.dynamic = YES;

What we have done is create a second physicsBody for the can this time and made it of circular shape and assigned it a dynamic property.  Now let’s remove the line in the update method that calls for testCollisionOfSprite:withSprite: because we don’t need that anymore.  Now run the app and you should see the soda can actually stop near the bottom of the screen which is where the border should be.  But the border is invisible, isn’t it?  Go ahead and comment out the border line in the init method and see for yourself J.

Ok so now let’s remove our line that made the can move from top to bottom:

//[can runAction:fallingAction];

And now let gravity do the work for you, add this line at the end of your initWithSize method:

//Set Gravity

self.physicsWorld.gravity = CGPointMake(0.0, -9.8);

Cool!  But the can didn’t bounce quite so much huh!?  Well that’s probably what a can would do.  But just for fun, let’s say it was a rubber can.  Add this line after the can’s dynamic physicsBody setting to YES:

can.physicsBody.restitution = 0.5;

Neat!  Well but we’re not gonna be covering physics now.  So turn that property off for now.  We just need physics for collision detection.  This is done via collision groups.

First we adopt the SKPhysicsContactDelegate Protocol so we can set out scene as the delegate to receive notifications of collisions:

@interface MyScene : SKScene <SKPhysicsContactDelegate>{

First we set our scene as the delegate in the initWithSize method:

self.physicsWorld.contactDelegate = self;

Now we must set the categories in our can and cooler objects.  So after the cooler sprite creation in the initWithSize method, add this code to create a physics body for it and set its category mask:

//collision physics

cooler.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:cooler.size];

cooler.physicsBody.dynamic = YES;

cooler.physicsBody.categoryBitMask = coolerCategory;

cooler.physicsBody.collisionBitMask = canCategory;

cooler.physicsBody.contactTestBitMask = canCategory;

So we create a rectangular body instead of a circle and set its category to cooler but its collision mask to canCategory.

Likewise in the can creation method (createSodaCan) we use this code:

//Add physics body to can

can.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius:can.size.width/2];

can.physicsBody.dynamic = YES;

//can.physicsBody.restitution = 0.5;

can.physicsBody.categoryBitMask = canCategory;

can.physicsBody.collisionBitMask = coolerCategory;

can.physicsBody.contactTestBitMask = coolerCategory;

As mentioned before, we set this body as a circle but commented out the restitution property for now.  We set its category to can and its collision mask to cooler.  We must define those categories above the @implementation line like so:

//DONT COLLIDE

static const uint32_t canCategory =  0x1 << 0;

static const uint32_t coolerCategory =  0x1 << 1;

Finally, we override the beginContact method of the protocol which reads:

//COLLISION PHYSICS

– (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 & coolerCategory) != 0)

{

NSLog(@”They collisionated!”);

}

}

We pass in a contact, then create 2 body references which will come in as part of that contact.  We set the contact bodies to either one and test for a collision and log something in the console.

Well this is as far as SpriteKit basics can get.  There are more advanced topics such as advanced physics, forces, particles, animations, parallax and raycasting that could be material for a Part 2 should there be enough interest in the subject.

Advertisements

3 thoughts on “iOS7 Sprite Kit for Game Design for iPhone & iPad

  1. Marcio, I’ve come across two of your tutorials now and just want to say nice job and thanks for sharing the knowledge!

    I’ve been working with SpriteKit for a little while now, but I am wanting to connect an interactive database model and am struggling. I was wondering if you have any experience, or have any insight on using ReactiveCocoa inside a SpriteKit project? I ask because of your “ReactiveCocoa in 3 Simple Steps” post and your obvious knowledge of using SpriteKit.

    What I have done so far:
    1. Accessed and created a mutable copy of a SQLite.db
    2. Created SKSpriteNodes based on the content of the copied database
    (I have the SKScene and the Architecture built, but this is where I am hoping to use RAC)
    3. I am wanting to organize those sprites into the scene architecture.
    – sprite.name isn’t giving me enough responsiveness for querying/organizing

    Any and all help would be greatly appreciated!

    Thanks
    Chris

  2. Enumerating through the [parent childNodeWithName:(@”name”)]’s only allows for individual
    levels of access into the NodeTree hierarchy.

    My current approach is a switch case statement, but it’s still not as dynamic/responsive as I had hoped.

    From a hierarchal approach without ReactiveCocoa, the solution would look like something like this and would require KVO/KVC and blocks that can pause or slow down the main thread:

    if(thisNode) // let’s say it’s the full page
    do:
    [self searchForChildWithName:@”name”];
    while (zoomIndex == 1);
    else
    if(childNode.name isEqual:@”name” && zoomIndex > previousZoomIndex) // find child
    do:
    [self runAction: group:@[[SKActions theseActions], …. nActions];
    ….
    This node would be waiting until an established threshold to perform it’s SKAction / role in the game.

    I was really hoping to clean it up using RACSignals and was hoping you could lend some insight, if possible!

    Thanks if you have any other questions feel free to email me: cch@indiana.edu

    Thanks for your response
    Chris

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s