2011年11月10日星期四

Cocos2d Sprite Tutorial Part 3

Cocos2d Sprite Tutorial Part 3:
So in Part 2 we introduced an animated dragon that had 8 directions of movement and was completely controllable via touching the screen. Isn’t cocos2d awesome! Well today we’re going to do a little more, today we’re going to create some villagers – tons of them in fact. We’ll use the information we’ve gotten so far including loading sprites from a sprite sheet and setting up animations.

Source Code after the break!



Source code to this tutorials can be downloaded here: Sprite Tutorial Part 3 Source Code

So what are we building – Lets take a quick look at what our finished product is going to look like:



That’s a crap load of villagers and still at 60 fps because of the optimizations we’ve done in part 2 of the tutorial. So how does it all work? Well lets get right into it shall we.

We’re going to start by creating our adventurer class – This will store the CCSprite instance as well as our moving and walking animations. We’ll be creating a new Adventurer instance for each and every little character on the screen.

Adventurer.h

#import "cocos2d.h"

@interface Adventurer : CCNode {
CCSprite *_charSprite;
CCAction *_walkAction;
CCAction *_moveAction;
BOOL _moving;
}

@property (nonatomic, retain) CCSprite *charSprite;
@property (nonatomic, retain) CCAction *walkAction;
@property (nonatomic, retain) CCAction *moveAction;

@end

We could have subclass’d CCSprite if we wanted and then we could call “initWithFile” directly instead of having to initialize the Adventure class first, however, I feel like it’s better to get into the habit of keeping your sprite instances separate.

Adventurer.m

#import "Adventurer.h"

@implementation Adventurer

@synthesize charSprite = _charSprite;
@synthesize moveAction = _moveAction;
@synthesize walkAction = _walkAction;

-(id) init{
self = [super init];
if (!self) {
return nil;
}

return self;
}

- (void) dealloc
{
self.charSprite = nil;
self.walkAction = nil;
self.moveAction = nil;
[super dealloc];
}

@end

Basic initializer and we’ve included a dealloc function that will be called if and when we kill off the adventurer class instance – All of the things we’ve seen before. This is a very good example of a basic class that you can easily work with. Always make sure if you “retain” and object you have some game plan to release the memory later on. It’s easy to get sloppy, so do you best to resist the urge and sooner or later you won’t know any other way.

Now that we have the characters, lets use them in some way… Time to go back to our “PlayLayer.h” that we’ve seen in part 1 and 2 and make some changes…

#import "cocos2d.h"

#import "SceneManager.h"
#import "Adventurer.h"

@interface PlayLayer : CCLayer {
CCTexture2D *_texture;
CCSpriteSheet *_spriteSheet;

NSMutableArray *_charArray;
}

@property (nonatomic, assign) CCTexture2D *texture;
@property (nonatomic, assign) CCSpriteSheet *spriteSheet;

@property (nonatomic, retain) NSMutableArray *charArray;

@end

Well we’ve added the “Adventurer.h” import and now have 3 class variables now that we want to keep track of. The first is the “_texture” variable which will be loading in the adventurer sprite sheet – The next is the “_spritesheet” which is that batch processing for all those CCSprites that we’re going to be creating. Finally we want to be able to keep track of all our adventurers, so we have an array of them called “_charArray”. You can also see we’re setting the @property values for them as well so we can indirectly reference them in PlayLayer.m

Ok Now we get to the bulk of the class. Don’t worry we’ll break it down right afterwards – first lets write it out in all it’s glory.

PlayLayer.m

#import "PlayLayer.h"
#import "Adventurer.h"

@implementation PlayLayer

@synthesize texture = _texture;
@synthesize spriteSheet = _spriteSheet;

@synthesize charArray = _charArray;

enum {
kTagSpriteSheet = 1,
};

-(id) init{
self = [super init];
if (!self) {
return nil;
}

CCSprite *background = [CCSprite spriteWithFile:@"Terrain.png"];
background.position = ccp(160, 240);
[self addChild:background];

_texture = [[CCTextureCache sharedTextureCache] addImage:@"adventurer.png"];
_spriteSheet = [CCSpriteSheet spriteSheetWithTexture:self.texture capacity:100];
[self addChild:_spriteSheet z:0 tag:kTagSpriteSheet];

self.charArray = [[NSMutableArray alloc] init]; 

[self schedule:@selector(gameLogic:) interval:1.0f];

return self;
}

