How To Make a Letter / Word Game with UIKit: Part 3/3

Marin Todorov
Learn how to create a fun anagrams game!

Learn how to create a fun anagrams game!

Welcome to the our tutorial series about creating a letter / word game with UIKit for the iPad.

If you successfully followed through the first and second parts of this tutorial, your game should be pretty functional. But it still lacks some key game elements and could use a bit more eye candy.

This third and final part of the series will be the most fun of them all! In this part, you’re going to be adding a lot of cool and fun features:

  • Visual effects to make tile dragging more satisfying
  • Gratuitous sound effects
  • In-game help for players
  • A game menu for choosing the difficulty level
  • And yes… explosions! :]

That’s quite a lot, so let’s once again get cracking!

Enhanced Tile Dragging: Not a Drag

When you run the completed project from Part 2 of this tutorial, you have this:

Your players can drag tiles around on the screen just fine, but the effect is very two-dimensional. In this section you’ll see how some simple visual effects can make the dragging experience much more satisfying.

When the player begins dragging a tile, it would be nice if the tile got a little bigger and showed a drop shadow below itself. These effects will make it look like the player is actually lifting the tile from the board while moving it.

To accomplish this effect, you’ll be using some features of QuartzCore. I’ve already included the QuartzCore framework in the starter project, so all you have to do to access it is add the proper import statement.

Open TileView.m and add the following import:

#import <QuartzCore/QuartzCore.h>

Now add the following code inside initWithLetter:andSideLength:, just after the line where you enabled user interactions:

//create the tile shadow
self.layer.shadowColor = [UIColor blackColor].CGColor;
self.layer.shadowOpacity = 0;
self.layer.shadowOffset = CGSizeMake(10.0f, 10.0f);
self.layer.shadowRadius = 15.0f;
self.layer.masksToBounds = NO;

UIBezierPath *path = [UIBezierPath bezierPathWithRect:self.bounds];
self.layer.shadowPath = path.CGPath;

Every UIView has a property called layer – this is the CALayer class layer used to draw the view. Luckily CALayer has a set of properties used to create a drop shadow. The property names mostly speak for themselves, but a few probably need some extra explanation.

  • masksToBounds should be set to NO if you are creating a drop shadow, because a drop shadow is rendered outside of the view’s bounds. If it were set to YES, you wouldn’t see the shadow.
  • shadowPath is a UIBezierPath that describes the shadow shape. Use this whenever possible to speed up the shadow rendering. In the code above, you use a rectangle path (same as the tile’s bounds), but you also apply rounding to the rectangle’s corners via shadowRadius, and you effectively have a shadow with the form of the tile itself. Nice!
  • Note the line self.layer.shadowOpacity = 0;. This makes the shadow invisible. So you create the shadow when the tile is initialized, but you show and hide it when the user drags the tile.

Add the following code at the end of touchesBegan:withEvent::

    //show the drop shadow
    self.layer.shadowOpacity = 0.8;

This turns on the shadow when the user starts dragging. Setting the opacity to 0.8 gives it a bit of transparency, which looks nice.

Add the following code at the end of touchesEnded:withEvent::

    self.layer.shadowOpacity = 0.0;

This turns off the shadow. The effect will look even better when you change the tile size, but build and run the app now just to see it working:

It’s a nice subtle touch that makes the user will appreciate – even if subconsciously.

Now to handle resizing the tile so that it looks like it’s being lifted off the board. First add the following code to the end of touchesBegan:withEvent::

    //enlarge the tile
    self.transform = CGAffineTransformScale(self.transform, 1.2, 1.2);

This sets the size of the tile to be 120% of the current tile size. Build and run. You’ll see the tiles get larger when you drag them:

However, when you stop dragging the tile, it doesn’t go back to its regular size. And if you keep tapping the same tile, things quickly get out of control:

It’s the tile that ate your iPad!! This won’t do. Thankfully, it’s an easy fix.

First add the following private variable to the variables section of TileView.m:

    CGAffineTransform _tempTransform;

You’ll use _tempTransform to temporarily store the tile view’s original transform before the player drags it, so that you can restore it when the drag is complete. The transform stores things like scale and rotation.

Now add the following line to touchesBegan:withEvent:, just above where you set the transform:

    //save the current transform
    _tempTransform = self.transform;

This saves a copy of the original transform so you can restore it later.

Now add this to the end of touchesEnded:withEvent::

    //restore the original transform
    self.transform = _tempTransform;

This restores the tile’s size to its pre-drag state. Build and run again and tile behavior should be fixed. Or is it?

Now the tiles aren’t straightening out when they are placed correctly!

This is a simple error, caused by the order in which you added the lines in touchesEnded:withEvent:. First you notified the tile’s delegate, and then you restored the original transform. But remember how the delegate also modifies the tile’s transform?

Fix this by moving the line you just added up above the line that reads if (self.dragDelegate). Now your method should look like this:

