2011年12月22日星期四

Build a Caterpillar Game with Cocos2D: Movement and Missiles

Build a Caterpillar Game with Cocos2D: Movement and Missiles:
This entry is part 5 of 5 in the series Build a Caterpillar Game with Cocos2D
This is the fifth installment of our Cocos2D tutorial series on cloning Centipede for iOS. Make sure you have completed the previous parts before beginning.





Last Time…


In the last tutorial, we discussed the AI of the caterpillar. You learned how to move the caterpillar through the sprout field as well as the bounds of the level.

In today’s tutorial, we will be jumping all over the place as we discuss the player movement/interaction and firing missiles.



Step 1: Controlling The Player


Controlling the player is actually quite simple. The first thing we need to do is tell our game layer to swallow touches in order to interact with it. Add the following code to your init method in GameLayer.m:

[[CCTouchDispatcher sharedDispatcher] addTargetedDelegate:self priority:0 swallowsTouches:YES];

As you saw in the first tutorial, this line of code will simply allow GameLayer to be a touch delegate allowing it to receive notifications of touches. Now, there are two methods that we need to implement. Add the following methods to GameLayer.m

// 1
- (BOOL)ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event {
return YES;
}

// 2
- (void)ccTouchMoved:(UITouch *)touch withEvent:(UIEvent *)event {
// 3
CGPoint touchLocation = [self convertTouchToNodeSpace:touch];

CGPoint oldTouchLocation = [touch previousLocationInView:touch.view];
oldTouchLocation = [[CCDirector sharedDirector] convertToGL:oldTouchLocation];
oldTouchLocation = [self convertToNodeSpace:oldTouchLocation];

// 4
int xChange = touchLocation.x - oldTouchLocation.x;
int yChange = touchLocation.y - oldTouchLocation.y;

int newX = self.player.position.x + xChange;
int newY = self.player.position.y + yChange;

// 5
if(newX < kGameAreaStartX + kGameAreaWidth - kGridCellSize &&
newX > kGameAreaStartX &&
newY > kGameAreaStartY + kGridCellSize / 2 &&
newY < kGameAreaStartY + (kGridCellSize * 3)) {

__block BOOL collide = NO;
CGPoint oldPosition = self.player.position;
// 6
self.player.position = ccp(newX,newY);

// 7
[self.sprouts enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
Sprout *sprout = (Sprout *)obj;
CGRect sproutRect = [sprout getBounds];
CGRect playerRect = [self.player getBounds];

if(CGRectIntersectsRect(sproutRect, playerRect)) {
collide = YES;
*stop = YES;
}

}];

// 8
if(collide) {
self.player.position = oldPosition;
}
}
}

  1. We first need to implement the ccTouchesBegan method to let the caller know that we are responding to touches. If you omit this, your game will crash.
  2. Next, is the ccTouchesMoved method which gets called when the player drags their finger on the device.
  3. Get a reference to the coordinates of the current touch location and the coordinates of the previous touch location.
  4. Get the change from the old location to the new one. We will use this change to determine how far the player should move.
  5. Here we have a series of checks to ensure that the player stays within the “player area” or the virtual bounding box that we set up for them. If not, we don’t execute the code in #7 that actually moves the player.
  6. Update the player’s position.
  7. Here we are checking to see if the player collides with any of the sprouts. Sprouts pose as obstacles for the player and we want to restrict the player’s movement if they are in his way.
  8. Finally, if there is a collision, revert the player’s position to their last good position.

Now if you run the game, you should be able to drag anywhere on your game area and move the player within the confined space.



Step 2: The Missile Object


The missile object is what allows the player and the other game objects to interact. The player will fire a constant stream of missiles with the speed and frequency varying on the current level.

Before we begin creating the Missile, we need to establish a few constants that will be used. Open GameConfig.h and add the following lines:

#define kMissileSpeed 1.0
#define kMissileMaxSpeed 10.0
#define kMissilesTotal 20
#define kMissileFrequency .6 //seconds
#define kMinMissileFrequency .2

I will explain each of these as they come up in the code that follows. Now, create a new GameObject subclass called Missile. Add the following code to Missile.h:

#import "cocos2d.h"
#import "GameObject.h"

@interface Missile : GameObject
@property (nonatomic, assign) BOOL dirty;
- (void)update:(ccTime)dt;
@end

The dirty property will be used in the future to denote that this missile is no longer in play. This could be because of it going off screen or colliding with another game object. Since the missiles are constantly moving, they will need an update method to animate them.

Now, open up Missile.m and add the following code:

#import "Missile.h"
#import "GameLayer.h"
#import "GameConfig.h"

@implementation Missile

@synthesize dirty = _dirty;

