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 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:
- Get the full path to the sound file and convert it to a URL by using
[NSURL fileURLWithPath:]
. - Call
[AVAudioPlayer initWithContentsOfURL:error:]
to load a sound file in an audio player. - Set the
numberOfLoops
to zero so that the sound won’t loop at all. CallprepareToPlay
to preload the audio buffer for that sound. - 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.
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.
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:
- Import QuartzCore to access the particle system classes.
-
_emitter
is a convenience variable used to give you access to theUIView
‘s layer property, cast as aCAEmitterLayer*
. -
layerClass
is a class method ofUIView
. You override this method when you want to change the underlying layer of your view class. Here you return theCAEmitterLayer
class and UIKit ensures thatself.layer
returns an instance ofCAEmitterLayer.
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:
- 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. - Load the particle.png image into a
UIImage
instance. There’s another one of thoseNSAssert
statements that crashes the app if it can’t find the file it’s looking for. - Create a new emitter cell by calling the class method
[CAEmitterCell emitterCell]
. Most of the rest of the method is spent configuring this object. - 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. - 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.
- Set the cell’s
birthRate
property to 1000, which tells it to create 1000 particles per second. You also set the cell’slifetime
property to 0.75, which makes each particle exist for 0.75 seconds. - 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 negativeblueSpeed
– this will decrease the blue component of the blending over time, decreasing the intensity of the particle’s color. - 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.
- 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.
- 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.
- Finally, you add the cell you created to the emitter layer.
emitterCells
is an array ofCAEmitterCells
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.
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:
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.