-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
  [self touchesMoved:touches withEvent:event];
  
  self.transform = _tempTransform;

  if (self.dragDelegate) {
    [self.dragDelegate tileView:self didDragToPoint:self.center];
  }

  self.layer.shadowOpacity = 0.0;
}

While you’re at it, add the following method right after touchesEnded:withEvent::

//reset the view transoform in case drag is cancelled
-(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
    self.transform = _tempTransform;
    self.layer.shadowOpacity = 0.0;
}

iOS calls touchesCancelled:withEvent: in certain special situations, like when the app receives a low memory warning. Your use of it ensures that the tile’s display is properly restored.

Another build and run, another hope that things will be working as expected! Well, almost… You may have already noticed it while developing the app, but what’s wrong with the following screenshot?

As you can see, the tile the player is dragging is displayed below another tile! That really destroys the illusion of lifting the tile.

This occurs because a parent view always displays its subviews in the order that they are added to it. That means any given tile may be displayed above some tiles and below others. Not good.

To fix this, add the following code to the end of touchesBegan:withEvent::

  [self.superview bringSubviewToFront:self];

This tells the view that contains the tile (self.superview) to display the tile’s view above all other views. Dragging should feel a lot better now.

Challenge: If you want to make tile dragging even cooler, how about if touching a tile that’s underneath other tiles animated those tiles out of the way, so it looks like the pile of tiles is disturbed by lifting the bottom tile? How would you implement that?

Adding Audio: From Mute to Cute

Tile dragging is now more realistic, but there’s only so far realism can go without audio. Sound is omnipresent and interactive in our daily lives, and your game can’t go without it.

The Xcode starter project includes some Creative Commons-licensed sound effects for your game. There are three sound files, which will correspond with the following game actions:

  • ding.mp3: Played when a tile matches a target.
  • wrong.m4a: Played when a tile is dropped on a wrong target.
  • win.mp3: Played when an anagram is solved.

For convenience, you will pre-define these file names in config.h. Add them after the UI defines you added there earlier:

//audio defines
#define kSoundDing  @"ding.mp3"
#define kSoundWrong @"wrong.m4a"
#define kSoundWin   @"win.mp3"

#define kAudioEffectFiles @[kSoundDing, kSoundWrong, kSoundWin]

You have each file name separately, to use when you want to play a single file, and also an array of all the sound effect file names, to use when preloading the sounds.

Now create a new Objective-C class in Anagrams/Classes/controllers, call it AudioController and make it a subclass of NSObject.

In AudioController.h inside the interface definition, add these two methods:

-(void)playEffect:(NSString*)name;
-(void)preloadAudioEffects:(NSArray*)effectFileNames;

Then in AudioController.m, add the framework that you need to play audio or video:

#import <AVFoundation/AVFoundation.h>

You will preload all sound effects and store them in a private variable in the AudioController class. Inside AudioController.m, add the following private variable section:

@implementation AudioController
{
    NSMutableDictionary* audio;
}

Next add the following method to preload all sound files:

-(void)preloadAudioEffects:(NSArray*)effectFileNames
{
    //initialize the effects array
    audio = [NSMutableDictionary dictionaryWithCapacity: effectFileNames.count];
    
    //loop over the filenames
    for (NSString* effect in effectFileNames) {
        
        //1 get the file path URL
        NSString* soundPath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent: effect];
        NSURL* soundURL = [NSURL fileURLWithPath: soundPath];
        
        //2 load the file contents
        NSError* loadError = nil;
        AVAudioPlayer *player = [[AVAudioPlayer alloc] initWithContentsOfURL:soundURL error: &loadError];
        NSAssert(loadError==nil, @"load sound failed");
        
        //3 prepare the play
        player.numberOfLoops = 0;
        [player prepareToPlay];
        
        //4 add to the array ivar
        audio[effect] = player;
    }
}

First you initialize audio, then you loop over the provided list of names and load the sounds. This process consist of a few steps:

  1. Get the full path to the sound file and convert it to a URL by using [NSURL fileURLWithPath:].
  2. Call [AVAudioPlayer initWithContentsOfURL:error:] to load a sound file in an audio player.
  3. Set the numberOfLoops to zero so that the sound won’t loop at all. Call prepareToPlay to preload the audio buffer for that sound.
  4. Finally, save the player object in the audio dictionary, using the name of the file as the dictionary key.

That effectively loads all sounds, prepares them for a fast audio start and makes them accessible by their file name. Now you just need to add one method, which plays a given sound:

-(void)playEffect:(NSString*)name
{
    NSAssert(audio[name], @"effect not found");
    
    AVAudioPlayer* player = (AVAudioPlayer*)audio[name];
    if (player.isPlaying) {
        player.currentTime = 0;
    } else {
        [player play];
    }
}

And voila! You have it!

