Cocos2d Game Development Blueprints
上QQ阅读APP看书,第一时间看更新

Extending CCSprite

Dr. Nicholas Fringe's enemies are an army of UFOs controlled by very intelligent extraterrestrial beings trying to wipe out all of mankind. That's why we're going to create them as a separate class that will derive from CCSprite, where we will define its evolved behavior.

Some developers prefer to derive this kind of class from CCNode and include a CCSprite instance as it offers more potential, but for the moment we are going to keep it simple and just extend CCSprite.

First of all, let's create the new class:

  1. Right-click on the Classes group in the project navigator and select New File….
  2. Click on iOS | cocos2d v3.x and choose to create the new file from the CCNode class template.
  3. Type CCSprite in the available field and click on Next.
  4. Call the file as UFO and be sure that the Classes folder is selected before clicking on Create.

Then replace the contents of UFO.h with the following block of code:

#import <Foundation/Foundation.h>
#import "cocos2d.h"

@interface UFO : CCSprite {

}

// Declare property for number of hits
@property (readwrite, nonatomic) int numHits;

// Declare method to init UFOs
-(id) initWithHits:(int)hits;

@end

Replace the contents of UFO.m with these lines:

#import "UFO.h"

@implementation UFO

// Implement initWithHits
-(id) initWithHits:(int)hits{
    
    // Initialize UFO sprite specifying an image
    self = [super initWithImageNamed:@"ufo_green.png"];
    
    if (!self) return(nil);
    
    // Initialize number of hits
    _numHits = hits;
    
    return self;
}
    
@end

We declared a readwrite integer property to keep the control of the number of hits a UFO can receive before exploding, and as you can see, we're taking advantage of the auto-synthesized properties feature that will help us minimize coding.

We also declared a custom init method so we can assign initial values to each instance of the UFO class; in our case, we just want to specify the image to create the UFO object and the initial number of hits. Notice that we are calling the parent's initWithImageNamed method because if we don't, our class won't be properly initialized.

Note

It's important to call a parent's init method when initializing a derived class.

To conclude with the UFO class, you just need to add the corresponding image to the project:

  1. In the project navigator, right-click on the Resources group and select Add Files to "ExplosionsAndUFOs"….
  2. In the Resources file, you will find ufo_green.png, so select it and click on Add.

Now let's put some enemies in the scene so Dr. Fringe can begin saving our planet. First, declare these two private instance variables by adding them in GameScene.m, after CCParticleSystem *_fire;:

    // Declare an array of UFOs
    NSMutableArray *_arrayUFOs;
    
    // Max number of UFOs in scene
    int _numUFOs;

Initialize them by adding the following lines in the init method, just before return self;:

    // Initialize the array of UFOs
    _numUFOs = 3;
    _arrayUFOs = [NSMutableArray arrayWithCapacity:_numUFOs];

We initialized the UFOs array to contain three objects for the moment, but things will get harder for our scientist later.

Import the UFO class to GameScene.m by adding the following line at the top of the class just after #import "GameScene.h":

#import "UFO.h"

The next step is adding the enemies to the scene, and we're going to achieve this by scheduling a spawn method where we will define their behavior. In the init method, add the following line after the lines you added earlier:

    // Schedule the UFOs spawn method
    [self schedule:@selector(spawnUFO) interval:5.0f];

Implement the spawnUFO method by adding the following:

-(void)spawnUFO {
    
    if ([_arrayUFOs count] < _numUFOs){
        // Create a new UFO
        UFO *ufo = [[UFO alloc] initWithHits:3];
    
        // Set inital UFO position
        ufo.position = CGPointMake(ufo.contentSize.width / 2, _screenSize.height + ufo.contentSize.height / 2);
    
        // Adding the new UFO to the array
        [_arrayUFOs addObject:ufo];
    
        // Adding the UFO to the scene
        [self addChild:ufo];
    
        //Creating movement actions
        CCActionMoveTo *actionMoveInitialPosition = [CCActionMoveTo actionWithDuration:0.6 position:CGPointMake(ufo.position.x, _screenSize.height - ufo.contentSize.height / 2)];
    
        CCActionMoveTo *actionMoveRight1 = [CCActionMoveTo actionWithDuration:0.3 position:CGPointMake(_screenSize.width - ufo.contentSize.width / 2, _screenSize.height - ufo.contentSize.height / 2)];
    
        CCActionMoveTo *actionMoveDownLeft = [CCActionMoveTo actionWithDuration:0.3 position:CGPointMake(ufo.contentSize.width / 2, _screenSize.height - 2 * ufo.contentSize.height)];
    
        CCActionMoveTo *actionMoveRight2 = [CCActionMoveTo actionWithDuration:0.6 position:CGPointMake(_screenSize.width - ufo.contentSize.width / 2, _screenSize.height - 2 * ufo.contentSize.height)];
    
        CCActionSequence *ufoSequence = [CCActionSequence actionWithArray:@[actionMoveInitialPosition, actionMoveRight1, actionMoveDownLeft, actionMoveRight2]];
    
        // Repeat movement infinitely
        CCActionRepeatForever *ufoLoop = [CCActionRepeatForever actionWithAction:ufoSequence];
    
        // Run the UFO movement
        [ufo runAction:ufoLoop];
     }
}

