With the iPad, it’s never been a better time to be a kid!
The iPad allows developers to create beautiful interactive children’s books that simply cannot be replicated in any other medium. For some examples, check out The Monster at the End of This Book, Bobo Explores Light, and Wild Fables.
In the old days, developers had to use third party frameworks like Cocos2d to get the job done. But now, Apple has provided their own 2D framework called Sprite Kit, which is perfect for creating these types of books.
In this tutorial, you’ll create an interactive children’s book using the Sprite Kit framework, where you’ll learn how to add objects to scenes, create animation sequences, allow the reader to interact with the book and even how to add sound and music to your book!
This tutorial assumes you are familiar with at least the basics of Sprite Kit. If you are new to Sprite Kit, please check out our Sprite Kit Tutorial for Beginners first.
Getting Started
First download the starter project for this tutorial. Extract the project to a convenient location on your drive, then open it up in Xcode for a quick look at how it’s structured.
The project’s contents are collected in four main folders, as shown below:
- Other Resources: this contains all resources from third-party sources.
- Sounds: this contains most of the project’s sound files.
- Art: this contains all of the creative image assets.
- Scenes: as you may have guessed, this contains all the classes that control the various scenes of the project.
This tutorial also uses music from Kevin MacLeod and sound effects from FreeSound. These are both great resources for your project, but you must provide attribution. See attribution.txt for more details.
Remember, kids… only you can prevent copyright theft. Oh, and forest fires too, according to Smokey the Bear.
Close the Other Resources, Sounds, and Art groups; you won’t be making any changes in those areas. You’ll only be working with the “Scene*” files located in the Scenes group.
Like any good book, it’s best to start at the beginning. The next section will walk you through the “title” scene for your book.
Adding the Title Page
The starter project provides stub versions of the methods used in this scene — it’s your job to add the code. Your first steps will be to initialize the scene and add a background image.
Open the Scene00.m file and add the following block of code to initWithSize:
just after the comment that reads /* add setup here */
:
SKSpriteNode *background = [SKSpriteNode spriteNodeWithImageNamed:@"bg_title_page"]; background.anchorPoint = CGPointZero; background.position = CGPointZero; [self addChild:background]; |
The code above creates an SKSpriteNode
object and initializes it using the spriteNodeWithImageNamed:
class method. It then sets the background
‘s anchorPoint
to CGPointZero
which is the lower left corner of the scene.
Next, it sets the background
‘s position
to CGPointZero
, which is the lower left corner of the screen. Finally, it uses the addChild:
class method to add the newly created node as a child of the SKScene
.
SKSpriteNode
subclasses SKNode
. SKNode
s are responsible for most of the content that you will add to your Sprite Kit apps.
Still working in the same file, add the following two lines of code to initWithSize:
immediately after the call to addChild:
:
[self setUpBookTitle]; [self setUpSoundButton]; |
The two methods called here are only shells at the moment; you’ll flesh them out in the next section.
Build and run your project; you should see the title page as illustrated below:
Adding the Book Title
As you saw in the code above, it’s a straightforward operation to add an SKSpriteNode
to your scene. With just a few lines of code, you can add a textured sprite to any scene.
Now that the background image has been added, you’ll need to add the book title. You could use an SKLabelNode
to add your title, or even a UIView
, but instead I’ve opted to use a graphic.
Still working in the Scene00.m file, add the following block of code to setUpBookTitle
:
SKSpriteNode *bookTitle = [SKSpriteNode spriteNodeWithImageNamed:@"title_text"]; bookTitle.name = @"bookTitle"; bookTitle.position = CGPointMake(421,595); [self addChild:bookTitle]; |
In the above code, you first create an SKSpriteNode
and then initialize it using spriteNodeWithImageNamed:
. You then name the node “bookTitle” for ease of reference later, and set the node’s position
to something other than CGPointZero
.
Why? Since the background image takes up the entire view, you can get away with positioning it in the bottom-left corner of the screen. The title image, on the other hand, must be positioned precisely where you want it.
Build and run your project; you’ll see your background image with the graphic title superimposed over it, as shown below:
That looks pretty neat. But wouldn’t it be great if you could add a little bit of action to your title screen?
Adding Animation to Objects
Sprite Kit makes it easy to add animation to the objects in your scene. Instead of simply having the title appear on the screen, you can make it slide on to the screen and bounce a little bit before it comes to rest.
To do this, you’ll use SKAction
objects with predefined sequences.
Head back to Scene00.m and add the following code to the end of setUpBookTitle
:
SKAction *actionMoveDown = [SKAction moveToY:600 duration:3.0]; SKAction *actionMoveUp = [SKAction moveToY:603 duration:0.25]; SKAction *actionMoveDownFast = [SKAction moveToY:600 duration:0.25]; [bookTitle runAction:[SKAction sequence:@[actionMoveDown, actionMoveUp, actionMoveDownFast]]]; |
The above code creates three actions using SKAction
‘s class method moveToY:duration:
; this specifies an action that moves a node vertically from its original position to the specified y
position. Next, it creates a new sequence action using SKAction
‘s sequence:
class method; this instructs bookTitle
to run the specified array of actions in order.
The net result of these actions is that the sprite will move the sprite down, then up, and then down again before it comes to rest.
Next, modify the line in setUpBookTitle
that sets bookTitle
‘s position
property as shown below:
bookTitle.position = CGPointMake(425,900); |
This simply modifies the start position of the title image so that it starts off-screen.
Build and run your project; the title now slides in slowly and bounces a little before it comes to rest at the position specified in actionMoveDownFast
.
Actions are great, but sounds are an integral part of any interactive children’s book. Sprite Kit makes this tremendously easy as well!
Adding Sound to Your Story
There are several methods to add music and other sounds to your book, but in this tutorial you’ll just focus on two methods, one for adding the background music, and the other for adding the narration and sound effects.
Head back to Scene00.m and add the following import statement:
@import AVFoundation; |
Add the following private instance variables to Scene00.m
at the spot indicated by the comment /* set up your instance variables here */
:
AVAudioPlayer *_backgroundMusicPlayer; SKSpriteNode *_btnSound; BOOL _soundOff; |
Next, add the following two lines of code just before the line that creates background
in locate initWithSize:
:
_soundOff = [[NSUserDefaults standardUserDefaults] boolForKey:@"pref_sound"]; [self playBackgroundMusic:@"title_bgMusic.mp3"]; |
This code retrieves the BOOL
value that represents the user’s preference for having the sound on or off and assigns it to the _soundOff
instance variable.
Now add the following block of code to playBackgroundMusic:
:
NSError *error; NSURL *backgroundMusicURL = [[NSBundle mainBundle] URLForResource:filename withExtension:nil]; _backgroundMusicPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:backgroundMusicURL error:&error]; _backgroundMusicPlayer.numberOfLoops = -1; _backgroundMusicPlayer.volume = 1.0; [_backgroundMusicPlayer prepareToPlay]; |
playBackgroundMusic:
plays the background music using a AVAudioPlayer
object.
AVAudioPlayer
isn’t specific to Sprite Kit, so it won’t be covered in detail here. For more detail on AVAudioPlayer
, check out the Audio Tutorial for iOS.Okay — you have some code to handle the user preference for enabling or disabling sound in your book. You should probably add a control to let the user set this preference! :]
Still working in Scene00.m
, add the code below to setUpSoundButton
:
if (_soundOff) { // NSLog(@"_soundOff"); [_btnSound removeFromParent]; _btnSound = [SKSpriteNode spriteNodeWithImageNamed:@"button_sound_off"]; _btnSound.position = CGPointMake(980, 38); [self addChild:_btnSound]; [_backgroundMusicPlayer stop]; } else { // NSLog(@"_soundOn"); [_btnSound removeFromParent]; _btnSound = [SKSpriteNode spriteNodeWithImageNamed:@"button_sound_on"]; _btnSound.position = CGPointMake(980, 38); [self addChild:_btnSound]; [_backgroundMusicPlayer play]; } |
The above code sets the texture of the _btnSound
SKSpriteNode
to the appropriate image depending on the user’s stored preference. It then checks _soundOff
and, regardless of its value, removes the sprite from the screen and initializes a new sprite with the appropriate on or off image. Then it sets the sprite’s position, adds it to the parent view, and finally plays or stops the music depending on the user’s preference.
There are a number of ways to handle changing a sprite’s image. The above method is only one way to accomplish this; the next section shows you another way to manage sprite images. The next section shows you another way to accomplish the same thing.
For now, build and run the app, and the background music should play. Tapping the button won’t do anything yet though.
Detecting Touch Events
The reason you can’t currently toggle the control is that the sprite doesn’t have any touch event handlers. So let’s add that next.
Add the following code to touchesBegan:withEvent:
in Scene00.m:
/* Called when a touch begins */ for (UITouch *touch in touches) { CGPoint location = [touch locationInNode:self]; // NSLog(@"** TOUCH LOCATION ** \nx: %f / y: %f", location.x, location.y); if([_btnSound containsPoint:location]) { // NSLog(@"xxxxxxxxxxxxxxxxxxx sound toggle"); [self showSoundButtonForTogglePosition:_soundOff]; } } |
touchesBegan:withEvent:
is part of the UIResponder
class which all SKNode
s extend. It tells the receiver when one or more fingers begin touching the view. In this method you’re able to capture the location of the touch and respond to it accordingly.
SKNode
extends UIResponder
, you can implement touch handling directly in your sprites. This tutorial performs all touch event handling in the scene nodes simply to keep the number of classes more manageable. However, in a production app it would likely make more sense to create an SKSpriteNode
subclass, such as SoundToggleButton
, and have that subclass handle its own touch events.Look at the if
statement above; you’ll see that if the touch location is on the sound button, you call showSoundButtonForTogglePosition:
.
Now would be a great time to write that method! :]
Add the following code to showSoundButtonForTogglePosition:
in Scene00.m:
// NSLog(@"togglePosition: %i", togglePosition); if (togglePosition) { _btnSound.texture = [SKTexture textureWithImageNamed:@"button_sound_on"]; _soundOff = NO; [[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"pref_sound"]; [[NSUserDefaults standardUserDefaults] synchronize]; [_backgroundMusicPlayer play]; } else { _btnSound.texture = [SKTexture textureWithImageNamed:@"button_sound_off"]; _soundOff = YES; [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"pref_sound"]; [[NSUserDefaults standardUserDefaults] synchronize]; [_backgroundMusicPlayer stop]; } |
The method above sets the appropriate texture for the SKSpriteNode
that is held in _btnSound
— much as you did in setUpSoundButton
. Once the texture has been set, you can store the user’s selection and either play or stop the music accordingly.
SKTexture
object stores a reference to the graphical data in memory that a sprite renders.Build and run, and now you should be able to tap the sound button to turn the background music on and off.
Adding the Next Scene
Add the following import to the top of Scene00.m:
#import "Scene01.h" |
You’ll need this to present the next scene; this is Page 1 of the book and is controlled by the Scene01
class.
Still working in Scene00.m, add the following code to setUpBookTitle
just after the line that creates actionMoveDownFast
, but before the line that calls runAction:
on bookTitle
:
SKAction *wait = [SKAction waitForDuration:0.75]; SKAction *showButton = [SKAction runBlock:^{ SKSpriteNode *buttonStart = [SKSpriteNode spriteNodeWithImageNamed:@"button_read"]; buttonStart.name = @"buttonStart"; buttonStart.position = CGPointMake(425,460); [self addChild:buttonStart]; [buttonStart runAction:[SKAction playSoundFileNamed:@"thompsonman_pop.wav" waitForCompletion:NO]]; }]; |
The code above creates a new action block which creates a sprite for the start button and plays a sound. By putting the code to create the sprite and play a sound inside an action, you can easily make this code run in a particular point in a sequence of actions.
As you may have figured out by now, there is usually more than one way to skin a cat — or code your children’s book!
Replace the line in setUpBookTitle
that calls runAction:
on bookTitle
with the following code:
[bookTitle runAction:[SKAction sequence:@[actionMoveDown, actionMoveUp, actionMoveDownFast, wait, showButton]]]; |
This simply adds the new action to the sequence that bookTitle
runs when called.
The next step is to modify the touch handling in touchesBegan:withEvent:
.
Add the following line to the top of touchesBegan:withEvent:
SKNode *startButton = [self childNodeWithName:@"buttonStart"]; |
This assigns the button – added earlier in the showButton
action – to the node.
Add the following else if
clause after the if
statement in touchesBegan:withEvent:
:
else if([startButton containsPoint:location]) { [_backgroundMusicPlayer stop]; // NSLog(@"xxxxxxxxxxxxxxxxxxx touched read button"); Scene01 *scene = [[Scene01 alloc] initWithSize:self.size]; SKTransition *sceneTransition = [SKTransition fadeWithColor:[UIColor darkGrayColor] duration:1]; [self.view presentScene:scene transition:sceneTransition]; } |
The above code adds a new scene and presents it with a new transition when it detects a user tap on the “Read Story” button.
Build and run your project. After the initial animation, you should see the new “Read Story” button, as shown below:
Tap the “Read Story” button and the next scene presents itself on-screen. You won’t see a whole lot at this point — your next task is add some content to the first page.
Adding Content to the First Page
In this section, you’ll add the first page to the book along with some physics simulation and some touch response logic. Before you do that you’ll need to set up the scene.
Open Scene01.m and add the following import statements to the top of the file:
#import "Scene00.h" #import "Scene02.h" #import "SKTUtils.h" |
The first two lines above give you access to other scenes so you can implement page forward and back controls. The third line allows you to use all of the goodies in SKTUtils
, which is an external resource developed for iOS Games by Tutorials and won’t be covered in detail in this tutorial.
Add the following private variables to the implementation section of Scene01.m:
SKSpriteNode *_footer; SKSpriteNode *_btnLeft; SKSpriteNode *_btnRight; |
Just as before, you’ll need to initialize the scene before you can do anything with it. There are several ways to accomplish this, but this tutorial takes the approach of creating separate methods for each scene instead of handling everything within a single method.
Add the following block of code to Scene01.m, just after the comment that reads /* additional setup */
:
[self setUpText]; [self setUpFooter]; [self setUpMainScene]; |
Those are your basic setup methods — your job is to fill them out. The good news is that the implementation of setUpText
setUpFooter
is identical for each scene in your project, so you’ll only need to write it once for all scenes.
Add the following code to setUpText
in Scene01.m:
SKSpriteNode *text = [SKSpriteNode spriteNodeWithImageNamed:@"pg01_text"]; text.position = CGPointMake(300 , 530); [self addChild:text]; [self readText]; |
Here you create an SKSpriteNode
, set its location, and add it to the scene. The sprite above holds an image for the text of the page; you’ll have one sprite for the text of each page in your book.
This method also calls readText
which plays the narration for the book provided by the very talented Amy Tominac.
Add the following code to readText
in Scene01.m:
if (![self actionForKey:@"readText"]) { SKAction *readPause = [SKAction waitForDuration:0.25]; SKAction *readText = [SKAction playSoundFileNamed:@"pg01.wav" waitForCompletion:YES]; SKAction *readSequence = [SKAction sequence:@[readPause, readText]]; [self runAction:readSequence withKey:@"readText"]; } else { [self removeActionForKey:@"readText"]; } |
Here you create a new SKAction
, much like you did earlier for the bouncing title text. The difference this time — and it’s an important difference — is that you also assign a key name of readText to your action. You’ll discover why a bit later.
You’re also using SKAction’s playSoundFileNamed:waitForCompletion:
class method, which is a really simple method for playing sounds. While this method works great for quick sound effects, it’s probably not the best choice for read-along text because you can’t interrupt the sound when it’s playing, among other reasons. You’re using it in this tutorial for ease-of-use only and to become familiar with the framework.
In a production-level app, you should consider using something with a little more functionality such as the ability to stop sounds while they are playing, such as one of Apple’s other audio technologies.
Build and run, and now you will see and hear the narrated text for page 1!
Adding the Navigation Controls
Next let’s add the navigation controls for your app, which will all be located along the bottom of the screen.
Add the following code to setUpFooter
in Scene01.m:
/* add the footer */ _footer = [SKSpriteNode spriteNodeWithImageNamed:@"footer"]; _footer.position = CGPointMake(self.size.width/2, 38); [self addChild:_footer]; /* add the right button */ _btnRight = [SKSpriteNode spriteNodeWithImageNamed:@"button_right"]; _btnRight.position = CGPointMake(self.size.width/2 + 470, 38); [self addChild:_btnRight]; /* add the left button */ _btnLeft = [SKSpriteNode spriteNodeWithImageNamed:@"button_left"]; _btnLeft.position = CGPointMake(self.size.width/2 + 400, 38); [self addChild:_btnLeft]; /* add the sound button */ if (_soundOff) { // NSLog(@"_soundOff"); [_btnSound removeFromParent]; _btnSound = [SKSpriteNode spriteNodeWithImageNamed:@"button_sound_off"]; _btnSound.position = CGPointMake(self.size.width/2 + 330, 38); [self addChild:_btnSound]; [_backgroundMusicPlayer stop]; } else { // NSLog(@"_soundOn"); [_btnSound removeFromParent]; _btnSound = [SKSpriteNode spriteNodeWithImageNamed:@"button_sound_on"]; _btnSound.position = CGPointMake(self.size.width/2 + 330, 38); [self addChild:_btnSound]; [_backgroundMusicPlayer play]; } |
The code above initializes _footer
with the footer area’s background image, sets its position, and adds it to the scene. It also adds sprites for both the page back and page forward buttons. Finally, it adds the sound toggle button, which should look familiar since it’s essentially the same code you used to set up the button in the previous scene.
Build and run, and you will now see the footer along the bottom of the scene (although tapping it won’t do anything yet):
Now that the basic scene setup is complete, it’s time to add the main character.
Adding The Main Character
Still working in the Scene01.m file, add the following private variable to the Scene01
implementation:
SKSpriteNode *_kid; |
This variable holds a reference to your main character’s sprite.
Next, add the following two lines to setUpMainScene
of Scene01.m:
[self setUpMainCharacter]; [self setUpHat]; |
These methods simply call other methods to keep your code nice and tidy. You’ll populate those next.
Add the following code to setUpMainCharacter
in Scene01.m:
_kid = [SKSpriteNode spriteNodeWithImageNamed:@"pg01_kid"]; _kid.anchorPoint = CGPointZero; _kid.position = CGPointMake(self.size.width/2 - 245, 45); [self addChild:_kid]; [self setUpMainCharacterAnimation]; |
This should be familiar territory to you by now; you create an SKSpriteNode
, set its anchorPoint
and position
, and add it to the scene. Then you call setUpMainCharacterAnimation
to set up the main character animation.
setUpMainCharacterAnimation
only exists as a shell — time to add some madness to your method! :]
Add the following code to setUpMainCharacterAnimation
in in Scene01.m:
NSMutableArray *textures = [NSMutableArray arrayWithCapacity:2]; for (int i = 0; i <= 2; i++) { NSString *textureName = [NSString stringWithFormat:@"pg01_kid0%d", i]; SKTexture *texture = [SKTexture textureWithImageNamed:textureName]; [textures addObject:texture]; } CGFloat duration = RandomFloatRange(3, 6); SKAction *blink = [SKAction animateWithTextures:textures timePerFrame:0.25]; SKAction *wait = [SKAction waitForDuration:duration]; SKAction *mainCharacterAnimation = [SKAction sequence:@[blink, wait, blink, blink, wait , blink, blink]]; [_kid runAction: [SKAction repeatActionForever:mainCharacterAnimation]]; |
Animations are performed using a series of images. In the code above, the main character blink animation uses two images to achieve this effect.
The first line creates an array to hold your images. Following that, you generate the image name, create an SKTexture
object for each image in the animation, then add that object to your array.
Next you create the animation sequence using SKAction
‘s animateWithTextures:timePerFrame:
class method which expects an array of textures.
Finally, you instruct the _kid
sprite to perform its action. By using the repeatActionForever
method, you tell the node to run this action continuously.
Build and run your project; hit the “Read Story” button and you’ll see the main character appear on-screen and blink his eyes while the narration plays in the background, as so:
An Introduction to Physics
You can really improve the appeal of your book by adding some interactivity. In this section, you’re going to create a hat for the main character which the reader can drag around the screen and place on the main character’s head.
Still working in Scene01.m, add the following instance variable to the Scene01
implementation:
SKSpriteNode *_hat; |
Add the following code to setUpHat
in Scene01.m:
SKLabelNode *label = [SKLabelNode labelNodeWithFontNamed:@"Thonburi-Bold"]; label.text = @"Help Mikey put on his hat!"; label.fontSize = 20.0; label.fontColor = [UIColor yellowColor]; label.position = CGPointMake(160, 180); [self addChild:label]; _hat = [SKSpriteNode spriteNodeWithImageNamed:@"pg01_kid_hat"]; _hat.position = CGPointMake(150, 290); _hat.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:_hat.size]; _hat.physicsBody.restitution = 0.5; [self addChild:_hat]; |
The first half of the code above creates an SKLabelNode
object and adds it to the scene. An SKLabelNode
is similar to a UILabel
; in Sprite Kit, it’s a node used to draw a string.
The second half of the code adds the physics to the scene. You created the SKSpriteNode
and assigned it to the _hat
variable as well as an SKPhysicsBody
which lets you apply a number of physical characteristics to your objects such as shape, size, mass, and gravity and friction effects.
Calling the SKPhysicsBody
‘s bodyWithRegtangleOfSize:
class method sets the shape of the body to match the node’s frame. You also set the restitution
to 0.5
which means your physics body will bounce off objects with half of its initial force.
Build and run your project; tap the “Read Story” button and…huh? Where did the hat go?
If you were watching the screen closely, you may have noticed the hat falling off the screen. That’s because there was no opposing body to stop it from falling.
You can fix that by adding a physics body to act as the ground.
Add the following code to setUpFooter
in Scene01.m, just after the line that calls addChild:
to add _footer
to the scene:
self.physicsBody = [SKPhysicsBody bodyWithEdgeLoopFromRect:_footer.frame]; |
Build and run your project again; this time, the hat has something to land on — the physics body you set up in the footer. As well, you can see the yellow text label that you created with SKLabelNode
, as shown below:
Okay, you’ve added some physics properties to the hat — but how do you go about adding interactivity?
Handling Touches and Moving the Hat
This section implements the touch handling for the hat so that you can move it around the screen, as well as touch handling for the Next, Previous and sound preference button.
touchesBegan:withEvent:
will end up as one of the longest methods you’ll write; it already includes the processing you wrote for Scene00
to toggle the background music, and you’ll add supporting code for the next/previous page buttons and for moving the hat in the next few code blocks.
Add the following block of code immediately after the existing if
statement of touchesBegan:withEvent:
in Scene01.m:
else if ([_btnRight containsPoint:location]) { // NSLog(@">>>>>>>>>>>>>>>>> page forward"); if (![self actionForKey:@"readText"]) // do not turn page if reading { [_backgroundMusicPlayer stop]; SKScene *scene = [[Scene02 alloc] initWithSize:self.size]; SKTransition *sceneTransition = [SKTransition fadeWithColor:[UIColor darkGrayColor] duration:1]; [self.view presentScene:scene transition:sceneTransition]; } } else if ([_btnLeft containsPoint:location]) { // NSLog(@"<<<<<<<<<<<<<<<<<< page backward"); if (![self actionForKey:@"readText"]) // do not turn page if reading { [_backgroundMusicPlayer stop]; SKScene *scene = [[Scene00 alloc] initWithSize:self.size]; SKTransition *sceneTransition = [SKTransition fadeWithColor:[UIColor darkGrayColor] duration:1]; [self.view presentScene:scene transition:sceneTransition]; } } |
Here you check to see if the Next or Previous page buttons were the target of the touch event, much like you did for the sound toggle button. You then handle the touch event by stopping the background music and moving to the appropriate scene.
However, there is one small difference here. Recall the key that you set for the action that narrates the text?
[self runAction:readSequence withKey:@"readText"]; |
The block of code you added above uses that key to check if the readText
action is currently playing. If it is, do nothing. If it’s not, turn the page.
But why check at all?
The reason is that when you start an SKAction
that plays a sound, it’s impossible to interrupt that sound. This is why you can’t turn the page while the text is being read. As was mentioned earlier, you’ll probably want to use something more robust to narrate the text in your production-level app.
It would be nice to give the reader a way to jump back to the first scene, no matter where they are in the book.
Add the following code right below the code that you added previously, which will take you back to the first scene when the user touches the book title in the footer:
else if ( location.x >= 29 && location.x <= 285 && location.y >= 6 && location.y <= 68 ) { // NSLog(@">>>>>>>>>>>>>>>>> page title"); if (![self actionForKey:@"readText"]) // do not turn page if reading { [_backgroundMusicPlayer stop]; SKScene *scene = [[Scene00 alloc] initWithSize:self.size]; SKTransition *sceneTransition = [SKTransition fadeWithColor:[UIColor darkGrayColor] duration:1]; [self.view presentScene:scene transition:sceneTransition]; } } |
The above code tests the touch location a little differently than the other checks you’ve made. Because the book’s title is part of the footer image, this simply checks to see whether or not the touch location falls within the area where you know the book title appears. It’s usually not a good idea to have “magic numbers” like this strewn about your app, but it serves to keep things simple in this tutorial.
The rest of the above code is exactly like the code that handles the Previous Page button touch events.
Finally, you’ll need to handle touch events on the hat. To do so, you’ll need to store some data between events.
Add the following two instance variables to the Scene01
implementation of Scene01.m:
BOOL _touchingHat;
CGPoint _touchPoint; |
In the above code, _touchingHat
stores whether the user is currently touching the hat, while _touchPoint
stores the most recent touch location.
To ensure the hat catches any touch events that occur both over it and another target area, check the hat first.
To do this, change the first if
in touchesBegan:withEvent:
to an else if
so that it looks like the following:
else if([_btnSound containsPoint:location]) |
Next, add the following code immediately above the line you just changed in Scene01.m:
if([_hat containsPoint:location]) { // NSLog(@"xxxxxxxxxxxxxxxxxxx touched hat"); _touchingHat = YES; _touchPoint = location; /* change the physics or the hat is too 'heavy' */ _hat.physicsBody.velocity = CGVectorMake(0, 0); _hat.physicsBody.angularVelocity = 0; _hat.physicsBody.affectedByGravity = NO; } |
When the user first touches the hat, the code above sets the _touchingHat
flag to YES
and stores the touch location in _touchPoint
. It also makes a few changes to the hat’s physics body. These changes are necessary because without them it’s virtually impossible to drag the hat around the screen as you’re constantly fighting with the physics engine.
You’ll need to track the touch as it moves across the screen, so add the following code to touchesMoved:withEvent:
in Scene01.m:
_touchPoint = [[touches anyObject] locationInNode:self]; |
Here you update the most recent touch location stored in _touchPoint
.
When the user stops touching the screen, you need to reset any hat-related data.
Add the following code to both touchesEnded:withEvent:
and touchesCancelled:withEvent:
:
_touchingHat = NO; _hat.physicsBody.affectedByGravity = YES; |
The above code sets the _touchingHat
flag to NO and re-enables gravity for the hat so that it will fall back to the floor when the user releases it.
There’s just one more thing to do to get the hat to track the user’s finger as it moves on the screen.
Add the following code to update:
:
if (_touchingHat) { _touchPoint.x = Clamp(_touchPoint.x, _hat.size.width / 2, self.size.width - _hat.size.width / 2); _touchPoint.y = Clamp(_touchPoint.y, _footer.size.height + _hat.size.height / 2, self.size.height - _hat.size.height / 2); _hat.position = _touchPoint; } |
update
invokes before each frame of the animation renders. Here you check to see if the user is dragging the hat; if it is, change _hat
‘s current location to the position stored in _touchPoint
. You’re using the Clamp
function from SKTUtils
to ensure the hat doesn’t move off the screen or below the footer.
Build and run your project; hit the “Read Story” button, and play around with the hat on the screen a little.
Moving the hat around is cool, but there isn’t any feedback as to whether or not the hat is on top of the main character’s head. It’s time to add that feedback.
Modify touchesEnded:withEvent:
so that it looks like the code below:
if (_touchingHat) { CGPoint currentPoint = [[touches anyObject] locationInNode:self]; if ( currentPoint.x >= 300 && currentPoint.x <= 550 && currentPoint.y >= 250 && currentPoint.y <= 400 ) { // NSLog(@"Close Enough! Let me do it for you"); currentPoint.x = 420; currentPoint.y = 330; _hat.position = currentPoint; SKAction *popSound = [SKAction playSoundFileNamed:@"thompsonman_pop.wav" waitForCompletion:NO]; [_hat runAction:popSound]; } else _hat.physicsBody.affectedByGravity = YES; _touchingHat = NO; } |
With the above bit of code you can determine if the user is touching the hat and where they attempted to release it. This is why you want to use the end event and not the begin event.
If the user releases the hat close enough to the kid’s head, your code re-positions the hat to an exact location as defined by currentPoint.x
and currentPoint.y
.
Additionally, a sound plays using SKAction
‘s playSoundFileNamed:
class method to alert the user that the hat is now firmly placed on the main character’s head – which is important! Did you see all that snow outside the window? Brrrrr!
Build and run your project; grab the hat and plunk it down on your character’s head, like so:
Adding Page Two
The code for page two is roughly the same as that for page one; to this end, it’s been pre-populated for you in Scene02.m. The code is heavily commented for your reading pleasure, and since it’s very similar to Scene01.m it won’t be covered in detail here.
One thing to note is that Scene02
has an instance variable — _catSound
— that references an SKAction
that plays a sound. This illustrates that you can store SKAction
s and reuse them, but it also improves the user experience in the case of actions that play sounds.
Your app loads the sound into memory when you create a sound action, so initializing the sound action before it’s needed eliminates any potential pauses your users might experience.
Build and run your project; tap “Read Story” and use the navigation buttons to move back and forth in the story (after the text is done reading), like so:
As an added bonus, tap on the cat to hear him meow!
Ok, enough messing around with pre-written code and kitties. Back to work, lazy pants! :]
Adding Page Three
Hopefully you enjoyed your short break to play with the kitty, but don’t think that just because you didn’t write the code for Page Two that it’s not in your best interest to review it! There’s lots to be learned by reviewing code written by others.
In the interest of keeping this tutorial focused, Scene03.m also provides a lot of the code you’ll need, but there’s still a little to add.
Add the following code to setUpMainScene
in Scene03.m:
/* add the kid and the cat */ SKSpriteNode *cat = [SKSpriteNode spriteNodeWithImageNamed:@"pg03_kid_cat"]; cat.anchorPoint = CGPointZero; cat.position = CGPointMake(self.size.width/2 - 25, 84); [self addChild:cat]; /* add Snowflakes */ float duration = 1.25f; // determines how often to create snowflakes [self runAction:[SKAction repeatActionForever:[SKAction sequence:@[[SKAction performSelector:@selector(spawnSnowflake) onTarget:self],[SKAction waitForDuration:duration]]]]]; |
Just as before, you create a new SKSpriteNode
, set its anchorPoint and position and add it to the scene. Nothing surprising there.
Next you create an SKAction
sequence that runs forever. You can use SKAction
’s performSelector:onTarget:
to run any method of your choosing. In this case, the action runs spawnSnowflake
; you’ll be writing that method in a moment.
The above sequence ends with a waitForDuration:
action; since the sequence repeats forever, it calls spawnSnowflake
, pauses for the specified number of seconds as stored in duration
, then repeats those two actions forever.
Add the following code to spawnSnowflake
in Scene03.m:
// here you can even add physics and a shake motion too CGFloat randomNumber = RandomFloatRange(1, 4); CGFloat duration = RandomFloatRange(5, 21); NSString *snowflakeImageName = [NSString stringWithFormat:@"snowfall0%.0f",randomNumber]; SKSpriteNode *snowflake = [SKSpriteNode spriteNodeWithImageNamed:snowflakeImageName]; snowflake.name = @"snowflake"; snowflake.position = CGPointMake(RandomFloatRange(0, self.size.height), self.size.height + self.size.height/2); [self addChild:snowflake]; SKAction *actionMove = [SKAction moveTo:CGPointMake(snowflake.position.x + 100, -snowflake.size.height/2) duration:duration]; SKAction *actionRemove = [SKAction removeFromParent]; [snowflake runAction:[SKAction sequence:@[actionMove, actionRemove]]]; |
The block of code above executes continuously because the SKAction
calling this method was instructed to run forever. Talk about willing to make a commitment. Men, please take note. ;]
So what does that code actually do?
- It generates an
SKSpriteNode
using a random snowflake image. - It creates and executes an
SKAction
sequence that includes an action to move the sprite down the screen usingSKAction
‘smoveTo:
class method. - Finally, it removes the sprite from the scene using
SKAction
‘sremoveFromParent
class method.
Build and run your project; you should see snowflakes start to appear and move down the screen, as shown below:
duration
variable to 0.15. The value is inversely proportional to the number of snowflakes on the screen. Build and run your project and watch a blizzard unfold right before your very own eyes! When you’re done creating the snowpocalypse, move along — that’s enough playing in the snow for now!Eagle-eyed readers will have noticed that the snowflakes run right over your footer. Time to fix that!
Add the following code to checkCollisions
in in Scene03.m:
[self enumerateChildNodesWithName:@"snowflake" usingBlock:^(SKNode *node, BOOL *stop) { SKSpriteNode *snowflake = (SKSpriteNode *)node; CGRect footerFrameWithPadding = CGRectInset(_footer.frame, -25, -25); // set padding around foot frame if (CGRectIntersectsRect(snowflake.frame, footerFrameWithPadding)) { [snowflake removeFromParent]; } }]; |
Using enumerateChildNodesWithName:
, you’re able to detect nodes with the specified name. Since you gave each snowflake a node name of ‘snowflake’, you can do two things here: one, apply the collision detection on matching nodes only, and two, remove the node only if you detect a collision. You detect collisions using CGRectIntersectsRect
.
The code above looks and works great, but how do you call it? How does the program know when to run it? That’s where the infamous “game loop” comes in handy. You may have noticed it already, but it’s explained in detail below if you didn’t follow through the code.
Sprite Kit uses a traditional rendering loop — often referred to as a game loop — to process the contents of each frame before its rendered and presented. Sprite Kit calls update:
for each frame; it’s in this method that you will write the code to call the collision detection method you created above.
Add the following code to update:
in Scene03.m:
[self checkCollisions]; |
Build and run your project; you now should see the snowflake sprites disappear as they approach the top edge of the footer, as shown below:
Where To Go From Here?
At this point, the rest of the story is up to you!
Note that the Scene04.m file has been populated with a generic “to be continued” scene. I want you to create Page Four using everything you’ve learned in this tutorial. Here’s a mockup of what your page could look like:
Everything you need is already included with the project. Have fun!
I hope you enjoyed working through this tutorial as much as I have enjoyed writing it. To compare notes, download the complete sample project herehere.
If you’re looking to learn more about Sprite Kit, be sure to check out our book, iOS Games by Tutorials.
If you have any questions or comments, feel free to join in the discussion below!
How to Create an Interactive Children’s Book for the iPad is a post from: Ray Wenderlich
The post How to Create an Interactive Children’s Book for the iPad appeared first on Ray Wenderlich.