The method takes in a file name, then looks it up in audio. If the sound is not loaded, the NSAssert will crash the app. After you have fetched the given sound player, you check if the sound currently playing. If so, you just need to “rewind” the sound by setting its currentTime to 0; otherwise, you simply call play.

Hooray! You now have a simple audio controller for your game. Using it is extremely simple, too. First you need to preload all audio files when the game starts.

Switch to GameController.h and distribute this code in the file:

//at the top with the rest of the imports
#import "AudioController.h"

//with the other properties
@property (strong, nonatomic) AudioController* audioController;

The audioController property stores the audio controller the GameController will use to play sounds.

Inside GameController.m, add the following lines to init right after the line that initializes the data property:

    self.audioController = [[AudioController alloc] init];
    [self.audioController preloadAudioEffects: kAudioEffectFiles];

This makes a new instance of the audio controller and uses the method you just wrote to preload the sounds listed in the config file. Nice! You can now go straight to playing some awesome sound effects!

Inside tileView:didDragToPoint:, add the following line after the comment that reads “//more stuff to do on success here”:

[self.audioController playEffect: kSoundDing];

Now the player will see and hear when they’ve made a valid move!

Also inside tileView:didDragToPoint:, add the following line after the comment that reads “//more stuff to do on failure here”:

[self.audioController playEffect:kSoundWrong];

Now the player will hear when they’ve made a mistake, too.

You can use the simple audio controller to preload any list of sounds and play them anywhere in the game.

Build and run, and check out whether you can hear the sounds being played at the right moment.

As long as you’re taking care of the sound effects, you can also add the “winning” sound to the game. Just play the win.mp3 file at the end of checkForSuccess:

//the anagram is completed!
[self.audioController playEffect:kSoundWin];

Sweet!

Note: I personally love working with my Logic Express, super-awesome music/audio software from Apple that is rock-solid and easy to use. You can grab Logic Pro from the App Store for $199, or cheat by getting Logic Express (as long as it’s still being sold) for $59 from Amazon.

If you just need an app to convert a couple of files for non-commercial use, you can also get Switch for free, which I’ve used for years for quick and dirty jobs.

Ladies & Gentlemen – Explosions!

Gamers love explosions. Making them at least – cool gamers don’t look at explosions and just keep on playing :]

Tough luck, though, as UIKit does not have explosions – right? Guess again. If you think that, then you probably haven’t read iOS 5 by Tutorials, where I wrote a little chapter on particle effects with UIKit.

An abbreviated version of that chapter is also available online.

Your Xcode starter project already includes a particle image file. Drill down in the file explorer to Anagrams/Assets/Particles, find particle.png and open it.

As you can see, there’s nothing fancy about the image – it’s just a white blur in fact, but that’s all it takes to create an explosion.

A particle image

Unfortunately, to keep the tutorial to a sane length I can’t go into much detail about the UIKit particle systems. But you can always lookup the Apple documentation and check out my previous writings on the subject. With just a little bit of particle system understanding, you will “get it” for sure.

You will now create two particle systems to use in the game.

Add a new class file in Anagrams/Classes/views called ExplodeView and make it a subclass of UIView.

As the name hints, this system will emulate an explosion. From a given point in space, many particles will accelerate in all directions, until they disappear quite fast. Like so:



Believe it or not, this will be quite easy. To begin, inside ExplodeView.m, add the following:

//1
//at the top with the imports
#import "QuartzCore/QuartzCore.h"

//2
//create the following private variable section
@implementation ExplodeView
{
    CAEmitterLayer* _emitter;
}

// 3
// with the other methods
+ (Class) layerClass
{
    //configure the UIView to have emitter layer
    return [CAEmitterLayer class];
}

Here’s what’s happening above:

  1. Import QuartzCore to access the particle system classes.
  2. _emitter is a convenience variable used to give you access to the UIView‘s layer property, cast as a CAEmitterLayer*.
  3. layerClass is a class method of UIView. You override this method when you want to change the underlying layer of your view class. Here you return the CAEmitterLayer class and UIKit ensures that self.layer returns an instance of CAEmitterLayer.

All right, you’re ready to start configuring the particle system. Replace the existing initWithFrame: with the following:

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        //initialize the emitter
        _emitter = (CAEmitterLayer*)self.layer;
        _emitter.emitterPosition = CGPointMake(self.bounds.size.width /2, self.bounds.size.height/2 );
        _emitter.emitterSize = self.bounds.size;
        _emitter.emitterMode = kCAEmitterLayerAdditive;
        _emitter.emitterShape = kCAEmitterLayerRectangle;
    }
    return self;
}

The above code stores a reference to self.layer in _emitter, cast to type CAEmitterLayer* because you know that’s what it is. (Remember, you overrode layerClass to make it so.) This makes it easier to manipulate your layer later on.

Then the code sets various properties on the emitter layer. Read through the code and if in doubt about a certain CAEmitterLayer property, consult the Apple docs or my particle systems tutorial.

