2011年10月17日星期一

Cocos2d Game Tutorial – How To Build a Tower Defense Game for the iPhone – Part 1 – Creep Waves!

Cocos2d Game Tutorial – How To Build a Tower Defense Game for the iPhone – Part 1 – Creep Waves!:
We made it! We’ve talked a little about design and have a fairly good idea of what the finish product will look like when we’re all done. Now we start the task of coding the thing. The first step for any tower defense game is the “creeps”. Creeps are the enemy characters that are invading your tower defense world and that you need to repel. So what are we going to do in this tutorial? Because this is just the first of a few, this is some of the items we’re going to cover:

  • How to make waypoint.
  • How to load up a tile based map and use objects from it instead of hardcoding them
  • How to create Creeps/Bad Guys/ Enemies you name it
  • How to make the move along our predetermined path
  • How to do smooth scrolling on the iPhone

Without those things the game will not be much of a tower defense game. Well, some of the items are just nice goodies, but we want them so we’re getting them in right away! So in this first installment I am going to going to show you how to have a basic enemy follow a predefined path on a tilemap, by following a series of waypoints. By the end of this series, you will have all the information to be well on your way to making the next great tower defense game.



Source code after the break!



To download the source code to this tutorial: Cocos2d iPhone Tower Defense Tutorial Part 1.

A “waypoint” for the uninitiated is defined by wikipedia as a “sets of coordinates that identify a point in physical space”. I like that definition so I will use it. It basically means that our stage will be our physical space and the location of the waypoint will be defined by the it’s x,y coordinate on that stage.

We will create create a series of these waypoints all over the stage and then will make an enemy follow those waypoints to a final destination! Sounds complicated? It’s not. Now, to set the bar a little higher for tower defense games in the future, I’m gonna do this with the help of “Tiled” which can be downloaded here: http://www.mapeditor.org/ and use the power of cocos2d which can be downloaded here: http://www.cocos2d-iphone.org/. Both of these tools will help us make something we can all be proud of and take advantage of some nice iPhones features.

Ok, so most likely you’ve already downloaded the source code and have seen a bunch of classes that we’re using. Most of them are self explanatory, but here is the list and a little description.

  1. TowerDefenseTutorialAppDelegate – Create the Window, load CCDirector and load the first scene (basic stuff).
  2. RootViewController – Extends “UIViewController” so we can easily change orientation.
  3. GameConfig – Some basic variables for orientation at this point.
  4. TutorialScene – Our main work horse class, will load the map and set the creeps on the right path.
  5. DataModel – A simple model interface that stores the main data arrays for easy lookup.
  6. Creep – Our resident bad guys, we have two to start and will add more as time goes on.
  7. Waypoint – Simple class that is used in conjunction with the tiled map editor.
  8. Wave – The class that will control the order of “Creeps” that are released at a time.

That may seem like a lot of classes, but most 1, 2 and 3 are almost the default classes that come with cocos2d and the classes like “Waypoint” and “Wave” are fairly light in what they do at this point. In fact “Waypoint” is basically just an extension of CCNode since all our waypoint needs is an x,y location.

Seriously, you don’t believe me… check out the WayPoint class header and implementation files

Waypoint.h:

#import "cocos2d.h"

@interface WayPoint : CCNode {
}
@end

Waypoint.m:

#import "WayPoint.h"

@implementation WayPoint
- (id) init
{
if ((self = [super init])) {
}
return self;
}
@end

As for the DataModel class, that should be straight forward for anyone who has dealt in NSMutableArrays as well. Lets jump straight into the code:

#import "cocos2d.h"

@interface DataModel : NSObject  {
CCLayer *_gameLayer;
NSMutableArray *_targets;
NSMutableArray *_waypoints;
NSMutableArray *_waves;
UIPanGestureRecognizer *_gestureRecognizer;
}

@property (nonatomic, retain) CCLayer *_gameLayer;

@property (nonatomic, retain) NSMutableArray * _targets;
@property (nonatomic, retain) NSMutableArray * _waypoints;
@property (nonatomic, retain) NSMutableArray * _waves;
@property (nonatomic, retain) UIPanGestureRecognizer *_gestureRecognizer;;
+ (DataModel*)getModel;