I apologize for the big block of code, but don't worry, it's easy to understand what we've just done.

Once we get the array ready to receive objects, we schedule a method that will take care of the UFO spawn. I decided to leave 5 seconds between spawns to give the scientist a chance.

The first thing we do in spawnUFO is to check whether arrayUFOs is already filled with the maximum amount of objects, and if it's not, then we proceed with initializing the object by specifying the number of hits we want it to support before exploding (3 in this case). Then we place UFO off the top of the screen and we add it to both the array and the scene.

As we want the UFO objects to have some intelligence and be harder to kill, I decided to define a loop movement that consists of a four-movement sequence:

  • actionMoveInitialPosition: This traces a path from outside the screen to its initial position, placed at the top-left corner of the screen
  • actionMoveRight1: This moves the enemy to the right side of the screen, keeping the same y value, and performs a lateral displacement
  • actionMoveDownLeft: This places the object on the left side of the screen again but a little lower than the initial position
  • actionMoveRight2: This performs another lateral displacement, keeping the y value

As you can see, the duration of the movements is different because I wanted to make the movement a little unpredictable.

The last line just runs the CCActionRepeatForever action, similar to what we did in the particle effect section.

Now you can run the game and see how the spaceships draw a vertiginous zigzag pattern that is almost impossible to predict (I am joking).

Extending CCSprite

Let's extend the UFO class a little more so we can create instances of this class with a higher number of hits and a different texture image. First of all, add a couple more images for the new types of spaceship:

  1. Right-click on the Resources group and select Add Files to "ExplosionsAndUFOs"….
  2. In the Resources folder, you will find ufo_red.png and ufo_purple.png. Select them and click on Add.

To achieve this, we need to add a new property to UFO.h, so add the following property:

// Declare property for type of UFO
@property (readonly, nonatomic) UFOTypes ufoType;