The settings in initWithFrame: only adjust the particle emitter layer. To actually start emitting particles, you need an emitter cell. You will create and configure your object’s emitter cell at the point when the view is added into the view controller’s hierarchy – that is, as soon as you add the view to a parent view.

Add the following implementation of didMoveToSuperview, which is the method that gets called on a UIView object when it’s added to its parent:

-(void)didMoveToSuperview
{
    //1
    [super didMoveToSuperview];
    if (self.superview==nil) return;

    //2  
    UIImage* texture = [UIImage imageNamed:@"particle.png"];
    NSAssert(texture, @"particle.png not found");    
    
    //3
    CAEmitterCell* emitterCell = [CAEmitterCell emitterCell];

    //4
    emitterCell.contents = (__bridge id)[texture CGImage];

    //5
    emitterCell.name = @"cell";

    //6
    emitterCell.birthRate = 1000;
    emitterCell.lifetime = 0.75;

    //7
    emitterCell.blueRange = 0.33;
    emitterCell.blueSpeed = -0.33;

    //8
    emitterCell.velocity = 160;
    emitterCell.velocityRange = 40;

    //9
    emitterCell.scaleRange = 0.5;
    emitterCell.scaleSpeed = -0.2;

    //10
    emitterCell.emissionRange = M_PI*2;

    //11
    _emitter.emitterCells = @[emitterCell];
}

That’s quite a bit of code. Let’s go through it one step at a time:

  1. Call the parent class’s implementation of didMoveToSuperview and then exit the method if there is no superview set on this object. This might happen if the object were removed from its parent.
  2. Load the particle.png image into a UIImage instance. There’s another one of those NSAssert statements that crashes the app if it can’t find the file it’s looking for.
  3. Create a new emitter cell by calling the class method [CAEmitterCell emitterCell]. Most of the rest of the method is spent configuring this object.
  4. Set the cell’s contents property to the texture you loaded. This is the image that will be used to create each of the particles that this cell will emit. The final shape and color of the particle will be determined by the other settings you make in this method. And if you don’t know what that (__bridge id) is, take a look at this Introduction to ARC tutorial.
  5. Name the cell “cell”. The name will be used later to modify the emitter layer’s properties via key-value coding. You can check out Apple’s docs if you want to learn more about key-value coding, but you don’t need for this tutorial.
  6. Set the cell’s birthRate property to 1000, which tells it to create 1000 particles per second. You also set the cell’s lifetime property to 0.75, which makes each particle exist for 0.75 seconds.
  7. Here you set the cell’s color to randomly vary its blue component. Setting blueRange to 0.33 will get you particles with random colors between [1,1,1] (rgb) and [1,1,0.67] (rgb) – basically anywhere between white and orange. You also set a negative blueSpeed – this will decrease the blue component of the blending over time, decreasing the intensity of the particle’s color.
  8. The birth velocity of the particles will be anywhere between 120 and 200. I hope you already see the pattern of how the range property works.
  9. The particles are emitted by default with a scale of 1.0. Therefore, setting a range of 0.5 will get you random particles from 0.5 to 1.5 times their original size. Finally you set a negative speed for the scale, thus continuing to shrink the particles over time. Not much shrinking will happen over a 0.75-second lifetime, but it for sure adds to the effect.
  10. Here you set a range (an angle) for the direction the emitter will emit the cells. You set it to a range of 360 degrees – that is, to randomly emit particles in all directions. Remember, this method takes it’s value in radians, not degrees, so 2 pi radians.
  11. Finally, you add the cell you created to the emitter layer. emitterCells is an array of CAEmitterCells for this emitter. (You can have more than one.)

Aaaaand you’re done. :]

You have now an awesome explosion view. Now add it behind a tile that has been dropped onto a proper target. Open up GameController.m and add:

//at the top of the file
#import "ExplodeView.h"

//at the end of placeTile:atTarget:
ExplodeView* explode = [[ExplodeView alloc] initWithFrame:CGRectMake(tileView.center.x,tileView.center.y,10,10)];
[tileView.superview addSubview: explode];
[tileView.superview sendSubviewToBack:explode];

You make a new instance of ExplodeView, positioned at the center of the dropped tile view, and add it to the gameView. To make sure it does not cover the tile, you also call sendSubviewToBack to make the explosion happen behind the tile.

Go! Go! Go! Build and run the project! Wowza!

That wasn’t that difficult, right? However, the effect looks much more like a gas leak than a single explosion. And also – it never stops!

What you would like to do now is kill the emitter after a second to make it look more like an explosion.

Go back to ExplodeView.m and add a method to stop the emitter cell.

-(void)disableEmitterCell
{
    [_emitter setValue:@0 forKeyPath:@"emitterCells.cell.birthRate"];
}

This is that key-value coding I mentioned earlier. The above string accesses the birthRate property of the object named cell that is contained in the array returned from the emitterCells property. By instructing the cell to emit 0 particles per second, you effectively turn it off. Nice.