@end

So most of this code is very straight forward. DataModel is a singleton class that extends from NSCoding. We do this for 2 reasons – 1) We do this because we’re going to use it to keep track of the objects in the class at some point in a later tutorial to save the current state and 2) We make it a singleton because we don’t need anymore than one DataModel for the game. We access the DataModel class from any class that imports it by calling it like this:

DataModel *m = [DataModel getModel];

Here is the singleton portion of the code for clarity:

+(DataModel*)getModel
{
if (!_sharedContext) {
_sharedContext = [[self alloc] init];
}
return _sharedContext;
}

We’re also keeping arrays to all the major players – “targets” are our creep enemies, “waypoints” are the navigation points that the creeps will follow and “waves” will store the wave classes about the numbers of creeps and how fast they spawn, etc.

What about UIPanGestureRecognizer and CCLayer? Well the CCLayer is a pointer to the actual game layer that all the action will take place on. It’s always good to keep track of that so it can be accessed by other classes. UIPanGestureRecognizer is out ticket to smooth scrolling around the screen and the ability to have a tower defense game that is limited to 480×320. Now we’ll be able to have any sized board. But more of that later…

Now that we’ve allivated your fears about the number of classes and what the code will look at lets look at something a little more verbose. Lets talk about our Bad guys, the creeps! We already know what the “Wave” and “DataModel” classes are, so this shouldn’t look so scary anymore. This is what the code looks like for them:

#import "cocos2d.h"

#import "DataModel.h"
#import "WayPoint.h"

@interface Creep : CCSprite  {
int _curHp;
int _moveDuration;
int _curWaypoint;
}

@property (nonatomic, assign) int hp;
@property (nonatomic, assign) int moveDuration;
@property (nonatomic, assign) int curWaypoint;

- (Creep *) initWithCreep:(Creep *) copyFrom;
- (WayPoint *)getCurrentWaypoint;
- (WayPoint *)getNextWaypoint;

@end

@interface FastRedCreep : Creep {
}
+(id)creep;
@end

@interface StrongGreenCreep : Creep {
}
+(id)creep;
@end

We’ve got a creep with a set of hit-points, a speed and the current waypoint it is traveling too. This is all the information we really need at this point. We’re going to have two types of creeps, because what tower defense game doesn’t have multiple types. A fast red creep and a slow, but strong creep – We could have just as easily added multiple other creeps, but the idea is to keep this simple for now.

Now since I’ve shown the header file, I probably should show you the implementation as well, but to keep this simple, I’m going to just show you the portions that matter to us. First lets looks at how one of our creeps is made:

@implementation FastRedCreep

+ (id)creep {
FastRedCreep *creep = nil;
if ((creep = [[[super alloc] initWithFile:@"Enemy1.png"] autorelease])) {
creep.hp = 10;
creep.moveDuration = 4;
creep.curWaypoint = 0;
}
return creep;
}

This is how we make a one of those little creeps – We just have a static function that we can call like “[FastRedCreep creep]” and it will return one to us that we can just add to the screen and start playing with right away. Since the class Creep itself extends “CCSprite” we get all the benefits of that class with no mess. We could have just as easily had a variable that points to a CCSprite object that would get loaded (and both have there advantages and disadvantages), but we’ll go with this for the tutorial.

Next up in the Creep class we take advantage of the DataModel and WayPoint class to actual give some behavior to our baddies.

- (WayPoint *)getCurrentWaypoint{
DataModel *m = [DataModel getModel];
WayPoint *waypoint = (WayPoint *) [m._waypoints objectAtIndex:self.curWaypoint];
return waypoint;
}

- (WayPoint *)getNextWaypoint{

DataModel *m = [DataModel getModel];
int lastWaypoint = m._waypoints.count;
self.curWaypoint++;
if (self.curWaypoint > lastWaypoint)
self.curWaypoint = lastWaypoint - 1;

WayPoint *waypoint = (WayPoint *) [m._waypoints objectAtIndex:self.curWaypoint];
return waypoint;
}

