How to Create an Interactive Children’s Book for the iPad

Learn how to create your own beautiful animated interactive children’s book for the iPad! By Tammy Coron.

Leave a rating/review
Save for later
Share
You are currently viewing page 4 of 5 of this article. Click here to view the first page.

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:

tc_spritekit_build5

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.

Note: Try building the app both with and without the changes to the hat’s physics bodies to see the difference it makes. You should be able to move the hat around with your finger easily when the changes to the physics body are applied, but it's a little more difficult to move it around when you comment out the changes.

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:

tc_spritekit_build7