Now add the following to the end of didMoveToSuperview:

[self performSelector:@selector(disableEmitterCell) withObject:nil afterDelay:0.1];
[self performSelector:@selector(removeFromSuperview) withObject:nil afterDelay:2.0];

First you schedule a call in 1/10th of a second to disable the emitter, and after 2 seconds, you make another call to remove the explosion view from its parent. Why not just remove the view? You want to let the exploded particles fly away and dissolve before killing the effect.

Build and run the game again and enjoy some nice explosions!


Note: After I finished creating this tutorial, I realized that I don’t know of an easier way to create particle systems that use Apple’s CAEmitterLayer, except by using the code as I showed you above. So I sat down and put together an app called UIEffectDesigner, which allows you to visually design your particle system effects and then show them onscreen with just a couple of lines of code. It’s still an early beta, but if you are interested in creating particle systems for UIKit or AppKit, give it a try.

Now you’re going to add one more effect. It’s quite similar to ExplodeView, but involves a bit of gravity, since this effect will be a long lasting emitter rather than a simple explosion.

Create a new class StarDustView in Anagrams/Classes/views, making it a subclass of UIView.

Replace the contents of StarDustView.m with:

#import "StarDustView.h"
#import "QuartzCore/QuartzCore.h"

@implementation StarDustView
{
    CAEmitterLayer* _emitter;
}

+ (Class) layerClass
{
    //configure the UIView to have emitter layer
    return [CAEmitterLayer class];
}

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        //initialize the emitter
        _emitter = (CAEmitterLayer*)self.layer;
        _emitter.emitterPosition = CGPointMake(self.bounds.size.width /2, self.bounds.size.height/2 );
        _emitter.emitterSize = self.bounds.size;
        _emitter.emitterMode = kCAEmitterLayerAdditive;
        _emitter.emitterShape = kCAEmitterLayerRectangle;
    }
    return self;
}

-(void)didMoveToSuperview
{
    [super didMoveToSuperview];
    
    if (self.superview==nil) return;
    
    //load the texture image
    UIImage* texture = [UIImage imageNamed: @"particle.png"];
    NSAssert(texture, @"particle.png not found");
    
    //create new emitter cell
    CAEmitterCell* emitterCell = [CAEmitterCell emitterCell];
    
    emitterCell.contents = (__bridge id)[texture CGImage];
    emitterCell.name = @"cell";
    
    emitterCell.birthRate = 200;
    emitterCell.lifetime = 1.5;

    emitterCell.blueRange = 0.33;
    emitterCell.blueSpeed = -0.33;
    
    emitterCell.yAcceleration = 100;
    emitterCell.xAcceleration = -200;
    
    emitterCell.velocity = 100;
    emitterCell.velocityRange = 40;

    emitterCell.scaleRange = 0.5;
    emitterCell.scaleSpeed = -0.2;

    emitterCell.emissionRange = M_PI*2;
    
    _emitter.emitterCells = @[emitterCell];
}

-(void)disableEmitterCell
{
    [_emitter setValue:@0 forKeyPath:@"emitterCells.cell.birthRate"];
}

@end

As you can see, the code is almost identical to ExplodeView. The only differences are a longer lifetime for the particles, and values set to xAcceleration and yAcceleration to give acceleration to the particles. This is the way to simulate gravity in your particle systems.

This new particle will emit sparkles for as long as it exists, like a lit fuse on a stick of dynamite.

You’re going to animate this effect to go through the screen behind the solved puzzle!

Switch to GameController.m and add a new import:

#import "StarDustView.h"

Then create the animation at the end of checkForSuccess:

//win animation
TargetView* firstTarget = _targets[0];

int startX = 0;
int endX = kScreenWidth + 300;
int startY = firstTarget.center.y;

This piece of code grabs the very first target on the screen and captures its y-coordinate. You also prepare two variables with the start and end x-coordinates.

Add the next bit of code right after what you just added:

StarDustView* stars = [[StarDustView alloc] initWithFrame:CGRectMake(startX, startY, 10, 10)];
[self.gameView addSubview:stars];
[self.gameView sendSubviewToBack:stars];

This actually creates the effect and adds it to the view, behind everything else.

Add the following after that:

[UIView animateWithDuration:3
                      delay:0
                    options:UIViewAnimationOptionCurveEaseOut
                 animations:^{
                     stars.center = CGPointMake(endX, startY);
                 } completion:^(BOOL finished) {
                 
                     //game finished
                     [stars removeFromSuperview];                     
                 }];

Here you fire off a UIKit animation that moves the effect position through the screen and removes the particle effect from its parent when it’s complete. The following image shows the path it will take:

Build and run the project, and try solving a puzzle. When you do, you’ll see the emitter going through the screen spreading sparkles all over the place. :]

The following screenshot shows it in action, but you really need to see it moving to get the full effect:

an_sparkles

That’s some Sparkle Motion! You can refine the effect further by making it follow different paths, zooming in and out, and so on, as you wish.