// 1
- (id)initWithGameLayer:(GameLayer *)layer {

if(self == [super initWithGameLayer:layer]) {
self.sprite = [CCSprite spriteWithSpriteFrameName:@"missile.png"];
}

return self;
}

- (void)update:(ccTime)dt {
// 2
int inc = kMissileSpeed * (self.gameLayer.level + 1.5);

// 3
if(inc > kMissileMaxSpeed) {
inc = kMissileMaxSpeed;
}

// 4
int y = self.position.y + inc;
self.position = ccp(self.position.x,y);

// 5
if(self.position.y > kGameAreaStartY + kGameAreaHeight) {
self.dirty = YES;
}
}

@end

  1. Our init method looks very familiar and is only responsible for creating the Missile’s sprite.
  2. This is how fast the missile will be moving. It starts with a base value and speeds up based on the current level.
  3. At some point, the speed of our missile might get out of control. We prevent this by adding a max speed.
  4. These two lines are what actually move the missile forward. We calculate a new y for the missile and update the position.
  5. Finally, if the missile collides with the top, set it’s _dirty property to true. We will garbage collect the dirty missiles inside of GameLayer.



Step 3: Firing The Missiles


Now that we have some missiles to fire, we need to fire them off from the player. Generally, when you have a large amount of objects such as missiles, you want to reuse as many as possible and not allocate new ones while the main loop is running. To solve this issue, we will create two arrays. One array will hold all of the missiles that are currently in play (ie fired by the player) and the other will hold the pool of missiles that have yet to be fired. At the end of the update method, we will clean up all of the dirty missiles and move them from the “in play” array to the “pool” array.

Add the following properties to your GameLayer.h file:

@property (nonatomic, retain) NSMutableArray *missilesWaiting;
@property (nonatomic, retain) NSMutableArray *missilesFiring;

Now open GameLayer.m, import Missile.h, and add the following lines in to your init method:

// 1
_missilesWaiting = [[NSMutableArray alloc] initWithCapacity:kMissilesTotal];
_missilesFiring = [[NSMutableArray alloc] initWithCapacity:kMissilesTotal];
// 2
for(int x = 0; x < kMissilesTotal; x++) {
Missile *missile = [[Missile alloc] initWithGameLayer:self];
[self.missilesWaiting addObject:missile];
[missile release];
}

  1. Initialize each of the missile arrays
  2. Loop kMissilesTotal times and create that many Missile objects. Once they are created, we add them to the missilesWaiting array.

Next, jump down to the update method and add the following code:

// 1
static float missleFireCount = 0;

- (void)update:(ccTime)dt {

// ...Caterpillar code...

// 2
float frequency = kMinMissileFrequency;
if(kMissileFrequency / (self.level * 1.25) > kMinMissileFrequency) {
frequency = kMissileFrequency / self.level;
}

// 3
if(missleFireCount < frequency) {
missleFireCount += dt;
} else {
missleFireCount = 0;
// 4
if([self.missilesWaiting count] > 0) {
Missile *missile = [self.missilesWaiting objectAtIndex:0];
[self.missilesFiring addObject:missile];
[self.missilesWaiting removeObjectAtIndex:0];
missile.position = self.player.position;
[self.spritesBatchNode addChild:missile.sprite];

}
}

// 5
__block Missile *dirty = nil;
[self.missilesFiring enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
Missile *missile = (Missile *)obj;
[missile update:dt];
if(missile.dirty) {
dirty = missile;
*stop = YES;
}
}];

// 6
if(dirty) {
dirty.dirty = NO;
[self.missilesWaiting addObject:dirty];
[self.missilesFiring removeObject:dirty];
[self.spritesBatchNode removeChild:dirty.sprite cleanup:NO];
}
}

  1. We need to create a static counter to help facilitate the frequency at which the missiles are fired.
  2. Calculates the missile fire frequency. As the level increases, so does the frequency.
  3. We only want to release a new missile if the current frequency is greater than or equal to the specified frequency for the level
  4. Pulls a missile out of the waiting array (if there are any in there) and adds it to the firing array. It’s also at this stage that we add the missile’s sprite to the batch node to be drawn.
  5. Enumerates all of the missiles checking for dirty one’s. If we find one, remember which one it is so we can move it back to the waiting array.
  6. If there was a dirty missile, move it from the waiting to the firing array and remove it’s sprite from the batch node.

This is all of the code needed to animate the missiles. Go ahead and run the game at this point and watch the missiles fire from the player as you move around.

Missiles



Conclusion


Now that we have enabled some player and missile animations, it’s really starting to feel like a game. We also did some basic collision detection between the player object and the sprout object. In the next tutorial in the series, we will dig deeper into collision detection as we establish collisions between the missiles and the sprouts, missiles and the centipede, and player and the centipede.

Happy coding!

没有评论:

发表评论