Throughout this three part tutorial we have been building a space game for the iPad. In the first two instalments we created the application, added the drawing of the spaceship, added flight and firing controls and hooked them up so that the ship could move and fire it’s laser. Now it’s time to add some asteroids to make the game really interesting.
In this final phase of the tutorial you will see some more advanced drawing including linear gradient fills, as well as the ability to dynamically and remove views from the display. We will also cover how to build simple collision detection code to compute whether the laser intercepts the asteroids.
The first step, is to create the AsteroidView class.
Creating The Asteroid View
You build the AsteroidView class the same way as the CircleControlView class from the second part of the tutorial. Once the wizard has completed you should see something like Figure 1 in the list of project files.
To produce the look of the asteroid we are going to draw a circle, but at various points do a random indent that will give it that craggy look. We set the number of indents at 20, but you can change that to whatever you think gives the best asteroid look.
From here we need to modify the AsteroidView.h
file to add the list of indents as well as the starting degree of the rotation. We also need to have a gradient reference variable that will hold the gradient fill we will use for the asteroid.
The first version of the AsteroidView.h
is shown in Listing 1.
Listing 1. AsteroidView.h
#import <UIKit/UIKit.h>
@interface AsteroidView : UIView {
NSArray *indents;
float startDegree;
CGGradientRef gradient;
}
@end
Now we need to implement the AsteroidView class in the AsteroidView.m
file which is shown in Listing 2.
Listing 2. AsteroidView.m
#import "AsteroidView.h"
@implementation AsteroidView
- (void)setupIndents {
NSMutableArray *indentList = [[[NSMutableArray alloc] init] autorelease];
for( int i = 0; i < 20; i++ ) {
float indent = ( ( (float)rand()/(float)RAND_MAX ) * 0.6 ) + 0.4;
[indentList addObject:[NSNumber numberWithFloat:indent]];
}
indents = [[NSArray arrayWithArray:indentList] retain];
startDegree = 0;
CGFloat locations[2] = { 0.0, 1.0 };
CGFloat components[8] = { 1.0, 0.9, 0.9, 1.0, 1.0, 0.0, 0.0, 1.0 };
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
gradient = CGGradientCreateWithColorComponents (colorSpace, components,
locations, 2);
self.opaque = FALSE;
[self setBackgroundColor:[[UIColor alloc] initWithRed:0 green:0 blue:0 alpha:0.0]];
CGColorSpaceRelease(colorSpace);
}
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self setupIndents];
}
return self;
}
- (void)drawRect:(CGRect)rect {
if ( indents == NULL ) {
[self setupIndents];
}
CGPoint center;
center.x = self.bounds.size.width / 2;
center.y = self.bounds.size.height / 2;
float degree = startDegree;
float indentDegree = 360.0 / (float)[indents count];
float radius = (float)( self.bounds.size.width / 2 ) * 0.9;
BOOL firstPointDrawn = NO;
CGPoint firstPoint;
CGMutablePathRef path = CGPathCreateMutable();
for( NSNumber *number in indents ) {
CGPoint newPoint;
newPoint.x = center.x + ( ( radius * [number floatValue] ) * sin( degree * 0.0174532925 ) );
newPoint.y = center.y + ( ( radius * [number floatValue] ) * cos( degree * 0.0174532925 ) );
if ( firstPointDrawn == NO ) {
CGPathMoveToPoint(path, NULL, newPoint.x, newPoint.y);
firstPoint.x = newPoint.x;
firstPoint.y = newPoint.y;
} else {
CGPathAddLineToPoint(path, NULL, newPoint.x, newPoint.y);
}
degree += indentDegree;
if ( degree > 360.0 )
degree -= 360.0;
firstPointDrawn = YES;
}
CGPathAddLineToPoint(path, NULL, firstPoint.x, firstPoint.y);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextAddPath(context, path);
CGContextClosePath(context);
CGContextClip(context);
CGContextDrawLinearGradient(context, gradient,
CGPointMake(self.bounds.size.width/2,0),
CGPointMake(self.bounds.size.width/2,self.bounds.size.height),
kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation);
CFRelease(path);
}
- (void)dealloc {
[super dealloc];
CGGradientRelease(gradient);
}
@end
The interesting work in this class is in two methods, the first is the setupIndents
method which creates the list of indents and the second is the drawRect
that does the drawing.
The setupIndents
method creates an NSMutableArray object which is an array to which you can dynamically add elements. The method then fills this array with a set of twenty random indent values. It then creates the indentList
array for the object by initializing an NSArray with the contents of the NSMutableArray.
The next thing setupIndents
does is create a gradient that will be used fill the asteroid. This gradient is used in the drawRect
method. The drawRect
method starts by creating a clipping path using the indent list. It then uses this clipping path on a rectangular drawing where the fill is the gradient fill. We have to go through that trouble because there is no gradient fill function for a polygon. So you need to set up the clipping as the polygon then draw a filled rectangle into it.
Testing The Asteroid View
Now that we have the first version of the code for the asteroid it’s time to test it. The simplest way is to go back to the XIB file and add an AsteroidView to the GameSurface view. You can see the Indentity Inspector in Figure 2 with the AsteroidView specified.
You need to ensure that this is a child of the Game Surface as shown in Figure 3.
This is an important part of the architecture of the asteroid handling system. All of the asteroids will be children of the game surface. The game surface creates asteroids by adding child views of type AsteroidView, watches for collisions between the laser it renders and it’s children, destroy the child view if a collision is detected, and generates new asteroids once all of the children are destroyed.
Once we are sure the AsteroidView is in the right place it needs to have it’s attributes set properly as shown in Figure 4.
The view needs to be set to Redraw mode with a Clear Color Background. It also needs to have user interaction and multiple touch disabled.
You can size it whatever you like but Figure 5 shows some reasonable settings to test the view.
With all of the attributes set now it’s time to launch the application and see our asteroid in all of it’s gradient fill glory. It should look something like Figure 6.
Of course, the set of indents is random so your results will vary.
Once you are sure the asteroid is rendering properly delete it from the Game Surface as shown in Figure 7.
After deleting the asteroid we can now add the code in the GameSurface view that will add them dynamically.
Adding Asteroids Dynamically
Listing 3 shows the asteroidsAdded boolean needs to be added to the GameSurface definition.
Listing 3. Adding asteroids to GameSurface.h
#import <UIKit/UIKit.h>
@interface GameSurface : UIView {
...
BOOL asteroidsAdded;
}
...
@end
Then we need a bunch of new methods on GameSurface.m
as shown in Listing 4.
Listing 4. Adding asteroids to GameSurface.m
#import "AsteroidView.h"
...
- (void)buildAsteroid {
CGRect safeZone = CGRectMake((self.bounds.size.width/2)-50,
(self.bounds.size.height/2)-50, 100, 100);
CGRect aFrame;
while( true ) {
aFrame.size.width = aFrame.size.height = ( ( (float)rand() / (float)RAND_MAX ) * 100.0 ) + 50;
aFrame.origin.x = ( (float)rand() / (float)RAND_MAX ) * ( self.bounds.size.width - aFrame.size.width );
aFrame.origin.y = ( (float)rand() / (float)RAND_MAX ) * ( self.bounds.size.height - aFrame.size.height );
if ( CGRectIntersectsRect(aFrame, safeZone) == FALSE )
break;
}
AsteroidView *av = [[AsteroidView alloc] initWithFrame:aFrame];
[self addSubview:av];
asteroidsAdded = YES;
}
- (void)createAsteroids {
for( int i = 0; i < 20; i++ ) {
[self buildAsteroid];
}
}
- (void)drawRect:(CGRect)rect
{
if ( asteroidsAdded == NO ) {
[self createAsteroids];
}
...
}
An important first step is to add the import at the type of the file to bring in the AsteroidView definition. From there we add two new methods, the first is buildAsteroid
and the second is createAsteroids
. The buildAsteroid
method creates a single asteroid and adds it as a child of the view. The createAsteroid
function just invokes buildAsteroids
twenty times to create a cluttered asteroid field.
The buildAsteroid
method is interesting in that it uses the CGRectIntersectsRect
function to ensure that the location it came up with for the asteroid is not too close to the center point of the screen. This ensures that on startup the ship is free from asteroids.
The drawRect
method is augmented to add an invocation of createAsteroids
to build the initial asteroid field.
If this works the result should look something like Figure 8.
Now it’s fine to have an asteroid field, but ideally it needs to move around so that it’s really menacing. So the next step is to animate the asteroids.
Animating The Asteroids
We want the asteroids to both move and spin. The spinning part will be handled by slowly adjusting the startDegree
value in the object. The location will be changed by the game surface using a direction and speed attribute associated with the asteroid. The updates for the AsteroidView.h
file are shown in Listing 5.
Listing 5. Adding cycling and location to the AsteroidView.h
#import <UIKit/UIKit.h>
@interface AsteroidView : UIView {
NSArray *indents;
float startDegree;
CGGradientRef gradient;
float direction;
float speed;
}
- (void)cycle;
@property (readwrite,assign) float direction;
@property (readwrite,assign) float speed;
@end
The cycle method will be used to spin the asteroid. And the @property
keywords indicate that the direction and speed values are properties of the object that can be set using dot notation (e.g. asteroid1.direction = 50).
The updates to AsteroidView.m are shown in Listing 6.
Listing 6. Adding cycling and location to the AsteroidView.m
#import "AsteroidView.h"
@implementation AsteroidView
@synthesize direction, speed;
...
- (void)cycle {
startDegree += 0.2;
if ( startDegree > 360 )
startDegree -= 360.0;
[self setNeedsDisplay];
}
@end
The @synthesize
keyword is an easy way of auto-generating accessors for the direction and speed properties. The new cycle method, which simply adjusts the startDegree
and refreshes the display is also shown.
The game surface also needs to be updated with a new cycleAsteroids method as shown in Listing 7.
Listing 7. Adding asteroid cycling to the GameSurface.h
#import <UIKit/UIKit.h>
@interface GameSurface : UIView {
...
}
...
- (void)cycleAsteroids;
@end
The new cycleAsteroids
method, and an update to buildAsteroid
, is shown in Listing 8.
Listing 8. Adding asteroid cycling to GameSurface.m
- (void)buildAsteroid {
...
AsteroidView *av = [[AsteroidView alloc] initWithFrame:aFrame];
av.direction = ( (float)rand() / (float)RAND_MAX ) * 360.0;
av.speed = ( ( (float)rand() / (float)RAND_MAX ) * 1.0 ) + 0.2;
[self addSubview:av];
asteroidsAdded = YES;
}
- (void)cycleAsteroids {
for( AsteroidView *v in [self subviews] ) {
if ( v ) {
CGPoint vorigin;
vorigin.x = v.frame.origin.x + ( v.speed * sin( v.direction * 0.0174532925 ) );
vorigin.y = v.frame.origin.y + ( v.speed * cos( v.direction * 0.0174532925 ) );
if ( vorigin.x < 0 || vorigin.y < 0 ||
vorigin.x + v.bounds.size.width > self.bounds.size.width ||
vorigin.y + v.bounds.size.height > self.bounds.size.height ) {
v.direction = v.direction + ( ( ( (float)rand() / (float)RAND_MAX ) * 180.0 ) + 180.0 );
if ( v.direction > 360.0 )
v.direction -= 360.0;
}
else {
[v setFrame:CGRectMake(vorigin.x, vorigin.y, v.bounds.size.width, v.bounds.size.height)];
}
[v cycle];
}
}
}
The buildAsteroid
method needs to be updated to add random values for the speed and direction. The new cycleAsteroid
method needs to first move the asteroid, and reset it’s speed and direction if it hits a wall, then send the asteroid the cycle method to get it to spin.
The final modification is to the PadsteroidsViewController.m
to get it to call the cycleAsteroids
method as shown in Listing 9.
Listing 9. Invoking cycleAsteroids from the PadsteroidsViewController.m
- (void)gameUpdate:(NSTimer*)theTimer {
[gameSurface cycleAsteroids];
...
}
When all this is working you should see something like Figure 9.
This is great, but right now the laser just passes underneath the asteroids as you can see in Figure 9. The next step is to figure out when the laser passes through an asteroid and to destroy it.
Adding Collision Detection
The lineIntersects
method for GameSurface.m
uses some trig to figure out of the vector from the ship passes through the rectangle of the asteroid. The method is shown in Listing 10.
Listing 10. Adding laser collision detection to GameSurface.m
-(BOOL)lineIntersects:(CGRect)rect start:(CGPoint)start end:(CGPoint)end {
double minX = start.x;
double maxX = end.x;
if(start.x > end.x) {
minX = end.x;
maxX = start.x;
}
if(maxX > rect.origin.x+rect.size.width)
maxX = rect.origin.x+rect.size.width;
if(minX < rect.origin.x)
minX = rect.origin.x;
if(minX > maxX)
return NO;
double minY = start.y;
double maxY = end.y;
double dx = end.x - start.x;
if(abs(dx) > 0.0000001) {
double a = (end.y - start.y) / dx;
double b = start.y - a * start.x;
minY = a * minX + b;
maxY = a * maxX + b;
}
if(minY > maxY) {
double tmp = maxY;
maxY = minY;
minY = tmp;
}
if(maxY > rect.origin.y + rect.size.height)
maxY = rect.origin.y + rect.size.height;
if(minY < rect.origin.y)
minY = rect.origin.y;
if(minY > maxY)
return NO;
return YES;
}
- (void)enableGun:(float)distance angle:(float)angle {
gunEnabled = YES;
gunDirection = angle;
CGPoint laserStart, laserEnd;
laserStart.x = ( self.bounds.size.width / 2 ) + shipLocation.x;
laserStart.y = ( self.bounds.size.height / 2 ) + shipLocation.y;
laserEnd.x = laserStart.x + ( 1000.0 * cos((gunDirection+90.0) * 0.0174532925) ) * -1;
laserEnd.y = laserStart.y + ( 1000.0 * sin((gunDirection+90.0) * 0.0174532925) ) * -1;
for( AsteroidView *v in [self subviews] ) {
if ( [self lineIntersects:v.frame start:laserStart end:laserEnd] ) {
[v removeFromSuperview];
}
}
[self setNeedsDisplay];
}
- (void)disableGun {
gunEnabled = NO;
if ( [[self subviews] count] == 0 ) {
[self createAsteroids];
}
[self setNeedsDisplay];
}
The enableGun
and disableGun
methods are now upgraded with code tests for hits and in the case of disbleGun
creates new asteroids if they have all been destroyed. The collision code in enableGun
iterates through all of the children of the view and uses the lineIntersects
method to check whether the vector of the laser intersects with the frame of the asteroid view.
As you can see in Figure 10 you can now fire the laser around in a sweep and destroy asteroids.
Here I’ve swept the bottom of the screen with the laser to destroy all of the asteroids on the bottom half.
Conclusion
This isn’t a complete game. There aren’t multiple levels, there’s no scoring, there is no way even to lose. But the all of the rudimentary tools are there to build whatever you like. And you can use the skills you have learned here; building an application, using the Interface Builder, creating custom views, fielding user interaction events, doing sophisticated drawing, and doing collision detection.
Feel free to use all of the code from this example in your own application. And please let me know if you build anything interesting. I’ll certainly download it and have a go.
Jack Herrington is an engineer, author, and presenter who lives and works in the San Francisco Bay Area. He lives with his wife, daughter and two adopted dogs. When he's not writing software, books, or articles you can find him cycling, running, or in the pool training for triathlons. You can keep up with Jack's work and his writing at http://jackherrington.com.