This is the logic that gets called for the creeps to pull the current waypoint location and to get a new one to follow once they’ve arrived at their destination. You can see the same code “WayPoint *waypoint = (WayPoint *) [m._waypoints objectAtIndex:self.curWaypoint];” which calls the DataModel class to find and return the waypoint at “curWaypoint” which is just an int value stored in the creep class. When we want to get the next location we increment the “curWaypoint” and verify that we haven’t hit the end of the array. Right now when the end of the array is hit we just keep pushing the end value into curWaypoint (meaning it’s not going to move at the end). We could just as simply made the value “0″ and the creep would do a loop! Try it, try changing “self.curWaypoint = lastWaypoint – 1;” to “self.curWaypoint = 0;” in getNextWaypoint and see what happens. This is how you can have creeps repeat the wave if the towers don’t destroy it the first time.

The code to make the creeps move after they’ve been created is located in the TutorialScene class.

-(void)FollowPath:(id)sender {

Creep *creep = (Creep *)sender;

WayPoint * waypoint = [creep getNextWaypoint];

int moveDuration = creep.moveDuration;
id actionMove = [CCMoveTo actionWithDuration:moveDuration position:waypoint.position];
id actionMoveDone = [CCCallFuncN actionWithTarget:self selector:@selector(FollowPath:)];
[creep stopAllActions];
[creep runAction:[CCSequence actions:actionMove, actionMoveDone, nil]];
}

Based on what we talked about before this should look a little obvious, but some of the actual animation code might be a little foreign if you haven’t read my other tutorials. After the creep is created for the first time in the “AddTarget” method – This function gets called repeatedly by itself. It checks the “sender” argument which in all cases should be the creep itself and then gets the next waypoint. Now given that the creep should be at the current waypoint and we want to goto current waypoint + 1 those will be different positions. We then run two actions. First the “MoveTo” action that will move our sprite from one (x,y) position to the destination (x,y) position. Then we call FollowPath again and the cycle goes on once again.

Much of what we’re going to be talking about next involves the “TutorialScene” class code. The header file is fairly bare and relates to using the tmx file based system, so I’m writing it out here:

#import "cocos2d.h"
#import "Creep.h"
#import "WayPoint.h"
#import "Wave.h"

// Tutorial Layer
@interface Tutorial : CCLayer
{
CCTMXTiledMap *_tileMap;
CCTMXLayer *_background;
int _currentLevel;
}

@property (nonatomic, retain) CCTMXTiledMap *tileMap;
@property (nonatomic, retain) CCTMXLayer *background;
@property (nonatomic, assign) int currentLevel;
+ (id) scene;
- (void)addWaypoint;
@end

Now in case you’ve never used tiled before, there are some great tutorials on the internet and the goal here is to not repeat the work of others, but to build on that work. So I recommend Ray Wenderlich’s – Collision and Collectables Tile Based Tutorial which provided the template for the map I used and has helped a ton of people as well as “Tom the Turret” from Ray as well the math portion of the tutorial can be found here.

Side Note: Speaking of credit, there is so many people that have helped along the way that it’s hard to keep track at this point in all our adventures. To that end, I’ll try to give credit where it’s due as these tutorials continue, just like I’ve done in the past.

// on "init" you need to initialize your instance
-(id) init {
if((self = [super init])) {
self.tileMap = [CCTMXTiledMap tiledMapWithTMXFile:@"TileMap.tmx"];
self.background = [_tileMap layerNamed:@"Background"];
self.background.anchorPoint = ccp(0, 0);
[self addChild:_tileMap z:0];

[self addWaypoint];
[self addWaves];

// Call game logic about every second
[self schedule:@selector(update:)];
[self schedule:@selector(gameLogic:) interval:1.0];
self.currentLevel = 0;
//Center the tile layer so we get the best possible starting view
self.position = ccp(-228, -122);
}
return self;
}

We load and store the newly created “CCTMXTiledMap” and add it to the game layer in four steps. We make calls to “addWayPoint” which is listed and explained below and “addWaves” method which just creates and stores two default waves for now.

We then use the scheduler for an update routine and a gameLogic routine which is also described below. Finally we set the current level to “0″ and move the layer on the screen to give us a good angle to see the action.

