Using Cocos2D AtlasSpriteManager
After having a lot of trouble dealing with the issue myself and seeing others having trouble on the cocos2d discussions, I’m going to go ahead and post my solution which so far is working pretty well for me. Please take into account I’m new with Objective C when looking this over.
The problem is each AtlastSpriteManager can 1) refer only to a single sheet image, and 2) each AtlasSprite can only be a child of at AtlasSpriteManager. So what happens if you a few game characters that you want to share certain animations, but not all?
I took the super-class of all my game Sprites, changed its type from a Sprite (or AtlasSpriteManager, which I tried but failed to well) to a generic CocosNode. I added in this to the header. Note that I only needed my Sprites to switch between two different sprite sheets as most.
@interface Touchable : CocosNode <Collideable> { AtlasSprite *activeSprite; AtlasSprite *sprite1; AtlasSprite *sprite2; ... } - (id)init:(CGSize)sizeOne two:(CGSize)sizeTwo; - (AtlasSpriteManager *)spriteManager1; - (AtlasSpriteManager *)spriteManager2; - (void)setSprite:(NSUInteger)newstate; - (void)setActiveSpriteOne; - (void)setActiveSpriteTwo; ...
To the implementation file…
- (AtlasSpriteManager *)spriteManager1 { return nil; } - (AtlasSpriteManager *)spriteManager2 { return nil; } - (id)init:(CGSize)sizeOne two:(CGSize)sizeTwo { self = [super init]; if(self) { sprite1 = [[AtlasSprite spriteWithRect:CGRectMake(sizeOne.width, sizeOne.height, sizeOne.width, sizeOne.height) spriteManager: [self spriteManager1]] retain]; [[self spriteManager1] addChild:sprite1]; if(![[self spriteManager1] parent]) [[[LevelController sharedSingleton] gameLayer] addChild:[self spriteManager1] z:1]; [sprite1 setVisible:NO]; sprite2 = [[AtlasSprite spriteWithRect:CGRectMake(sizeTwo.width, sizeTwo.height, sizeTwo.width, sizeTwo.height) spriteManager: [self spriteManager2]] retain]; [[self spriteManager2] addChild:sprite2]; if(![[self spriteManager2] parent]) [[[LevelControler sharedSingleton] gameLayer] addChild:[self spriteManager2] z:1]; [sprite2 setVisible:NO]; } return self; } - (void)setSprite:(NSUInteger)newstate { } - (void)setActiveSpriteOne { activeSprite = sprite1; [sprite2 setVisible:NO]; [sprite1 setVisible:YES]; } - (void)setActiveSpriteTwo{ activeSprite = sprite2; [sprite2 setVisible:YES]; [sprite1 setVisible:NO]; }
A few methods of my superclass ‘Sprite’ object that I modify slightly to take changes into account. You can no longer want to run the actions (which include animations) on yourself, but on the active sprite.
- (void)setState:(enum ActionFigureState)newstate{ [activeSprite stopAction:[stateActions objectAtIndex:state]]; [self setSprite:newstate]; [activeSprite runAction:[stateActions objectAtIndex:newstate]]; state = newstate; } - (void) setPosition:(CGPoint)pos { [super setPosition:pos]; [activeSprite setPosition:pos]; }
So what we have now is a superclass that is calling a instance method [self spriteManager1] in order to manage attach its two different sprites. Notice that no animation specific code is here in the superclass, except for the fact that a single ‘Sprite’ can work with 2 AtlasSheetManagers at most. You could easily take away this restriction, but I had no need to complicate things.
This gets helpful when I get into a child class that actually knows how to draw itself. Here is my “SmallStick.m” which has an empty header file except for being a child of Touchable. This implementation code also contains all the this object’s interaction with the game, such as collision detection, how to handle if it is touched, and how to add itself to the Chipmunk space.
#define width1 79 #define height1 68 #define columns1 7 #define width2 79 #define height2 68 #define columns2 7 @implementation SmallStick static AtlasSpriteManager *spriteManager1 = nil; static AtlasSpriteManager *spriteManager2 = nil; + (AtlasSpriteManager *)spriteManager1 { @synchronized(spriteManager1) { // I dont actually know if synchronized is needed here if (!spriteManager1) { [Texture2D saveTexParameters]; [Texture2D setAliasTexParameters]; spriteManager1 = [[AtlasSpriteManager alloc] initWithFile:@"smallstick_common.png" capacity:50]; [Texture2D restoreTexParameters]; } return spriteManager1; } return nil; } + (AtlasSpriteManager *)spriteManager2 { @synchronized(spriteManager2) { if (!spriteManager2) { [Texture2D saveTexParameters]; [Texture2D setAliasTexParameters]; spriteManager2 = [[AtlasSpriteManager alloc] initWithFile:@"smallstick_weaponless.png" capacity:50]; [Texture2D restoreTexParameters]; } return spriteManager2; } return nil; } - (AtlasSpriteManager *)spriteManager1 { return [SmallStick spriteManager1]; } - (AtlasSpriteManager *)spriteManager2 { return [SmallStick spriteManager2]; } -(id)init { self = [super init:CGSizeMake(width1, height1) two:CGSizeMake(width2, height2)]; if(self) { ... } return self; } - (void) setSprite:(NSInteger)newstate { switch (newstate) { case fState_charging: case fState_attacking: case fState_recovering: [self setActiveSpriteTwo]; break; default: [self setActiveSpriteOne]; break; } } - (void)loadActions { AtlasAnimation *chargeA = [AtlasAnimation animationWithName:@"weaponless_charge" delay:0.05f]; for(int i=0;i<18;i++) { int x= i % columns2; int y= i / columns2; [chargeA addFrameWithRect: CGRectMake(x*width2, y*height2, width2, height2) ]; } Action *charge = [RepeatForever actionWithAction:[Animate actionWithAnimation: chargeA]]; ... stateActions = [[NSArray alloc] initWithObjects: charge, dying, ..., nil];
The part I like about this is when I want to reuse the ‘smallstick_common.png’ atlas sheet I can just not override the class method +(AtlasSpriteManager *)spriteManager2 in my child classes.
Don’t forget to remove the sprite from the AtlasManager when you’re done.
[[self spriteManager1] removeChild:sprite1 cleanup:YES]; [[self spriteManager2] removeChild:sprite2 cleanup:YES];
First of all, I hope this doesn’t have some hidden downside. As I said, I’m new to the platform. But I hope somebody finds this useful.
This is a great tutorial that for some reason I can’t get working. If you have the time can you post the source code? Thanks!
I did post the code…right above. What problems are you getting?
Thanks for sharing your insight. However, I’m wondering what you would do if your sprite makes references to more than 2 spritesheets?
Well there are a few things you could do. You could write a more general solution that instead of using hacked function1, function2, function3 methods, just set up everything in an array and run through it there. In each ‘Touchable’ you could keep a static copy of an array that contains references to the spritesheets, and make those changes accordingly. Or you could literally start just adding more ‘hack’ functions in there, so you can use a 3rd or 4th spritesheet. I actually did this when I added a new object needed a third spritesheet.
However, I knew beforehand that no single Sprite would need to access more than 2 spritesheets, so this way took a little less time to throw together. It’s not extensible coding, but sometimes you have to make sacrifices when pumping out updates every 1.5 weeks!
But the question is, since a single spritesheet can be up to 1024×1024, would you really need a sprite that needs to draw from more than 2-3 of them? Maybe you do, but I didn’t for my game. Sorry I didn’t come up with a more generic solution, but I’m still relatively new to ObjC and Cocos2d.
Thanks for your reply. I thought maybe you somehow use those 2 spritesheets to swap between all the spritesheets (if you have more than 2). I guess there is no easy way out
there are a couple of snippets that I did not understand:
[Texture2D saveTexParameters];
[Texture2D setAliasTexParameters];
spriteManager1 = [[AtlasSpriteManager alloc] initWithFile:@”smallstick_common.png” capacity:50];
[Texture2D restoreTexParameters];
what exactly is being done here? What are all those calls made to Texture2D? I tried looking at Texture2D.h but with basically no OpenGL background I couldn’t understand.
Also, what does “capacity” mean for a textureAtlas anyway?
You can take out those three Texture2D lines with no negative effects. So you can just have:
spriteManager1 = [[AtlasSpriteManager alloc] initWithFile:@”smallstick_common.png” capacity:50];
All those are doing is setting up the texture tool to use aliased textures (versus anti-aliased). Capacity refers to the number of AtlasSprites frames you plan to add as a child to that AtlasSpriteManager. If you are creating 50 frames (say 5 animations with 10 frames each) then you give it a capacity of 50. However, you do not have to be exact with this, you could leave it at 1 and it would just take a little more time at run-time to resize the array.
Nice tutorial! The persuit of performance using AtlasSprites and the AtlasSpriteManager is taking a lot of my time also.
Question: are you creating an “inside particular” sprite manager for each “sprite” you raise? Is this because of the difficulty of controlling the z?
I am using a single (shared) sprite manager as a “Static” variable in my “enemy” class, used by all sprite instances.
I’m creating an ‘inside particular’ because for one ‘sprite’ character that has like 60-100 frames for all the animations, it approaches the max texture size of 1024×1024. So I end using 4-6 different sheets, each stored in their static members, which allows the managers to be re-used for sprites that share frames.
Some of this implementation is particular to my own game, though it may be helpful for other games.
Thanks for answering! For all ‘stickguys’ instances that look alike, you have a single, actually two sprite managers… But the point is: is those two sprite managers shared (like a ‘static-class-variable’) by all soldiers – or if you have 10 soldiers of the same look in scene you have 20 (because of the two) sprite managers on memory?
I suppose even if you have several sprite managers using the same texture, cocos2d keeps a single texture on the openGLES memory – but someone more smart than me could really confirm it commenting after.
In the end, I guess the AtlasSpriteManager could be ‘dropped’ from cocos2d… and its functionality could be put inside AtlasSprite directly – and atlasSprites would be using a Texture2d or something directly. I think it would make things easier and more flexible (mainly about Z).
By the way, congrats for StickWars! Wish you all the success!
That is the point of using those static-class-variable managers…they are only allocated ONCE, so if I have 10 soldiers who all use two different sprite sheets, I only have two managers (one per sheet) in memory, with each AtlasSprite being attached to the correct manager. You could think of them as singletons, as there will only be one ‘alloc’ called no matter how many objects use it.
The annoying part with this is when applying actions, you have to track the state to remember whether or not to apply the action to the AtlasSprite from sheet1 or sheet 2. In my game, this wasn’t an issue since I didn’t use actions that much, but for other games this could be a major hassle.
You are correct, you could just create many copies of the SpriteManagers and the texture manager would only load the sprite sheet once. However, you can’t just make one AtlasSpriteManager per AtlasSprite as that just as slow as Sprite, so you still need to share the managers to some extent. And each time you create another Manager for the same texture, you are throwing away the entire performance gain that you get from using a single Manager for one texture.
Read on the cocos2d forums about why AtlasSpriteManager is there. It will not be dropped, because it is a useful tool that you can use to ‘batch’ opengl calls without having to delve into the OpenGL yourself. By twisting your code around to fit this seemingly arbitrary design structure, you vastly increase the efficiency of the OpenGL code running underneath it by allowing OpenGL to copy many different parts of the same texture in one big operation, avoiding the overhead of many small operations.
Thanks, and good luck on your project!
where is to download the above source code.i cannot find the download link in whole page.
This code isn’t available as a separate download…you should be able to copy and paste it out directly out of the boxes.
However, this code was written almost a year ago and the cocos2d library has changed substantially since then. I would recommend reading this to understand how I got this to work, but check the cocos2d page for easier methods which were added in later on.