Adding a Hint Feature

By now you’ve probably already noticed that if you aren’t trained in anagrams, the game can be quite challenging. Unless, of course, you’ve been cheating and looking up the answers in the Plist! :]

It might be a good idea to implement in-game help for your player so they don’t get stuck and loose interest. What you are going to develop next is a button called Hint, which will move a tile onto a correct target, thus helping the player solve the puzzle.

Begin by adjusting the HUD. In HUDView.h, add one more property to the class:

@property (strong, nonatomic) UIButton* btnHelp;

Then switch to HUDView.m to add the code to create the button on the HUD. At the end of viewWithRect:, but before the return hud; line, add:

//load the button image
UIImage* image = [UIImage imageNamed:@"btn"];

//the help button
hud.btnHelp = [UIButton buttonWithType:UIButtonTypeCustom];
[hud.btnHelp setTitle:@"Hint!" forState:UIControlStateNormal];
hud.btnHelp.titleLabel.font = kFontHUD;
[hud.btnHelp setBackgroundImage:image forState:UIControlStateNormal];
hud.btnHelp.frame = CGRectMake(50, 30, image.size.width, image.size.height);
hud.btnHelp.alpha = 0.8;
[hud addSubview: hud.btnHelp];

This should configure the button nicely! You set the title to “Hint!”, set the custom game font you use for the HUD and also position the button on the left side of the screen. In the end, you add it to the hud view.

Build and run the project, and you should see the button appear onscreen:

That was a little quick gratification! Now you need to connect the button to a method so that it will do something when pressed.

Inside GameController.m, add the following custom implementation of the hud property’s setter method.

//connect the Hint button
-(void)setHud:(HUDView *)hud
{
    _hud = hud;
    [hud.btnHelp addTarget:self action:@selector(actionHint) forControlEvents:UIControlEventTouchUpInside];
}

Have a look at this custom setter (phew, I think I covered a really wide range of tricks). When a HUD instance is set to the property, the game controller can also hook up the HUD’s button to one of its own methods. The game controller just sets the target/selector pair of the hud.btnHelp button to its own method, actionHint. Sleek! :]

You’ll also need to add actionHint to the GameController implementation:

//the user pressed the hint button
-(void)actionHint
{
    NSLog(@"Help!");
}

You’ll replace that with more than just a log statement later. Now when you tap the hint button, you should theoretically see “Help!” appear in the Xcode console. Build and run the project and give the button a try.

Button_why_no_click

Your Hint button does not work for a reason. Remember when you added this line in HUDView’s viewWithRect:?

hud.userInteractionEnabled = NO;

Because HUDView does not handle touches, it also does not forward them to its subviews! Now you have a problem:

  • If you disable touches on the HUD, you can’t connect HUD buttons to methods.
  • If you enable touches on the HUD, the player cannot interact with game elements that are under the HUD layer.

What can you do? Remember the layer hierarchy?

You are now going to use a special technique that is comes in handy on rare occasions, but it’s the way to go in this case – and its the most elegant solution as well.

UIKit lets you dynamically decide for each view whether you want to “swallow” a touch and handle it, or “let it through” to the view underneath.

You’re going to implement a method in HUDView that will, for each touch, decide whether to let it through to the game elements or “swallow” it.

Open up HUDView.m and find the line where you disable user interaction. Change that line to:

hud.userInteractionEnabled = YES;

Now that you will be handling touches in HUDView, you need to decide which ones you want to handle. You’ll implement a custom hitTest:withEvent: for the HUDView class.

hitTest:withEvent: gets called automatically from UIKit whenever a touch falls on the view, and UIKit expects as a result from this method the view that will handle the touch. If the method returns nil, then the touch is forwarded to a view under the current view, just as if userInteraction is disabled.

Add the following to HUDView.m:

-(id)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    //1 let touches through and only catch the ones on buttons
    UIView* hitView = (UIView*)[super hitTest:point withEvent:event];
    
    //2
    if ([hitView isKindOfClass:[UIButton class]]) {
        return hitView;
    }
    
    //3
    return nil;
    
}

The logic behind this method is quite simple:

  1. First you call the super’s implementation to get which view would normally handle the touch.
  2. Then you check whether this view is a button (!), and if it is, then you forward the touch to that button.
  3. If it’s not a button, you return nil, effectively forwarding the touch to the underlaying game elements layer.

With this method in place, you have a working HUD layer. Build and run the project, and you will see that the Hint button reacts to touches AND you can drag the tiles around:

Challenge: For puzzles with small tiles, it’s actually possible to drop a tile under the button, where it will be stuck forever. How would you solve this problem?

OK, going back to GameController.m, you can also quickly implement the hinting function. Here’s the plan: you’ll take away some of the user’s score for using the Hint feature, then you’ll find the first non-matched tile and its matching target, and then you’ll just animate the tile to the target’s position. The end.