Well now that we got some of the formalities down, we’re going to actual dig into the meat and potatoes of the code. To do that we need to open up the .tmx file in the resource folder. Remember you can get the program at mapeditor.org

Tiled Tower Defense Map

Ok, back to the work – We cant make this run 60 pages. So what you’ll notice in the picture above is that we’ve also placed objects on the tiled map. Those are the basis for the path that are creeps are going to follow. Since tilded doesn’t take good pictures of objects that are only 1 pixel on the map we can look at the TileMap.tmx file and see what those objects look like:

<objectgroup name="Objects" width="27" height="20">
<object name="Waypoint0" x="887" y="292"/>
<object name="Waypoint1" x="438" y="296"/>
<object name="Waypoint2" x="429" y="22"/>
<object name="Waypoint3" x="22" y="23"/>
<object name="Waypoint4" x="24" y="493"/>
<object name="Waypoint5" x="433" y="497"/>
<object name="Waypoint6" x="437" y="337"/>
<object name="Waypoint7" x="888" y="339"/>
</objectgroup>

Now how does this go from being just a list of coordinates to being something greater? We use a little bit of everything we have talked about so far in a routine we call “addWaypoint”:

-(void)addWaypoint {
DataModel *m = [DataModel getModel];
CCTMXObjectGroup *objects = [self.tileMap objectGroupNamed:@"Objects"];
WayPoint *wp = nil;

int wayPointCounter = 0;
NSMutableDictionary *wayPoint;
while ((wayPoint = [objects objectNamed:[NSString stringWithFormat:@"Waypoint%d", spawnPointCounter]])) {
int x = [[wayPoint valueForKey:@"x"] intValue];
int y = [[wayPoint valueForKey:@"y"] intValue];

wp = [WayPoint node];
wp.position = ccp(x, y);
[m._waypoints addObject:wp];
wayPointCounter++;
}
NSAssert([m._waypoints count] > 0, @"Waypoint objects missing");
wp = nil;
}

We are going to loop through the objects in our TMX file and pull out the data we need! Each object has been conveniently called “Waypoint#” so the order and the loading is trivial. We then create new instances of the WayPoint class and set the position value for each of them before storing them int the _waypoints array in the DataModel class for easy lookup.

Well what about how you load the creeps? Is that as easy? You bet’cha!

-(void)addTarget {

DataModel *m = [DataModel getModel];
Wave * wave = [self getCurrentWave];
if (wave.totalCreeps < 0) {
return; //[self getNextWave];
}
wave.totalCreeps--;

Creep *target = nil;
if ((arc4random() % 2) == 0) {
target = [FastRedCreep creep];
} else {
target = [StrongGreenCreep creep];
} 

WayPoint *waypoint = [target getCurrentWaypoint ];
target.position = waypoint.position;
waypoint = [target getNextWaypoint ];

[self addChild:target z:1];

int moveDuration = target.moveDuration;
id actionMove = [CCMoveTo actionWithDuration:moveDuration position:waypoint.position];
id actionMoveDone = [CCCallFuncN actionWithTarget:self selector:@selector(FollowPath:)];
[target runAction:[CCSequence actions:actionMove, actionMoveDone, nil]];

// Add to targets array
target.tag = 1;
[m._targets addObject:target];
}