However, you will need to define UFOTypes too. We will create an enumerated type for the different kinds of spaceships, so add the following lines just above the interface declaration @interface UFO : CCSprite {:

typedef enum {
    
    typeUFOGreen = 0,
    typeUFORed,
    typeUFOPurple
    
} UFOTypes;

Once we've specified the value for the first enumerated component (typeUFOGreen = 0), we don't need to specify the rest as it will follow an enumerated sequence.

Declare a new initializer method with the following line in UFO.h:

// Declare method to init UFOs with type
-(id) initWithType:(UFOTypes)type;

Implement this method in UFO.m with the following lines:

// Implement initWithType
-(id) initWithType:(UFOTypes)type {
    // Set the ufo type
    _ufoType = type;
    
    NSString *textureName;
    int numHits;
    
    switch (_ufoType) {
        case typeUFOGreen:
            // Assign textureName and numHits values
            textureName = @"ufo_green.png";
            numHits = 3;
            break;
        case typeUFORed:
            // Assign textureName and numHits values
            textureName = @"ufo_red.png";
            numHits = 5;
            break;
        case typeUFOPurple:
            // Assign textureName and numHits values
            textureName = @"ufo_purple.png";
            numHits = 7;
            break;
            
        default:
            break;
    }
    
    // Initialize UFO sprite specifying texture image
    self = [super initWithImageNamed:textureName];
    
    if (!self) return(nil);
    
    // Initialize number of hits
    _numHits = numHits;
    
    return self;
}

This method will receive the type as an input argument and depending on it, the method will create one kind of UFO or another. Notice that we're calling the [super initWithImageNamed:] method to properly create our instance.

Then, find the old initialization:

UFO *ufo = [[UFO alloc] initWithHits:3];

Replace it with the new initializer method:

int type = arc4random_uniform(3);
UFO *ufo = [[UFO alloc] initWithType:type];

We are passing a random number between 0 and 2 to create the UFO objects in an unpredictable manner. Come on, run the game and be scared of the almost indestructible spaceships with unknown technology!

Shooting some lasers

Did you think that the backpack-reactor was Dr. Fringe's only invention? You were wrong; he needs some kind of weapon to face the aliens and fortunately, he has developed some high tech lasers. So let's allow him to shoot them when we touch the screen.

First add the required image:

  1. Right-click on the Resources group and select Add Files to "ExplosionsAndUFOs"….
  2. In the Resources folder, you will find laser_green.png, so select it and click on Add.

We're going to declare an array of lasers so we can keep control of them, so add these two private instance variables after int _numUFOs;:

    // Declare array of green lasers
    NSMutableArray *_arrayLaserGreen;
    
    // Max number of lasers in scene
    int _numLaserGreen;

Initialize the variables in the init method by adding the following code before return self;:

    // Initialize the array of green lasers
    _numLaserGreen = 5;
    _arrayLaserGreen = [NSMutableArray arrayWithCapacity:_numLaserGreen];

Dr. Nicholas Fringe's invention is still in the beta phase, that's why he can't shoot more than five laser beams at the same time. We need to enable touch interaction, something that we already know how to do. In the init method, add the following line at the end before return self;:

self.userInteractionEnabled = YES;

Implement touchBegan by adding these lines to GameScene.m:

-(void) touchBegan:(UITouch *)touch withEvent:(UIEvent *)event {
    
    if ([_arrayLaserGreen count] < _numLaserGreen){
        // Create green laser and setting its position
        CCSprite *laserGreen = [CCSprite spriteWithImageNamed:@"laser_green.png"];
        laserGreen.position = CGPointMake(_scientist.position.x + _scientist.contentSize.width / 4, _scientist.position.y + _scientist.contentSize.height / 2);        
        // Add laser to array of lasers
        [_arrayLaserGreen addObject:laserGreen];
        
        // Add the laser to the scene
        [self addChild:laserGreen];
        
        // Declare laser speed
        float laserSpeed = 400.0;
        
        // Calculate laser's final position
        CGPoint nextPosition = CGPointMake(laserGreen.position.x, _screenSize.height + laserGreen.contentSize.height / 2);
        
        // Calculate duration
        float laserDuration = ccpDistance(nextPosition, laserGreen.position) / laserSpeed;
        
        // Move laser sprite out of the screen
        CCActionMoveTo *actionLaserGreen = [CCActionMoveTo actionWithDuration:laserDuration position:nextPosition];
        
        // Action to be executed when the laser reaches its final position
        CCActionCallBlock *callDidMove = [CCActionCallBlock actionWithBlock:^{
            
            // Remove laser from array and scene
            [_arrayLaserGreen removeObject:laserGreen];
            [self removeChild:laserGreen];
            
        }];
        
        CCActionSequence *sequenceLaserGreen = [CCActionSequence actionWithArray:@[actionLaserGreen, callDidMove]];
        
        [laserGreen runAction:sequenceLaserGreen];
    }
}

The very first thing we're doing is checking whether we have shot the maximum number of laser beams. If not, we create a new one, setting its position by the laser gun and adding it to the scene and the array of green lasers.

Each laser will trace a vertical path from its initial shooting position to the top, off the screen. That's why we're calculating the final position as the same x axis value and _screenSize.height + laserGreen.contentSize.height / 2 on the y axis. This is the same approach we take every time we want to place some sprite off the screen, so it's nothing new to us.

As we want the laser to always move at the same speed, we specify its velocity to calculate the duration of the movement and we make this calculation in the same way we did in the previous chapter: divide the distance to be covered by the laser's speed.

Then we create a CCActionMoveTo instance with the calculated direction and the final position. Once the move action ends, we want the laser to disappear from both the scene and the array, so we implement a CCActionCallBlock action where we place this logic.

As the last instruction, we build a sequence with the movement action and the action block and run it. Build and run your game and look what we've just done.

Shooting some lasers

When UFOs collide

Despite being in the beta phase, Dr. Fringe's laser beams can destroy the alien's spaceships, so let's implement collision detection for this purpose.

When a laser beam hits a UFO, on one hand the spaceship's number of hits will decrease for one unit, and on the other hand, the laser will disappear from the scene. The same will happen to the UFO when its number of hits is 0.

First let's declare and implement an instance of the UFO method to check whether its number of hits has reached 0 or not. In UFO.h, add the following line just after the initWithHits method:

// Declare check method
-(BOOL) checkNumHits;

Then in UFO.m, implement checkNumHits by adding these lines at the bottom of the file, before the @end clause:

// Implement checkNumHits
-(BOOL) checkNumHits{
   
    if(_numHits == 0) {
        // Remove UFO from scene and return TRUE
        [self removeFromParent];
        return TRUE;
    }
    
    return FALSE;
}

This method will look at _numHits. If the _numHits instance variable equals 0, and in that case, it will remove the object from its parent (the scene) and return TRUE. For any other _numHits instance variable, it will return FALSE.

Back to GameScene.m. We are going to develop a method to wrap collision detection tasks. In the update method, add the following lines at the end:

    // Collision detection
    [self detectCollisions];

Implement the method by pasting the following lines:

-(void)detectCollisions {
    
     CCSprite *laserGreen;
    
     // For each UFO on the scene
     for(UFO *ufo in _arrayUFOs) {
        
        // For each laser beam shot
        for (laserGreen in _arrayLaserGreen){

            // Detect laserGreen-ufo collision
            if (CGRectIntersectsRect(ufo.boundingBox, laserGreen.boundingBox)) {
                
                CCLOG(@"COLLISION DETECTED");

                // Decrease UFO's number of hits
                ufo.numHits--;
                
                // Check if numHits is 0
                if ([ufo checkNumHits]) {                    
                CCLOG(@"UFO DESTROYED");
 }
            }
        }
}
}

We're iterating the green lasers and the spaceships array and trying to detect a collision between their rectangles. When a collision happens, we decrease the UFO number of hits and we check whether it should be destroyed. In that case, we remove the object from the scene. However, at the moment it won't work properly. If you run the object, you will realize that's because once we destroy the first _numUFOs spaceships, no more are spawned.

This is due to the fact that we aren't removing the destroyed spaceships from the array. You may be thinking that making an object disappear from the scene has a straightforward solution: remove the object from the array and the child from the scene, but it's not that simple.

If you want to try it, add this line after CCLOG(@"UFO DESTROYED");:

[_arrayUFOs removeObject:ufo];

Run the project again and shoot until you destroy one UFO.

There you have it, the app crashed due to an uncaught exception:

Terminating app due to uncaught exception 'NSGenericException', reason: '*** Collection <__NSArrayM: 0x17805b900> was mutated while being enumerated.'
***

If you pay attention to the log message, it makes sense. It's warning us that we're modifying the array while it's being enumerated (iterated) and this is a forbidden action.

So, how can we deal with it? The approach followed by game developers is to create a separate array to store the objects we want to delete and then proceed to remove them as soon as the loop ends.

As we will have the same problem with laser beams if we try to remove them when a collision happens, we will apply a similar approach.

First of all, delete the line to remove the spaceship from arrayUFOs and declare two new arrays after int _numLaserGreen;:

    // Array of removable laser beams
    NSMutableArray *_lasersGreenToRemove;
    
    // Array of removable UFOs
    NSMutableArray *_ufosToRemove;

Initialize them at the end of the init method, just after self.userInteractionEnabled = YES;:

    // Initialize removable objects arrays
    _lasersGreenToRemove = [NSMutableArray array];
    _ufosToRemove = [NSMutableArray array];

Then, in detectCollisions, find the following line:

    CCLOG(@"COLLISION DETECTED");

Replace it with:

    // Stopping laser beam actions
    [laserGreen stopAllActions];
                
    // Adding the object to the removable objects array
    [_lasersGreenToRemove addObject:laserGreen];
                
    // Remove the laser from the scene
    [self removeChild:laserGreen];

Then find the line:

 CCLOG(@"UFO DESTROYED");

Replace it with the following ones:

    // Stopping ufo actions
    [ufo stopAllActions];
                    
    // Adding the object to the removable objects array
    [_ufosToRemove addObject:ufo];

Finally, add these lines after the _arrayLaserGreen loop:

  for (CCSprite *laserGreen in _arrayLaserGreen){
 .
 .
 .
  }
  // Remove objects from array
  [_arrayLaserGreen removeObjectsInArray:_lasersGreenToRemove];

Add this one after the _arrayUFOs loop:

 for(UFO *ufo in _arrayUFOs) {
  .
  .
  .
  }
  // Remove objects from array 
  [_arrayUFOs removeObjectsInArray:_ufosToRemove];

Remember that we aren't removing UFOs from the scene in this method because we're already performing this task inside checkNumHits.

If you run the game now, you will find that each spaceship's spawn/destruction and the laser beam's shooting and disappearing functions are working correctly.