-(void)addAdventurer {

NSLog(@"Add Adventurer");

NSMutableArray *animFrames = [NSMutableArray array];

[animFrames removeAllObjects];

for (int i = 0; i < 9; i++) {
CCSpriteFrame *frame = [CCSpriteFrame frameWithTexture:self.texture rect:CGRectMake(i*16, 0, 16, 29) offset:CGPointZero];
[animFrames addObject:frame];
}

Adventurer * adventurer = [[[Adventurer alloc] init] autorelease];
if (adventurer != nil) {
CCSpriteFrame *frame1 = [CCSpriteFrame frameWithTexture:self.texture rect:CGRectMake(0, 0, 19, 29) offset:CGPointZero];
adventurer.charSprite = [CCSprite spriteWithSpriteFrame:frame1];

CGSize s = [[CCDirector sharedDirector] winSize];

int minY = adventurer.charSprite.contentSize.height/2;
int maxY = s.height - adventurer.charSprite.contentSize.height/2;
int rangeY = maxY - minY;
int actualY = (arc4random() % rangeY) + minY;

int minX = -300;
int maxX = 0;
int rangeX = maxX - minX;
int actualX = (arc4random() % rangeX) + minX;

adventurer.charSprite.position = ccp(actualX, actualY);

CCAnimation *animation = [CCAnimation animationWithName:@"walk" delay:0.2f frames:animFrames];
CCAnimate *animate = [CCAnimate actionWithAnimation:animation restoreOriginalFrame:NO];
CCSequence *seq = [CCSequence actions: animate,
nil];
adventurer.walkAction  = [CCRepeatForever actionWithAction: seq ];  

id actionMove = [CCMoveTo actionWithDuration:10.0f position:ccp(s.width + 200,actualY)];
id actionMoveDone = [CCCallFuncND actionWithTarget:self selector:@selector(spriteMoveFinished:data:)data:adventurer];
adventurer.moveAction = [CCSequence actions:actionMove, actionMoveDone, nil];

[adventurer.charSprite runAction:adventurer.walkAction];
[adventurer.charSprite runAction:adventurer.moveAction];

[self addChild:adventurer.charSprite];
[_charArray addObject:adventurer];
}

}

-(void)gameLogic:(ccTime)dt {

[self addAdventurer];
}

-(void)spriteMoveFinished:(id)sender data:adv{
Adventurer * adventurer = (Adventurer*)adv;

CGSize s = [[CCDirector sharedDirector] winSize];

int minY = adventurer.charSprite.contentSize.height/2;
int maxY = s.height - adventurer.charSprite.contentSize.height/2;
int rangeY = maxY - minY;
int actualY = (arc4random() % rangeY) + minY;

int minX = -300;
int maxX = 0;
int rangeX = maxX - minX;
int actualX = (arc4random() % rangeX) + minX;

adventurer.charSprite.position = ccp(actualX, actualY);

[adventurer stopAction:adventurer.moveAction];
[adventurer.charSprite runAction:adventurer.moveAction];

}

- (void) dealloc
{
[[CCSpriteFrameCache sharedSpriteFrameCache] removeUnusedSpriteFrames];
[self.charArray removeAllObjects];

[super dealloc];
}

@end

Ok, that wasn't painful - Quick and easy like pulling off a bandaid. Lets get right into breaking everything down:

-(id) init{
self = [super init];
if (!self) {
return nil;
}

CCSprite *background = [CCSprite spriteWithFile:@"Terrain.png"];
background.position = ccp(160, 240);
[self addChild:background];

_texture = [[CCTextureCache sharedTextureCache] addImage:@"adventurer.png"];
_spriteSheet = [CCSpriteSheet spriteSheetWithTexture:self.texture capacity:100];
[self addChild:_spriteSheet z:0 tag:kTagSpriteSheet];

self.charArray = [[NSMutableArray alloc] init]; 

[self schedule:@selector(gameLogic:) interval:1.0f];

return self;
}

Ok so first up - "init" function starts like the adventurer class in that we make sure to call the parent class (in this case CCLayer) and if for some reason that fails then we return nil. We then want to load in the background from "Terrain.png" and position it directly in the middle of the screen (since we know that the default anchorPoint for a sprite is ccp(0.5,0.5)). We then add it to the layer directly.