Replace the NSLog statement in actionHint with the following:

//1
self.hud.btnHelp.enabled = NO;

//2 
self.data.points -= self.level.pointsPerTile/2;
[self.hud.gamePoints countTo: self.data.points withDuration: 1.5];

This is pretty simple:

  1. You temporarily disable the button so that no hint animations overlap.
  2. Then you subtract the penalty for using a hint from the current score and tell the score label to update its display.

Now add the following to the end of the same method:

//3 find the first target, not matched yet
TargetView* target = nil;
for (TargetView* t in _targets) {
    if (t.isMatched==NO) {
        target = t;
        break;
    }
}

//4 find the first tile, matching the target
TileView* tile = nil;
for (TileView* t in _tiles) {
    if (t.isMatched==NO && [t.letter isEqualToString:target.letter]) {
        tile = t;
        break;
    }
}

To continue:

  1. You loop through the targets and find the first non-matched one and store it in target.
  2. You do the same looping over the tiles, and get the first tile matching the letter on the target.

To wrap up, add this code to the end of the method:

//5
// don't want the tile sliding under other tiles
[self.gameView bringSubviewToFront:tile];

//6
//show the animation to the user
[UIView animateWithDuration:1.5
                      delay:0
                    options:UIViewAnimationOptionCurveEaseOut
                 animations:^{
                     tile.center = target.center;
                 } completion:^(BOOL finished) {
                     //7 adjust view on spot
                     [self placeTile:tile atTarget:target];
                     
                     //8 re-enable the button
                     self.hud.btnHelp.enabled = YES;

                     //9 check for finished game
                     [self checkForSuccess];                     
                 }];

The above code animates the tile to the empty target:

  1. First bring the tile to the front of its parent’s view hierarchy. This ensures that it doesn’t move under any other tiles, which would look weird.
  2. Set the tile’s center to the target’s center, and do so over 1.5 seconds.
  3. Call placeTile:atTarget: to straighten the tile and hide the target.
  4. Enable the button again so the player can use more hints.
  5. Finally, call checkForSuccess in case that was the last tile. No player will use a hint for that, but you still have to check.

And that’s all! You had already implemented most of the functionality, like placing tiles on a target, checking for the end of the game, etc. So you just had to add a few lines and it all came together. :]

Build and run the project and give the Hint button a try!

Eek! Bugs! It was pointed out in the Comments that a bug can occur if the user presses the hint button after the game is over, either during the game over animation or while showing the menu you will build in the next section. (Basically, it repeatedly plays the completion effects and displays menus, and then mayhem ensues.)

I’ve already subtly changed one line of the tutorial to help fix this. (I reversed steps 8 and 9 in the preceding code block.) But to really fix it, you’ll also need to make the following minor edits:

Add the following at the end of setHud::

    hud.btnHelp.enabled = NO;

This just disables the hint button as soon as the HUD is setup, ensuring it starts out disabled.

Add the following inside checkForSuccess, just before the line that reads [self stopStopWatch];:

    self.hud.btnHelp.enabled = NO;

This disables the hint button at the end of the game, but before any effects being to play.

Add the following at the end of dealRandomAnagram:

    self.hud.btnHelp.enabled = YES;

This enables the hint button when a new anagram is displayed.

And that should do it. Sorry for the interruption. Now back to our regularly scheduled tutorial.

What’s On the Menu

The gameplay is complete, but the player can only play Level 1, and cannot choose to solve more difficult anagrams. So your last task for this tutorial is to add a game menu.

Inside ViewController.m, near the top of the file, change the private interface declaration line to look like this:

@interface ViewController () <UIActionSheetDelegate>

This declares that your class conforms to the UIActionSheetDelegate protocol. You need to do this because you’ll be presenting the difficulty choices with a UIActionSheet.

Add the following method in ViewController.m:

#pragma mark - Game manu
//show the level selector menu
-(void)showLevelMenu
{
    UIActionSheet* action = [[UIActionSheet alloc] initWithTitle:@"Play @ difficulty level:"
                                                        delegate:self
                                               cancelButtonTitle:nil
                                          destructiveButtonTitle:nil
                                               otherButtonTitles:@"Easy-peasy", @"Challenge accepted" , @"I'm totally hard-core", nil];
    [action showInView:self.view];
}

The above method simply shows a UIKit action sheet with the three difficulty levels:

  • Easy-peasy
  • Challenge accepted
  • I’m totally hard-core

These levels are loaded from the three .plist level config files.

You would like to show the game menu as soon as the game appears on the screen, so add the following implementation of viewDidAppear: in ViewController.m:

//show tha game menu on app start
-(void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    [self showLevelMenu];
}

This calls showLevelMenu to display the menu to the user.

Now you need to decide what to do when the player selects a level. It’s not hard – just load the .plist file, call dealRandomAnagram, and everything should work by itself!

Give it a try. Add the following method to ViewController.m:

//level was selected, load it up and start the game
-(void)actionSheet:(UIActionSheet *)actionSheet willDismissWithButtonIndex:(NSInteger)buttonIndex
{
    //1 check if the user tapped outside the menu
    if (buttonIndex==-1) {
        [self showLevelMenu];
        return;
    }
    
    //2 map index 0 to level 1, etc...
    int levelNum = buttonIndex+1;
    
    //3 load the level, fire up the game
    self.controller.level = [Level levelWithNum:levelNum];
    [self.controller dealRandomAnagram];
}

This implements one of the methods in the UIActionSheetDelegate protocol.

  1. It checks if the passed buttonIndex equals -1. If so, that means the player tapped outside the menu, in which case, just show the menu again.
  2. Find the level number by adding 1 to the given buttonIndex. You do this because you numbered your level files level1.plist, level2.plist, etc., but buttonIndex, like other indices in Objective-C, starts at 0.
  3. Finally, you create a new Level instance for the selected level, assign it to the game controller and call dealRandomAnagram on the game controller.

Woop, woop!

Build and run the project. You’ll see that you have things a bit mixed up – the game appears with an anagram already loaded, while the player still sees the menu:

To make matters worse, if you choose an item from the menu, it displays it on top of the existing anagram!

an_needs_clearing

To fix the first problem, scroll inside ViewController.m, find these two lines inside viewDidLoad and delete them:

self.controller.level = level1;
[self.controller dealRandomAnagram];

In the same method, but a bit above, also delete this line:

Level* level1 = [Level levelWithNum:1];

These were all test code chunks that you no longer need.

Build and run again. Now the player sees the menu first, and when they choose the difficulty level the game starts. Nice!

But now, when the game is over the player cannot go back to the menu screen. And if they do, we already saw that the new anagram will display right on top of the old one!

You could make the ViewController be a delegate of GameController, so that when the game is over GameController could send a message straight to ViewController. That would be overkill for this situation, however. And since you want to know tricks for flexible game design with UIKit, why don’t you add a block to show the game menu that gets executed upon game completion.

Open up GameController.h and add the following:

//just under the imports at the top and above the interface declaration
typedef void (^CallbackBlock)();

//with other properties
@property (strong, nonatomic) CallbackBlock onAnagramSolved;

You first declare a new block type that does not take arguments or return a result. This makes your property declaration code look cleaner. You then create a property that gives your GameController access to a block of the newly-defined type.

Now switch to GameController.m. First, add the following helper method that you’ll call between levels:

//clear the tiles and targets
-(void)clearBoard
{
    [_tiles removeAllObjects];
    [_targets removeAllObjects];
    
    for (UIView *view in self.gameView.subviews) {
        [view removeFromSuperview];
    }
}

The method removes all tiles and targets from the controller and the view. You’ll use this to fix the problem you saw earlier, where new anagrams are displayed on top of old ones.

Inside checkForSuccess, find the line [stars removeFromSuperview];. This is when the final win animation ends. Just below that line, but still inside the block where it is located, add:

//when animation is finished, show menu
[self clearBoard];
self.onAnagramSolved();

Here you call clearBoard to clean up between levels, and then you call the callback block stored in onAnagramSolved.

And finally, add the end-game code block! Switch for the last time to ViewController.m and at the end of viewDidLoad, add:

__weak ViewController* weakSelf = self;
self.controller.onAnagramSolved = ^(){
    [weakSelf showLevelMenu];
};

Very simple and elegant! You create a weak reference to the ViewController instance, then you set the game controller onAnagramSolved block to call showLevelMenu.

Build and run one last time:

Your finished letter / word game with UIKit!

Congratulations, you have made a complete and polished letter / word game with UIKit!

Where to Go from Here?

Here is a link to the final source code for the tutorial.

You made it! It was a long tutorial, but you learned a ton of cool stuff.

The game is complete, but there are all sorts of ways you could expand it. Here are some ideas off the top of my head:

  • Think about how to make this game (or your own) multi-player via Game Center.
  • You could also simply implement leaderboards via Game Center.
  • Add more explosions, more levels and more anagrams.
  • Implement score tracking for different players. You could load and save scores from a plist or JSON file.

That’s all for today. Thanks for sticking with this tutorial, and do leave a comment!

Marin Todorov

Part of: Realm and raywenderlich.com. Author of books and apps. More: <a href="http://www.underplot.com">www.underplot.com</a>

Other Items of Interest

Save time.
Learn more with our video courses.

raywenderlich.com Weekly

Sign up to receive the latest tutorials from raywenderlich.com each week, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

PragmaConf 2016 Come check out Alt U

Our Books

Our Team

Video Team

... 20 total!

iOS Team

... 78 total!

Android Team

... 26 total!

Unity Team

... 11 total!

Articles Team

... 15 total!

Resident Authors Team

... 18 total!

Podcast Team

... 7 total!

Recruitment Team

... 9 total!