How To Make a Letter / Word Game with UIKit: Part 3/3
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 By Marin Todorov.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
How To Make a Letter / Word Game with UIKit: Part 3/3
50 mins
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.
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:
- First you call the super’s implementation to get which view would normally handle the touch.
- Then you check whether this view is a button (!), and if it is, then you forward the touch to that button.
- 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?
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:
- You temporarily disable the button so that no hint animations overlap.
- 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:
- You loop through the targets and find the first non-matched one and store it in
target
. - 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:
- 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.
- Set the tile’s center to the target’s center, and do so over 1.5 seconds.
- Call
placeTile:atTarget:
to straighten the tile and hide the target. - Enable the button again so the player can use more hints.
- 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::
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];
:
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:
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.
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.
hud.btnHelp.enabled = NO;
self.hud.btnHelp.enabled = NO;
self.hud.btnHelp.enabled = YES;