Next up we are getting all our textures and sprite sheets setup correctly - We will be loading "adventurer.png" into a CCTexture2D variable which we then use to setup the spritesheet with the "spriteSheetWithTexture" call (we could have also called spriteSheetWithFile" and passed in adventurer.png as well, but when we setup progress loading having a "texture" variable will be useful). We then add the spritesheet to the CCLayer directly so when we start adding CCSprites we can take advantage of it's batch processing immediately.

Finally we initialize our NSMutableArray and schedule a callback function "gamelogic" that is on a 1 second timer. The function is listed here:

-(void)gameLogic:(ccTime)dt {

[self addAdventurer];
}

We will use this function to create a new adventurer everyone second into perpetuity.

Next up is function that makes it happen "AddAventurer" - This function will not only create a new character, but will animate it and then move him in the right direction.

Well we've seen this before, we're just setting up the 9 CCSpriteFrames we need to make the "walking" animation.  
Adventurer * adventurer = [[[Adventurer alloc] init] autorelease];
if (adventurer != nil) {
CCSpriteFrame *frame1 = [CCSpriteFrame frameWithTexture:self.texture rect:CGRectMake(0, 0, 19, 29) offset:CGPointZero];
adventurer.charSprite = [CCSprite spriteWithSpriteFrame:frame1];
The next setup is that we now need to create the adventurer instance and initialize the CCSprite member variable "charSprite" with the first CCSpriteFrame and call the function "spriteWithSpriteFrame" and we've got the first frame in!
int minY = adventurer.charSprite.contentSize.height/2;
int maxY = s.height - adventurer.charSprite.contentSize.height/2;
int rangeY = maxY - minY;
int actualY = (arc4random() % rangeY) + minY;

int minX = -300;
int maxX = 0;
int rangeX = maxX - minX;
int actualX = (arc4random() % rangeX) + minX;

adventurer.charSprite.position = ccp(actualX, actualY);
Ok so even though we have the sprite populated, all our sprites are going to be positioned in the lower left hand corner unless we change the position ourselves. So what I'm doing is positioning the sprite off screen within 300 pixels of the left. The hight will run from the top of the individual sprite to the height of the screen so none of our characters get there heads copped off or that we lose their feet offscreen. We get take those generated pixel coordinates and apply them to the sprite directly.
CCAnimation *animation = [CCAnimation animationWithName:@"walk" delay:0.2f frames:animFrames];
CCAnimate *animate = [CCAnimate actionWithAnimation:animation restoreOriginalFrame:NO];
CCSequence *seq = [CCSequence actions: animate,
nil];
adventurer.walkAction  = [CCRepeatForever actionWithAction: seq ];  

id actionMove = [CCMoveTo actionWithDuration:10.0f position:ccp(s.width + 200,actualY)];
id actionMoveDone = [CCCallFuncND actionWithTarget:self selector:@selector(spriteMoveFinished:data:)data:adventurer];
adventurer.moveAction = [CCSequence actions:actionMove, actionMoveDone, nil];

[adventurer.charSprite runAction:adventurer.walkAction];
[adventurer.charSprite runAction:adventurer.moveAction];

[self addChild:adventurer.charSprite];
[_charArray addObject:adventurer];
}
The final portion of "addAdventurer" handles the walking and moving of the character across the screen. We'll take the "animFrames" array of all of the CCSpriteFrame we've stored up and turn them into an animation (they'll be running at a speed of 0.2f from one to the next - it will take approximately 2 seconds to run through the full animation). We then put the animation into a sequence (using the CCSequence class) and finally run the animation forever in a loop using the conveniently named "CCRepeatForever" and store the finished product into our adventure class. So now all we have to do is run the action on the sprite and add the sprite to the spritesheet and we'd have a fully animated sprite on the screen... but we still want to have it move (what the point otherwise)... So we create another CCAction with the CCMoveTo class and for the first time we'll be using another action called "CCCallFuncND" which will allow us to do a callback immediately following this "move to" being complete. ------------------- Side Note: If you want to call a function without any parameters you can use "CCCallFuncN" - it will still pass in the sender (the CCSprite in this case):
id actionMoveDone = [CCCallFuncN actionWithTarget:self selector:@selector(spriteMoveFinished:)];
If you don't want any parameters passed, you can always use just CCCallFunc ------------------- Now there is really only one more thign we need to cover and that's what happens when the CCMoveTo is complete and the "CCCallFuncND" is called:
-(void)spriteMoveFinished:(id)sender data:adv{
Adventurer * adventurer = (Adventurer*)adv;

CGSize s = [[CCDirector sharedDirector] winSize];

int minY = adventurer.charSprite.contentSize.height/2;
int maxY = s.height - adventurer.charSprite.contentSize.height/2;
int rangeY = maxY - minY;
int actualY = (arc4random() % rangeY) + minY;

int minX = -300;
int maxX = 0;
int rangeX = maxX - minX;
int actualX = (arc4random() % rangeX) + minX;

adventurer.charSprite.position = ccp(actualX, actualY);

[adventurer stopAction:adventurer.moveAction];
[adventurer.charSprite runAction:adventurer.moveAction];

}
This is a little redundant and if I wanted to clean this up by creating helper functions like "SetPositionAtX:###AtY:###" I could, but I didn't want to hide anything. This will take the adventurer class that we passed as a parameter with the selector in the CCCallFuncND and uses it to reset the sprite position and reset the animations again. This means we aren't deleting any of the adventurers, just resting them back to a new position offscreen and letting them going to the same location in the original CCMoveTo. It makes for an interesting effect... When the adventurer is first created he will walk directly across the screen, after this reset, he'll walk in angles :) Lastly we have the dealloc function:
- (void) dealloc
{
[[CCSpriteFrameCache sharedSpriteFrameCache] removeUnusedSpriteFrames];
[self.charArray removeAllObjects];

[super dealloc];
}
Just remove the textures and remove all the objects and we're done. Since we never delete the objects or the PlayLayer none of these will be called until later, but again it's a good habit to get in. Once we add the ability to quit the current game, it will make things easier. Source code to this tutorials can be downloaded here: Sprite Tutorial Part 3 Source Code I hope you had fun, I wrote this in one sitting, so if you find any mistakes, remember I'm human :) Please let me know if I missed anything in the comments! Seeeeeeeeeya!

没有评论:

发表评论