When addTarget gets called you see we’ll be getting the current wave to see what creep we’ll need to release (I’ve disabled this a little bit, but I wanted to have it in so you can see what we’re going to do in future tutorials). Right now we still get wave information , but I randomly generate either a “Fast Creep” or a “Strong Creep” and then set the creeps current position based on the first waypoint (you’ll remember that the curWaypoint variable in Creep is “0″ so it will get the position of “Waypoint0″ from the tmx file. Then we add the child to the current game layer and give it an action to move to the next waypoint (this is the same action code as in FollowPath, which was listed above). finally we give it a tag of 1 to represent that it is a creep and add it to the DataModel for easy retrieval.

But what calls add target – I hear you say… well that is also covered by our scheduler.

-(void)gameLogic:(ccTime)dt {

DataModel *m = [DataModel getModel];
Wave * wave = [self getCurrentWave];
static double lastTimeTargetAdded = 0;
double now = [[NSDate date] timeIntervalSince1970];
if(lastTimeTargetAdded == 0 || now - lastTimeTargetAdded >= wave.spawnRate) {
[self addTarget];
lastTimeTargetAdded = now;
}

}

- (void)update:(ccTime)dt {
// Doesn't do anything... for now...
}

So currently “gameLogic” determines when to release a new target based on the wave “spawnRate” with some simple time logic. Our update function while listed because it will be important later on, isn’t being used yet.

Finally, we’ve done a lot so far and covered as much about the tower defense game without listing all the code out. There is still one more thing to cover… UIPanGestureRecognizer

I was thinking of just using the one 480×320 wide screen for the first tutorial, but I really wanted to get this in because I don’t think any tower defense game can function without more room to play with. You could go with the code I’ve attached to this post and make a whole new map and it will still allow you to scroll over the whole area. The code that makes that happen is below.

- (CGPoint)boundLayerPos:(CGPoint)newPos {
CGSize winSize = [CCDirector sharedDirector].winSize;
CGPoint retval = newPos;
retval.x = MIN(retval.x, 0);
retval.x = MAX(retval.x, -_tileMap.contentSize.width+winSize.width);
retval.y = MIN(0, retval.y);
retval.y = MAX(-_tileMap.contentSize.height+winSize.height, retval.y);
return retval;
}

boundLayerPos prevents the viewable layer from moving any farther than its edges. Meaning that if you wanted to increase the tileMap size from 27×20 to 50×50 it would allow it and still work perfectly. I mean you would need to change your waypoints so the creeps don’t just pop on the screen in the middle of the map, but you could do it. This code above is used in the actual work horse for the gesture code below:

- (void)handlePanFrom:(UIPanGestureRecognizer *)recognizer {

if (recognizer.state == UIGestureRecognizerStateBegan) {
//Not used, but included for now
CGPoint touchLocation = [recognizer locationInView:recognizer.view];
touchLocation = [[CCDirector sharedDirector] convertToGL:touchLocation];
touchLocation = [self convertToNodeSpace:touchLocation];               

} else if (recognizer.state == UIGestureRecognizerStateChanged) {
// We have recognized a change in the gesture on the screen.
CGPoint translation = [recognizer translationInView:recognizer.view];
translation = ccp(translation.x, -translation.y);
CGPoint newPos = ccpAdd(self.position, translation);
self.position = [self boundLayerPos:newPos];
[recognizer setTranslation:CGPointZero inView:recognizer.view];   

} else if (recognizer.state == UIGestureRecognizerStateEnded) {
// We have finished the gesture - run a CCMoveTo action based on the velocity of the swipe
float scrollDuration = 0.2;
CGPoint velocity = [recognizer velocityInView:recognizer.view];
CGPoint newPos = ccpAdd(self.position, ccpMult(ccp(velocity.x, velocity.y * -1), scrollDuration));
newPos = [self boundLayerPos:newPos];

[self stopAllActions];
CCMoveTo *moveTo = [CCMoveTo actionWithDuration:scrollDuration position:newPos];
[self runAction:[CCEaseOut actionWithAction:moveTo rate:1]];           

}
}

What this does by the end is lets you know where the gestured swipe started out, ended up and how fast it was. If we didn’t use the “boundLayerPos” you can see that someone could easily do a swipe that would send the layer flying off the screen. We don’t currently use the “UIGestureRecognizerStateBegan” state, but we do use the “UIGestureRecognizerStateChanged” to verify the bounds are maintained and for changes that have occurred in the gesture (say changing directions) and “UIGestureRecognizerStateEnded” where we actually use the gestures velocity and with a cocos2d CCMoveT action we smoothly scroll the layer on the screen.

So what have we learned – Just the biggies

  • How to make waypoint.
  • How to load up a tile based map and use objects from it instead of hardcoding them
  • How to create Creeps/Bad Guys/ Enemies you name it
  • How to make the move along our predetermined path
  • How to do smooth scrolling on the iPhone

What we still have yet to find out –

  • How do we handle rotation so our creep if facing in the direction he/she is moving in
  • What happens when we get to the end of the waypoints!

All these questions will be answered in the upcoming articles and more!

没有评论:

发表评论