Lab 2: Asteroids

In this lab, you'll learn about basic memory management and collections by implementing the classic game, Asteroids.

Part 0 — Getting started

  1. Like in the first lab, you'll be modifying a pre-existing project. Download the Asteroids project and open in it Xcode to get started.
  2. Skim the code a little bit to see what's there already. Here's an overview of what each class does:
    ASView
    The main NSView into which everything draws. Maintains an NSMutableArray of drawables. Redraws every 1/60th of a second using a timer.
    ASDrawable
    Something you can draw to the screen. You initialize it with an NSImage. It has a position and a rotation, as well as a velocity.
    ASKeyboard
    Keeps track of which keys are pressed. We'll cover user input later in the semester.
    ASShip
    A subclass of ASDrawable which represents your ship. Fires bullets and moves in response to keyboard input.
    ASBullet
    Another subclass of ASDrawable which represents a bullet. Dies automatically after a set number of frames.
    The most important files to look at are ASDrawable.h and ASView.h.
  3. Build and run the project. You can use the arrow keys to move, and the spacebar to shoot.
  4. Try shooting a few bullets. Notice that the drawable count in the lower right keeps increasing, even though the bullets disappear from the screen. This program has a memory leak.

Part 1 — A leaky ship

  1. The first step in fixing this leak is to find out what is leaking. In this case, it's pretty clear that the bullets are sticking around long after they should be deallocated. The ASShip creates the bullets in its update method. Go to ASShip.m and find this method.
  2. Find the part of the method that creates the bullet (conveniently marked with the comment // create the bullet). Using the three rules of memory management, fix the leak. If you forgot the rules, they are:
    1. If you retain, you must release.
    2. If you alloc, you must release.
    3. If you copy, you must release.
    4. If you didn't do any of those, don't release.
  3. Build and run your project to see if you fixed the leak. There should only be one drawable after the bullets disappear from the screen.

Part 2 — Adding an asteroid

  1. Make a new class called ASAsteroid (using New File… in the File menu). The superclass should be ASDrawable. Make sure to #import "ASDrawable.h".
  2. The first method for ASAsteroid will initialize a new, large asteroid. The method should be called - (id)initLarge. Add this method to ASAsteroid.h.
  3. To implement the method in ASAsteroid.m, follow the standard pattern of initialization:
    1. Call the relevant init method in the superclass using [super initWith...].
    2. Initialize your object's state. You don't have any state to initialize, so you can skip this part.
    3. Return self.
    The init method for the superclass is initWithImage:. You can make a large asteroid image using [NSImage imageNamed:@"asteroidLarge"], which finds an image with that name in the application's bundle.
  4. Let's add one of these large asteroids to the view, so it will show up on the screen. Go to ASView.m and find the - (void)awakeFromNib method (it should be near the top of the file). Create a new ASAsteroid (initializing it using your initLarge method) and add it to the list of drawables using addDrawable:. Look at how the ASShip is added if you aren't sure how to do this.
  5. Add an #import "ASAsteroid.h" to the top of the file, so the compiler doesn't complain.
  6. Build and run. Does the asteroid show up?
  7. You probably noticed that the asteroid is just sitting there at the origin. To make it move, set its xVelocity and yVelocity to some non-zero value using property syntax. Build and run again after doing this.

Part 3 — Death and destruction

  1. Right now, your asteroid is pretty friendly; you can pass through it, and your bullets don't harm it. It's time to change that.
  2. Every frame, the ASView tells each drawable to update. You'll implement collision detection by overriding this method. Add an empty -(void)update method to ASAsteroid.
  3. To check what objects intersect the asteroid, use fast enumeration to iterate through each of them. The NSArray of drawables on the screen is available in self.view.drawables. The loop should look like this:
    for (ASDrawable *drawable in self.view.drawables) { ... }
  4. The asteroid should kill the ship when touched. To check if drawable is a ship, you can use [drawable isKindOfClass:[ASShip class]] (make sure to #import "ASShip.h" so you can do this). If the drawable is a ship, and it collidesWith:self (a method on ASDrawable) then kill the ship using [drawable die].
  5. Build and run to watch the asteroid crush your poor, defenseless ship.
  6. To fight back, your ship's bullets should destroy the asteroid on contact. Add another check to your for loop which tests whether [drawable isKindOfClass:[ASBullet class]] (again remembering to #import "ASBullet.h"). If the bullet collidesWith:self, then both self and drawable (the bullet itself) should die.
  7. Build and run to get your revenge. Make sure the drawable count goes to 1 after you destroy the asteroid.

Part 4 — Breaking in two

  1. In the actual game, asteroids break into smaller pieces when you shoot them. Let's implement this.
  2. Each asteroid will keep a collection of smaller asteroids, which it will add to the view when shot. The natural way to store this collection is as an NSArray. Add an NSArray *smallerAsteroids instance variable to ASAsteroid.
  3. Add two more methods to ASAsteroid, - (id)initMedium and - (id)initSmall. They should be identical to - (id)initLarge except for the image name, which should be @"asteroidMedium" and @"asteroidSmall", respectively.
  4. In - (id)initLarge, initialize smallerAsteroids to be an array of two ASAsteroids, each initialized with initMedium. Similarly, in - (id)initMedium, initialize three ASAsteroids with initSmall. Read the documentation on and use the initWithObjects: method on NSArray to initialize the array. Remember to end the argument list with nil. For example, - (id)initLarge should have a line like this:
    smallerAsteroids = [[NSArray alloc] initWithObjects:asteroid1, asteroid2, nil];
  5. - (id)initSmall should make smallerAsteroids an empty array (which is different from nil). Just [[NSArray alloc] init] will create an empty array.
  6. Create a - (void)dealloc method which releases the smallerAsteroids array, then sends [super dealloc]. Without this method, you would have allocated an NSArray without releaseing it, breaking the second rule of memory management.
  7. Finally, before you send [self die] in your update method, enumerate through the smaller asteroids using fast enumeration, just like you did with self.view.drawables in the same method. Call [self.view addDrawable:smallerAsteroid] for each smallerAsteroid in the array.
  8. To make the smaller asteroids come out in the right place, you'll have to set smallerAsteroid.x = self.x and smallerAsteroid.y = self.y as well. To make the asteroids come out at a random angle, set the velocity with a random component, like this:
    smallerAsteroid.xVelocity = self.xVelocity + (rand() % 7) - 3;
    smallerAsteroid.yVelocity = self.yVelocity + (rand() % 7) - 3;
  9. Build and run to test your changes. Make sure the drawable count goes down to 1 once everything is destroyed. If not, you should search for the leak. Make sure every single time you alloc, there is a release or autorelease somewhere to go with it.

Part 5 — Shields up, weapons online

  1. Now let's add a shield to defend against the asteroids. This shield will be a property of ASShip. Create both an instance variable ASDrawable *shield and a property with the nonatomic and retain attributes. Look at ASView.h for an example of property declaration.
  2. Every time you declare a property, you must add a corresponding @synthesize or @dynamic in your .m file. Add the line @synthesize shield; inside of the @implementation block for ASShip in ASShip.m. This generates setters and getters for the shield property, including proper memory management.
  3. @synthesize won't release properties when your object is deallocated, however. Create a - (void)dealloc method for ASShip which releases shield and sends [super dealloc]. You must do this every time you have a property with the retain attribute.
  4. Now you'll make the shield turn on and off in response to keyboard input. The property shieldPressed on ASKeyboard is YES whenever the shield button is pressed. In the - (void)update method of ASShip, add two if statements:
    1. If self.shield is nil and k.shieldPressed, set self.shield to a newly initialized ASDrawable with the image named @"shield". If you didn't check whether self.shield was nil, you would create a new ASDrawable each frame, which isn't very efficient. Using the setter will automatically retain the new drawable; make sure to release anything you may have allocated with alloc.
    2. If self.shield is not nil, but k.shieldPressed is false, set self.shield to nil. The setter generated by @synthesize will automatically release the instance variable for you.
  5. Also in - (void)update, send [self.shield draw] to draw the shield on your ship. Remember that if self.shield is nil, this statement will do nothing, so a nil check is redundant.
  6. Build and run your project. You should be able to press the "s" key and toggle your shield. The drawable count should return to 1 when you let go of "s".
  7. The shield doesn't actually do its job yet. Making it work is pretty easy, though. Just modify the update method in ASAsteroid.m and check if drawable.shield is not nil before destroying any drawable of class ASShip.
  8. Build and run… but wait! It doesn't compile. "Request for member shield in something not a structure or union," says gcc. The problem is that, as far as the compiler knows, drawable is just an ASDrawable, which doesn't have a shield property. You can either send [drawable shield], which is equivalent (but will give you only a warning), or make an intermediate variable of type ASShip and access the property on that.
  9. Build and run after making your change. Does the shield work? If so, congratulations, you're done with lab 3. Feel free to experiment with adding more features to the game (e.g. explosions, alien ships that shoot at you) if you want.